Add preview pack smoke gate

This commit is contained in:
Affaan Mustafa
2026-05-17 15:35:23 -04:00
parent 1a384dc533
commit 3215e655ef
9 changed files with 724 additions and 12 deletions

View File

@@ -16,6 +16,7 @@ surfaces, or posting announcements.
| `docs/architecture/harness-adapter-compliance.md` | Adapter matrix and scorecard | Verified by `npm run harness:adapters -- --check` | | `docs/architecture/harness-adapter-compliance.md` | Adapter matrix and scorecard | Verified by `npm run harness:adapters -- --check` |
| `docs/architecture/observability-readiness.md` | Local operator-readiness gate | Verified by `npm run observability:ready` | | `docs/architecture/observability-readiness.md` | Local operator-readiness gate | Verified by `npm run observability:ready` |
| `docs/architecture/progress-sync-contract.md` | GitHub, Linear, handoff, roadmap, and work-item sync boundary | Checked by `node scripts/platform-audit.js --format json --allow-untracked docs/drafts/` | | `docs/architecture/progress-sync-contract.md` | GitHub, Linear, handoff, roadmap, and work-item sync boundary | Checked by `node scripts/platform-audit.js --format json --allow-untracked docs/drafts/` |
| `scripts/preview-pack-smoke.js` | Deterministic preview-pack smoke gate | Verified by `npm run preview-pack:smoke` |
| `docs/releases/2.0.0-rc.1/release-notes.md` | GitHub release copy source | Must be refreshed with final live release/package/plugin URLs before publication | | `docs/releases/2.0.0-rc.1/release-notes.md` | GitHub release copy source | Must be refreshed with final live release/package/plugin URLs before publication |
| `docs/releases/2.0.0-rc.1/quickstart.md` | Clone-to-first-workflow path | Covers clone, install, verify, first skill, and harness switch | | `docs/releases/2.0.0-rc.1/quickstart.md` | Clone-to-first-workflow path | Covers clone, install, verify, first skill, and harness switch |
| `docs/releases/2.0.0-rc.1/launch-checklist.md` | Operator launch checklist | Must remain approval-gated for release, package, plugin, and announcement actions | | `docs/releases/2.0.0-rc.1/launch-checklist.md` | Operator launch checklist | Must remain approval-gated for release, package, plugin, and announcement actions |
@@ -69,6 +70,7 @@ Run these from the exact release commit before publication:
```bash ```bash
git status --short --branch git status --short --branch
node scripts/platform-audit.js --format json --allow-untracked docs/drafts/ node scripts/platform-audit.js --format json --allow-untracked docs/drafts/
npm run preview-pack:smoke
npm run harness:adapters -- --check npm run harness:adapters -- --check
npm run harness:audit -- --format json npm run harness:audit -- --format json
npm run observability:ready npm run observability:ready

View File

@@ -78,6 +78,7 @@ Record the exact commit SHA and command output before any publication action:
| Evidence | Command | Required result | Recorded output | | Evidence | Command | Required result | Recorded output |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Clean release branch | `git status --short --branch` | On intended release commit; no unrelated files | Pending final strict clean-checkout release pass; `publication-evidence-2026-05-17.md` records current `main` with unrelated untracked `docs/drafts/` | | Clean release branch | `git status --short --branch` | On intended release commit; no unrelated files | Pending final strict clean-checkout release pass; `publication-evidence-2026-05-17.md` records current `main` with unrelated untracked `docs/drafts/` |
| Preview-pack smoke | `npm run preview-pack:smoke` | Preview pack artifacts, Hermes boundary, final verification command list, and publication blockers pass | Pending final strict clean-checkout release pass; deterministic smoke gate is in-tree |
| Harness audit | `npm run harness:audit -- --format json` | 70/70 passing | `publication-evidence-2026-05-17.md`: 70/70 | | Harness audit | `npm run harness:audit -- --format json` | 70/70 passing | `publication-evidence-2026-05-17.md`: 70/70 |
| Adapter scorecard | `npm run harness:adapters -- --check` | PASS | `publication-evidence-2026-05-16.md`: PASS, 11 adapters | | Adapter scorecard | `npm run harness:adapters -- --check` | PASS | `publication-evidence-2026-05-16.md`: PASS, 11 adapters |
| Observability readiness | `npm run observability:ready` | 21/21 passing | `publication-evidence-2026-05-17.md`: 21/21, ready yes | | Observability readiness | `npm run observability:ready` | 21/21 passing | `publication-evidence-2026-05-17.md`: 21/21, ready yes |

View File

@@ -80,6 +80,7 @@
"scripts/observability-readiness.js", "scripts/observability-readiness.js",
"scripts/operator-readiness-dashboard.js", "scripts/operator-readiness-dashboard.js",
"scripts/platform-audit.js", "scripts/platform-audit.js",
"scripts/preview-pack-smoke.js",
"scripts/hooks/", "scripts/hooks/",
"scripts/install-apply.js", "scripts/install-apply.js",
"scripts/install-plan.js", "scripts/install-plan.js",
@@ -300,6 +301,7 @@
"harness:audit": "node scripts/harness-audit.js", "harness:audit": "node scripts/harness-audit.js",
"observability:ready": "node scripts/observability-readiness.js", "observability:ready": "node scripts/observability-readiness.js",
"operator:dashboard": "node scripts/operator-readiness-dashboard.js", "operator:dashboard": "node scripts/operator-readiness-dashboard.js",
"preview-pack:smoke": "node scripts/preview-pack-smoke.js",
"platform:audit": "node scripts/platform-audit.js", "platform:audit": "node scripts/platform-audit.js",
"discussion:audit": "node scripts/discussion-audit.js", "discussion:audit": "node scripts/discussion-audit.js",
"security:ioc-scan": "node scripts/ci/scan-supply-chain-iocs.js", "security:ioc-scan": "node scripts/ci/scan-supply-chain-iocs.js",

View File

@@ -469,6 +469,7 @@ function buildRequirements(rootDir, platformReport) {
const publicationReadiness = readText(rootDir, 'docs/releases/2.0.0-rc.1/publication-readiness.md'); const publicationReadiness = readText(rootDir, 'docs/releases/2.0.0-rc.1/publication-readiness.md');
const namingMatrix = readText(rootDir, 'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md'); const namingMatrix = readText(rootDir, 'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md');
const previewManifest = readText(rootDir, 'docs/releases/2.0.0-rc.1/preview-pack-manifest.md'); const previewManifest = readText(rootDir, 'docs/releases/2.0.0-rc.1/preview-pack-manifest.md');
const previewPackSmoke = readText(rootDir, 'scripts/preview-pack-smoke.js');
const progressSync = readText(rootDir, 'docs/architecture/progress-sync-contract.md'); const progressSync = readText(rootDir, 'docs/architecture/progress-sync-contract.md');
const observabilityReadiness = readText(rootDir, 'docs/architecture/observability-readiness.md'); const observabilityReadiness = readText(rootDir, 'docs/architecture/observability-readiness.md');
const stalePrSalvage = readText(rootDir, 'docs/stale-pr-salvage-ledger.md'); const stalePrSalvage = readText(rootDir, 'docs/stale-pr-salvage-ledger.md');
@@ -478,6 +479,22 @@ function buildRequirements(rootDir, platformReport) {
const packageJson = readPackage(rootDir); const packageJson = readPackage(rootDir);
const scripts = packageJson.scripts || {}; const scripts = packageJson.scripts || {};
const legacyContext = { stalePrSalvage, legacyInventory, roadmap }; const legacyContext = { stalePrSalvage, legacyInventory, roadmap };
const previewPackManifestReady = includesAll(previewManifest, [
'publication-readiness.md',
'release-notes.md',
'quickstart.md'
]);
const previewPackSmokeReady = scripts['preview-pack:smoke'] === 'node scripts/preview-pack-smoke.js'
&& fileExists(rootDir, 'scripts/preview-pack-smoke.js')
&& includesAll(previewManifest, ['scripts/preview-pack-smoke.js', 'npm run preview-pack:smoke'])
&& includesAll(previewPackSmoke, [
'ecc.preview-pack-smoke.v1',
'preview-pack-artifacts-present',
'hermes-boundary-sanitized',
'publication-blockers-preserved'
]);
const hermesArtifactsReady = fileExists(rootDir, 'docs/HERMES-SETUP.md')
&& fileExists(rootDir, 'skills/hermes-imports/SKILL.md');
const githubLive = !platformReport.github.skipped && platformReport.github.totals.errors === 0; const githubLive = !platformReport.github.skipped && platformReport.github.totals.errors === 0;
const queuesCurrent = githubLive const queuesCurrent = githubLive
@@ -535,23 +552,29 @@ function buildRequirements(rootDir, platformReport) {
'ecc-preview-pack', 'ecc-preview-pack',
'ECC 2.0 preview pack ready', 'ECC 2.0 preview pack ready',
'docs/releases/2.0.0-rc.1/preview-pack-manifest.md', 'docs/releases/2.0.0-rc.1/preview-pack-manifest.md',
includesAll(previewManifest, ['publication-readiness.md', 'release-notes.md', 'quickstart.md']) ? 'in_progress' : 'not_complete', previewPackManifestReady && previewPackSmokeReady ? 'current' : previewPackManifestReady ? 'in_progress' : 'not_complete',
includesAll(previewManifest, ['publication-readiness.md', 'release-notes.md', 'quickstart.md']) previewPackManifestReady && previewPackSmokeReady
? 'preview pack manifest and deterministic smoke gate are in-tree'
: previewPackManifestReady
? 'preview pack manifest is in-tree' ? 'preview pack manifest is in-tree'
: 'preview pack manifest is incomplete', : 'preview pack manifest is incomplete',
'final clean-checkout release approval and publish evidence still pending' previewPackManifestReady && previewPackSmokeReady
? 'repeat clean-checkout preview-pack smoke before publication'
: 'final clean-checkout release approval and publish evidence still pending'
), ),
buildRequirement( buildRequirement(
'hermes-specialized-skills', 'hermes-specialized-skills',
'Include Hermes specialized skills safely', 'Include Hermes specialized skills safely',
'docs/HERMES-SETUP.md and skills/hermes-imports/SKILL.md', 'docs/HERMES-SETUP.md and skills/hermes-imports/SKILL.md',
fileExists(rootDir, 'docs/HERMES-SETUP.md') && fileExists(rootDir, 'skills/hermes-imports/SKILL.md') hermesArtifactsReady && previewPackSmokeReady ? 'current' : hermesArtifactsReady ? 'in_progress' : 'not_complete',
? 'in_progress' hermesArtifactsReady && previewPackSmokeReady
: 'not_complete', ? 'Hermes setup/import artifacts are covered by preview-pack smoke'
fileExists(rootDir, 'docs/HERMES-SETUP.md') && fileExists(rootDir, 'skills/hermes-imports/SKILL.md') : hermesArtifactsReady
? 'Hermes setup and import skill are present' ? 'Hermes setup and import skill are present'
: 'Hermes setup/import artifacts missing', : 'Hermes setup/import artifacts missing',
'final preview-pack smoke and release review pending' hermesArtifactsReady && previewPackSmokeReady
? 'repeat preview-pack smoke before release review'
: 'final preview-pack smoke and release review pending'
), ),
buildRequirement( buildRequirement(
'naming-and-plugin-publication', 'naming-and-plugin-publication',

View File

@@ -0,0 +1,353 @@
#!/usr/bin/env node
'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const RELEASE = '2.0.0-rc.1';
const RELEASE_DIR = `docs/releases/${RELEASE}`;
const SCHEMA_VERSION = 'ecc.preview-pack-smoke.v1';
const REQUIRED_ARTIFACTS = [
'README.md',
'docs/HERMES-SETUP.md',
'skills/hermes-imports/SKILL.md',
'docs/architecture/cross-harness.md',
'docs/architecture/harness-adapter-compliance.md',
'docs/architecture/observability-readiness.md',
'docs/architecture/progress-sync-contract.md',
'scripts/preview-pack-smoke.js',
`${RELEASE_DIR}/release-notes.md`,
`${RELEASE_DIR}/quickstart.md`,
`${RELEASE_DIR}/launch-checklist.md`,
`${RELEASE_DIR}/publication-readiness.md`,
`${RELEASE_DIR}/publication-evidence-2026-05-15.md`,
`${RELEASE_DIR}/publication-evidence-2026-05-16.md`,
`${RELEASE_DIR}/publication-evidence-2026-05-17.md`,
`${RELEASE_DIR}/operator-readiness-dashboard-2026-05-17.md`,
`${RELEASE_DIR}/naming-and-publication-matrix.md`,
`${RELEASE_DIR}/x-thread.md`,
`${RELEASE_DIR}/linkedin-post.md`,
`${RELEASE_DIR}/article-outline.md`,
`${RELEASE_DIR}/telegram-handoff.md`,
`${RELEASE_DIR}/demo-prompts.md`,
];
const REQUIRED_VERIFICATION_COMMANDS = [
'git status --short --branch',
'node scripts/platform-audit.js --format json --allow-untracked docs/drafts/',
'npm run preview-pack:smoke',
'npm run harness:adapters -- --check',
'npm run harness:audit -- --format json',
'npm run observability:ready',
'npm run security:ioc-scan',
'npm audit --audit-level=moderate',
'npm audit signatures',
'node tests/docs/ecc2-release-surface.test.js',
'node tests/run-all.js',
'cd ecc2 && cargo test',
];
const REQUIRED_PUBLICATION_BLOCKERS = [
'GitHub prerelease `v2.0.0-rc.1`',
'npm `ecc-universal@2.0.0-rc.1`',
'Claude plugin tag',
'Codex repo-marketplace distribution evidence',
'ECC Tools billing/product readiness',
];
const HERMES_BOUNDARY_MARKERS = [
'Public Release Candidate Scope',
'ECC v2.0.0-rc.1 documents the Hermes surface',
'Sanitization Checklist',
'Do not ship raw workspace exports',
'Output Contract',
];
function usage() {
console.log([
'Usage: node scripts/preview-pack-smoke.js [--format <text|json>] [--root <dir>]',
'',
'Deterministic smoke gate for the ECC 2.0 rc.1 preview pack.',
'',
'Options:',
' --format <text|json> Output format (default: text)',
' --root <dir> Repository root to inspect (default: cwd)',
' --help, -h Show this help',
].join('\n'));
}
function readArgValue(args, index, flagName) {
const value = args[index + 1];
if (!value || value.startsWith('--')) {
throw new Error(`${flagName} requires a value`);
}
return value;
}
function parseArgs(argv) {
const args = argv.slice(2);
const parsed = {
format: 'text',
help: false,
root: path.resolve(process.cwd()),
};
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === '--help' || arg === '-h') {
parsed.help = true;
continue;
}
if (arg === '--format') {
parsed.format = readArgValue(args, index, arg).toLowerCase();
index += 1;
continue;
}
if (arg.startsWith('--format=')) {
parsed.format = arg.slice('--format='.length).toLowerCase();
continue;
}
if (arg === '--root') {
parsed.root = path.resolve(readArgValue(args, index, arg));
index += 1;
continue;
}
if (arg.startsWith('--root=')) {
parsed.root = path.resolve(arg.slice('--root='.length));
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}
if (!['text', 'json'].includes(parsed.format)) {
throw new Error(`Invalid format: ${parsed.format}. Use text or json.`);
}
return parsed;
}
function readText(rootDir, relativePath) {
try {
return fs.readFileSync(path.join(rootDir, relativePath), 'utf8');
} catch (_error) {
return '';
}
}
function fileExists(rootDir, relativePath) {
return fs.existsSync(path.join(rootDir, relativePath));
}
function safeParseJson(text) {
if (!text.trim()) {
return null;
}
try {
return JSON.parse(text);
} catch (_error) {
return null;
}
}
function includesAll(text, needles) {
return needles.every(needle => text.includes(needle));
}
function lineNumberForIndex(text, index) {
return text.slice(0, index).split('\n').length;
}
function findForbiddenContent(rootDir, relativePaths) {
const offenders = [];
const privatePathPattern = /\/Users\/(?!\.\.\.)[A-Za-z0-9._-]+|\/home\/(?!user|runner)[A-Za-z0-9._-]+/g;
for (const relativePath of relativePaths) {
const text = readText(rootDir, relativePath);
if (!text) {
continue;
}
for (const match of text.matchAll(privatePathPattern)) {
offenders.push({
path: relativePath,
line: lineNumberForIndex(text, match.index),
marker: match[0],
});
}
}
return offenders;
}
function makeCheck(id, status, evidence, fix) {
return {
id,
status,
evidence,
fix: status === 'pass' ? '' : fix,
};
}
function buildReport(options = {}) {
const rootDir = path.resolve(options.root || process.cwd());
const packageJson = safeParseJson(readText(rootDir, 'package.json')) || {};
const packageScripts = packageJson.scripts || {};
const packageFiles = Array.isArray(packageJson.files) ? packageJson.files : [];
const manifestPath = `${RELEASE_DIR}/preview-pack-manifest.md`;
const manifest = readText(rootDir, manifestPath);
const hermesSetup = readText(rootDir, 'docs/HERMES-SETUP.md');
const hermesSkill = readText(rootDir, 'skills/hermes-imports/SKILL.md');
const missingArtifacts = REQUIRED_ARTIFACTS.filter(relativePath => !fileExists(rootDir, relativePath));
const unlistedArtifacts = REQUIRED_ARTIFACTS.filter(relativePath => !manifest.includes(`\`${relativePath}\``));
const missingCommands = REQUIRED_VERIFICATION_COMMANDS.filter(command => !manifest.includes(command));
const missingBlockers = REQUIRED_PUBLICATION_BLOCKERS.filter(blocker => !manifest.includes(blocker));
const missingHermesMarkers = HERMES_BOUNDARY_MARKERS.filter(marker => !`${hermesSetup}\n${hermesSkill}`.includes(marker));
const forbiddenContent = findForbiddenContent(rootDir, [
...REQUIRED_ARTIFACTS,
manifestPath,
'docs/business/social-launch-copy.md',
]);
const checks = [
makeCheck(
'preview-pack-script-registered',
packageScripts['preview-pack:smoke'] === 'node scripts/preview-pack-smoke.js'
&& packageFiles.includes('scripts/preview-pack-smoke.js')
&& fileExists(rootDir, 'scripts/preview-pack-smoke.js')
? 'pass'
: 'fail',
'package script and npm package file entry for preview-pack smoke gate',
'Add preview-pack:smoke to package scripts and include scripts/preview-pack-smoke.js in package files.'
),
makeCheck(
'preview-pack-artifacts-present',
missingArtifacts.length === 0 && unlistedArtifacts.length === 0 ? 'pass' : 'fail',
missingArtifacts.length === 0 && unlistedArtifacts.length === 0
? `${REQUIRED_ARTIFACTS.length} required artifacts exist and are listed in the manifest`
: `missing artifacts: ${missingArtifacts.join(', ') || 'none'}; unlisted artifacts: ${unlistedArtifacts.join(', ') || 'none'}`,
'Restore missing preview-pack artifacts and list every required artifact in preview-pack-manifest.md.'
),
makeCheck(
'final-verification-commands-listed',
missingCommands.length === 0 ? 'pass' : 'fail',
missingCommands.length === 0
? `${REQUIRED_VERIFICATION_COMMANDS.length} final verification commands are listed`
: `missing commands: ${missingCommands.join('; ')}`,
'Add the missing final verification commands to preview-pack-manifest.md.'
),
makeCheck(
'hermes-boundary-sanitized',
missingHermesMarkers.length === 0 && forbiddenContent.length === 0 ? 'pass' : 'fail',
missingHermesMarkers.length === 0 && forbiddenContent.length === 0
? 'Hermes setup and import skill preserve the public sanitization boundary'
: `missing markers: ${missingHermesMarkers.join(', ') || 'none'}; forbidden content: ${forbiddenContent.map(item => `${item.path}:${item.line}`).join(', ') || 'none'}`,
'Restore Hermes sanitization language and remove private local paths from preview-pack docs.'
),
makeCheck(
'publication-blockers-preserved',
missingBlockers.length === 0
&& /approval-gated release, package, plugin, and\s+announcement steps/.test(manifest)
? 'pass'
: 'fail',
missingBlockers.length === 0
? 'publication remains explicitly approval-gated'
: `missing blockers: ${missingBlockers.join(', ')}`,
'Keep publication blockers explicit until the live release, package, plugin, and billing surfaces exist.'
),
];
const failed = checks.filter(check => check.status !== 'pass');
const digest = crypto
.createHash('sha256')
.update(JSON.stringify(checks.map(check => [check.id, check.status, check.evidence])))
.digest('hex')
.slice(0, 12);
return {
schema_version: SCHEMA_VERSION,
release: RELEASE,
ready: failed.length === 0,
digest,
summary: {
passed: checks.length - failed.length,
failed: failed.length,
total: checks.length,
},
checks,
};
}
function renderText(report) {
const lines = [
'ECC preview pack smoke',
`Release: ${report.release}`,
`Ready: ${report.ready ? 'yes' : 'no'}`,
`Digest: ${report.digest}`,
'',
'Checks:',
];
for (const check of report.checks) {
lines.push(`- ${check.status} ${check.id}: ${check.evidence}`);
if (check.fix) {
lines.push(` fix: ${check.fix}`);
}
}
lines.push('');
lines.push(`Passed: ${report.summary.passed}`);
lines.push(`Failed: ${report.summary.failed}`);
return `${lines.join('\n')}\n`;
}
function main() {
let parsed;
try {
parsed = parseArgs(process.argv);
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
if (parsed.help) {
usage();
return;
}
const report = buildReport({ root: parsed.root });
if (parsed.format === 'json') {
console.log(JSON.stringify(report, null, 2));
} else {
process.stdout.write(renderText(report));
}
if (!report.ready) {
process.exit(2);
}
}
if (require.main === module) {
main();
}
module.exports = {
REQUIRED_ARTIFACTS,
REQUIRED_PUBLICATION_BLOCKERS,
REQUIRED_VERIFICATION_COMMANDS,
buildReport,
parseArgs,
renderText,
};

View File

@@ -171,6 +171,7 @@ test('preview pack manifest assembles release, Hermes, and publication gates', (
'docs/HERMES-SETUP.md', 'docs/HERMES-SETUP.md',
'skills/hermes-imports/SKILL.md', 'skills/hermes-imports/SKILL.md',
'docs/architecture/harness-adapter-compliance.md', 'docs/architecture/harness-adapter-compliance.md',
'scripts/preview-pack-smoke.js',
'docs/releases/2.0.0-rc.1/publication-readiness.md', 'docs/releases/2.0.0-rc.1/publication-readiness.md',
'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md', 'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md',
]) { ]) {
@@ -189,6 +190,7 @@ test('preview pack manifest assembles release, Hermes, and publication gates', (
assert.ok(manifest.includes('no raw workspace exports')); assert.ok(manifest.includes('no raw workspace exports'));
assert.ok(manifest.includes('Final Verification Commands')); assert.ok(manifest.includes('Final Verification Commands'));
assert.ok(manifest.includes('npm run preview-pack:smoke'));
assert.ok(manifest.includes('Reference-Inspired Adapter Direction')); assert.ok(manifest.includes('Reference-Inspired Adapter Direction'));
}); });
@@ -263,6 +265,8 @@ test('publication readiness checklist gates public release actions on evidence',
} }
assert.ok(source.includes('publication-evidence-2026-05-15.md')); assert.ok(source.includes('publication-evidence-2026-05-15.md'));
assert.ok(source.includes('Preview-pack smoke'));
assert.ok(source.includes('npm run preview-pack:smoke'));
assert.ok(may15Evidence.includes('PR #1921')); assert.ok(may15Evidence.includes('PR #1921'));
assert.ok(may15Evidence.includes('PR #1933')); assert.ok(may15Evidence.includes('PR #1933'));
assert.ok(may15Evidence.includes('PR #1934')); assert.ok(may15Evidence.includes('PR #1934'));

View File

@@ -59,6 +59,7 @@ function buildExpectedPublishPaths(repoRoot) {
"scripts/observability-readiness.js", "scripts/observability-readiness.js",
"scripts/operator-readiness-dashboard.js", "scripts/operator-readiness-dashboard.js",
"scripts/platform-audit.js", "scripts/platform-audit.js",
"scripts/preview-pack-smoke.js",
"scripts/skill-create-output.js", "scripts/skill-create-output.js",
"scripts/repair.js", "scripts/repair.js",
"scripts/harness-adapter-compliance.js", "scripts/harness-adapter-compliance.js",
@@ -129,6 +130,7 @@ function main() {
"scripts/consult.js", "scripts/consult.js",
"scripts/discussion-audit.js", "scripts/discussion-audit.js",
"scripts/operator-readiness-dashboard.js", "scripts/operator-readiness-dashboard.js",
"scripts/preview-pack-smoke.js",
"scripts/work-items.js", "scripts/work-items.js",
"scripts/platform-audit.js", "scripts/platform-audit.js",
".gemini/GEMINI.md", ".gemini/GEMINI.md",

View File

@@ -32,18 +32,26 @@ function seedRepo(rootDir, overrides = {}) {
files: [ files: [
'scripts/observability-readiness.js', 'scripts/observability-readiness.js',
'scripts/operator-readiness-dashboard.js', 'scripts/operator-readiness-dashboard.js',
'scripts/platform-audit.js' 'scripts/platform-audit.js',
'scripts/preview-pack-smoke.js'
], ],
scripts: { scripts: {
'discussion:audit': 'node scripts/discussion-audit.js', 'discussion:audit': 'node scripts/discussion-audit.js',
'observability:ready': 'node scripts/observability-readiness.js', 'observability:ready': 'node scripts/observability-readiness.js',
'operator:dashboard': 'node scripts/operator-readiness-dashboard.js', 'operator:dashboard': 'node scripts/operator-readiness-dashboard.js',
'platform:audit': 'node scripts/platform-audit.js', 'platform:audit': 'node scripts/platform-audit.js',
'preview-pack:smoke': 'node scripts/preview-pack-smoke.js',
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js', 'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js',
'security:advisory-sources': 'node scripts/ci/supply-chain-advisory-sources.js' 'security:advisory-sources': 'node scripts/ci/supply-chain-advisory-sources.js'
} }
}, null, 2), }, null, 2),
'scripts/operator-readiness-dashboard.js': 'operator dashboard generator', 'scripts/operator-readiness-dashboard.js': 'operator dashboard generator',
'scripts/preview-pack-smoke.js': [
'ecc.preview-pack-smoke.v1',
'preview-pack-artifacts-present',
'hermes-boundary-sanitized',
'publication-blockers-preserved'
].join('\n'),
'docs/ECC-2.0-GA-ROADMAP.md': [ 'docs/ECC-2.0-GA-ROADMAP.md': [
'https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1', 'https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1',
'Linear ITO-44 ITO-59', 'Linear ITO-44 ITO-59',
@@ -63,7 +71,11 @@ function seedRepo(rootDir, overrides = {}) {
].join('\n'), ].join('\n'),
'docs/releases/2.0.0-rc.1/publication-readiness.md': 'Claude plugin Codex plugin', 'docs/releases/2.0.0-rc.1/publication-readiness.md': 'Claude plugin Codex plugin',
'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md': 'Claude plugin Codex plugin npm package Publication Paths', 'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md': 'Claude plugin Codex plugin npm package Publication Paths',
'docs/releases/2.0.0-rc.1/preview-pack-manifest.md': 'publication-readiness.md release-notes.md quickstart.md', 'docs/releases/2.0.0-rc.1/preview-pack-manifest.md': [
'publication-readiness.md release-notes.md quickstart.md',
'`scripts/preview-pack-smoke.js`',
'npm run preview-pack:smoke'
].join('\n'),
'docs/releases/2.0.0-rc.1/release-notes.md': 'release notes', 'docs/releases/2.0.0-rc.1/release-notes.md': 'release notes',
'docs/releases/2.0.0-rc.1/x-thread.md': 'x thread', 'docs/releases/2.0.0-rc.1/x-thread.md': 'x thread',
'docs/releases/2.0.0-rc.1/linkedin-post.md': 'linkedin post', 'docs/releases/2.0.0-rc.1/linkedin-post.md': 'linkedin post',
@@ -77,8 +89,8 @@ function seedRepo(rootDir, overrides = {}) {
'PR queue', 'PR queue',
'Not complete' 'Not complete'
].join('\n'), ].join('\n'),
'docs/HERMES-SETUP.md': 'Hermes setup', 'docs/HERMES-SETUP.md': 'Hermes setup Public Release Candidate Scope',
'skills/hermes-imports/SKILL.md': 'Hermes imports', 'skills/hermes-imports/SKILL.md': 'Hermes imports Sanitization Checklist Do not ship raw workspace exports Output Contract',
'docs/stale-pr-salvage-ledger.md': [ 'docs/stale-pr-salvage-ledger.md': [
'Remaining Manual-Review Backlog', 'Remaining Manual-Review Backlog',
'Linear ITO-55', 'Linear ITO-55',
@@ -218,6 +230,18 @@ function runTests() {
assert.strictEqual(report.ready, false); assert.strictEqual(report.ready, false);
assert.strictEqual(report.publicationReady, false); assert.strictEqual(report.publicationReady, false);
assert.ok(report.requirements.some(item => item.id === 'completion-dashboard' && item.status === 'complete')); assert.ok(report.requirements.some(item => item.id === 'completion-dashboard' && item.status === 'complete'));
assert.ok(report.requirements.some(item => (
item.id === 'ecc-preview-pack'
&& item.status === 'current'
&& item.evidence.includes('deterministic smoke gate')
&& item.gap === 'repeat clean-checkout preview-pack smoke before publication'
)));
assert.ok(report.requirements.some(item => (
item.id === 'hermes-specialized-skills'
&& item.status === 'current'
&& item.evidence.includes('covered by preview-pack smoke')
&& item.gap === 'repeat preview-pack smoke before release review'
)));
assert.ok(report.requirements.some(item => item.id === 'ecc-tools-next-level' && item.status === 'in_progress')); assert.ok(report.requirements.some(item => item.id === 'ecc-tools-next-level' && item.status === 'in_progress'));
assert.ok(report.requirements.some(item => ( assert.ok(report.requirements.some(item => (
item.id === 'agentshield-enterprise-iteration' item.id === 'agentshield-enterprise-iteration'
@@ -253,6 +277,8 @@ function runTests() {
&& item.gap === 'repeat Linear/project status update and local work-items sync after each significant merge batch' && item.gap === 'repeat Linear/project status update and local work-items sync after each significant merge batch'
))); )));
assert.ok(report.top_actions.some(item => item.id === 'naming-and-plugin-publication')); assert.ok(report.top_actions.some(item => item.id === 'naming-and-plugin-publication'));
assert.ok(!report.top_actions.some(item => item.id === 'ecc-preview-pack'));
assert.ok(!report.top_actions.some(item => item.id === 'hermes-specialized-skills'));
assert.ok(!report.top_actions.some(item => item.id === 'legacy-salvage')); assert.ok(!report.top_actions.some(item => item.id === 'legacy-salvage'));
assert.ok(!report.top_actions.some(item => item.id === 'linear-roadmap-and-progress')); assert.ok(!report.top_actions.some(item => item.id === 'linear-roadmap-and-progress'));
} finally { } finally {
@@ -287,6 +313,45 @@ function runTests() {
} }
})) passed++; else failed++; })) passed++; else failed++;
if (test('preview pack and Hermes gates stay in progress until smoke gate is wired', () => {
const rootDir = createTempDir('operator-dashboard-preview-smoke-');
try {
seedRepo(rootDir, {
'package.json': JSON.stringify({
files: [
'scripts/observability-readiness.js',
'scripts/operator-readiness-dashboard.js',
'scripts/platform-audit.js'
],
scripts: {
'discussion:audit': 'node scripts/discussion-audit.js',
'observability:ready': 'node scripts/observability-readiness.js',
'operator:dashboard': 'node scripts/operator-readiness-dashboard.js',
'platform:audit': 'node scripts/platform-audit.js',
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js',
'security:advisory-sources': 'node scripts/ci/supply-chain-advisory-sources.js'
}
}, null, 2),
'scripts/preview-pack-smoke.js': null,
'docs/releases/2.0.0-rc.1/preview-pack-manifest.md': 'publication-readiness.md release-notes.md quickstart.md'
});
const report = buildSeededReport(rootDir);
const previewPack = report.requirements.find(item => item.id === 'ecc-preview-pack');
const hermes = report.requirements.find(item => item.id === 'hermes-specialized-skills');
assert.strictEqual(previewPack.status, 'in_progress');
assert.strictEqual(previewPack.gap, 'final clean-checkout release approval and publish evidence still pending');
assert.strictEqual(hermes.status, 'in_progress');
assert.strictEqual(hermes.gap, 'final preview-pack smoke and release review pending');
assert.ok(report.top_actions.some(item => item.id === 'ecc-preview-pack'));
assert.ok(report.top_actions.some(item => item.id === 'hermes-specialized-skills'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('AgentShield enterprise evidence covers export and policy promotion markers', () => { if (test('AgentShield enterprise evidence covers export and policy promotion markers', () => {
const cases = [ const cases = [
{ {

View File

@@ -0,0 +1,260 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'preview-pack-smoke.js');
const {
REQUIRED_ARTIFACTS,
REQUIRED_PUBLICATION_BLOCKERS,
REQUIRED_VERIFICATION_COMMANDS,
buildReport,
parseArgs,
renderText,
} = require(SCRIPT);
const RELEASE_DIR = 'docs/releases/2.0.0-rc.1';
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function writeFile(rootDir, relativePath, content) {
const targetPath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, content);
}
function manifestContent() {
return [
'# ECC v2.0.0-rc.1 Preview Pack Manifest',
'',
'## Pack Contents',
'',
'| Artifact | Role | Gate |',
'| --- | --- | --- |',
...REQUIRED_ARTIFACTS.map(artifact => `| \`${artifact}\` | release artifact | checked |`),
'',
'## Hermes Skill Boundary',
'',
'- no raw workspace exports;',
'',
'## Final Verification Commands',
'',
'```bash',
...REQUIRED_VERIFICATION_COMMANDS,
'```',
'',
'## Publication Blockers',
'',
...REQUIRED_PUBLICATION_BLOCKERS.map(blocker => `- ${blocker}`),
'',
'The preview pack is not public without approval-gated release, package, plugin, and announcement steps.',
].join('\n');
}
function seedRepo(rootDir, overrides = {}) {
const files = {
'package.json': JSON.stringify({
files: ['scripts/preview-pack-smoke.js'],
scripts: {
'preview-pack:smoke': 'node scripts/preview-pack-smoke.js',
},
}, null, 2),
'scripts/preview-pack-smoke.js': 'preview pack smoke script',
[`${RELEASE_DIR}/preview-pack-manifest.md`]: manifestContent(),
'docs/HERMES-SETUP.md': [
'# Hermes Setup',
'Public Release Candidate Scope',
'ECC v2.0.0-rc.1 documents the Hermes surface',
'No raw workspace export is included.',
].join('\n'),
'skills/hermes-imports/SKILL.md': [
'---',
'name: hermes-imports',
'---',
'Sanitization Checklist',
'Do not ship raw workspace exports',
'Output Contract',
].join('\n'),
};
for (const artifact of REQUIRED_ARTIFACTS) {
if (!Object.prototype.hasOwnProperty.call(files, artifact)) {
files[artifact] = `${artifact} public preview-pack content`;
}
}
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
if (content === null) {
continue;
}
writeFile(rootDir, relativePath, content);
}
}
function run(args = [], options = {}) {
return execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
}
function runProcess(args = [], options = {}) {
return spawnSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000,
});
}
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing preview-pack-smoke.js ===\n');
let passed = 0;
let failed = 0;
if (test('parseArgs accepts smoke flags and rejects invalid values', () => {
const rootDir = createTempDir('preview-pack-smoke-args-');
try {
const parsed = parseArgs([
'node',
'script',
'--format=json',
`--root=${rootDir}`,
]);
assert.strictEqual(parsed.format, 'json');
assert.strictEqual(parsed.root, path.resolve(rootDir));
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
assert.throws(() => parseArgs(['node', 'script', '--root']), /--root requires a value/);
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('seeded release pack passes every smoke check', () => {
const rootDir = createTempDir('preview-pack-smoke-pass-');
try {
seedRepo(rootDir);
const report = buildReport({ root: rootDir });
assert.strictEqual(report.schema_version, 'ecc.preview-pack-smoke.v1');
assert.strictEqual(report.ready, true);
assert.strictEqual(report.summary.failed, 0);
assert.ok(report.checks.every(check => check.status === 'pass'));
const text = renderText(report);
assert.ok(text.includes('Ready: yes'));
assert.ok(text.includes('Passed: 5'));
assert.ok(text.includes('Failed: 0'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('script registration fails closed without package wiring', () => {
const rootDir = createTempDir('preview-pack-smoke-package-');
try {
seedRepo(rootDir, {
'package.json': JSON.stringify({ files: [], scripts: {} }, null, 2),
});
const report = buildReport({ root: rootDir });
const registration = report.checks.find(check => check.id === 'preview-pack-script-registered');
assert.strictEqual(report.ready, false);
assert.strictEqual(registration.status, 'fail');
assert.ok(registration.fix.includes('preview-pack:smoke'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('Hermes boundary fails closed on private local paths', () => {
const rootDir = createTempDir('preview-pack-smoke-private-path-');
try {
seedRepo(rootDir, {
[`${RELEASE_DIR}/quickstart.md`]: 'Do not ship /Users/affoon/private-state in public docs.',
});
const report = buildReport({ root: rootDir });
const boundary = report.checks.find(check => check.id === 'hermes-boundary-sanitized');
assert.strictEqual(report.ready, false);
assert.strictEqual(boundary.status, 'fail');
assert.ok(boundary.evidence.includes(`${RELEASE_DIR}/quickstart.md:1`));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('CLI emits json and uses status 2 for failed smoke reports', () => {
const rootDir = createTempDir('preview-pack-smoke-cli-');
try {
seedRepo(rootDir);
const stdout = run(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
const parsed = JSON.parse(stdout);
assert.strictEqual(parsed.ready, true);
writeFile(rootDir, 'package.json', JSON.stringify({ files: [], scripts: {} }, null, 2));
const failedRun = runProcess(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
assert.strictEqual(failedRun.status, 2);
assert.strictEqual(failedRun.stderr, '');
assert.ok(failedRun.stdout.includes('"ready": false'));
} finally {
cleanup(rootDir);
}
})) passed++; else failed++;
if (test('CLI help exits successfully and invalid flags fail before reporting', () => {
const help = runProcess(['--help']);
assert.strictEqual(help.status, 0);
assert.strictEqual(help.stderr, '');
assert.ok(help.stdout.includes('Usage: node scripts/preview-pack-smoke.js'));
const invalid = runProcess(['--format=xml']);
assert.strictEqual(invalid.status, 1);
assert.strictEqual(invalid.stdout, '');
assert.match(invalid.stderr, /Error: Invalid format/);
})) passed++; else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
if (failed > 0) {
process.exit(1);
}
}
if (require.main === module) {
runTests();
}