#!/usr/bin/env node 'use strict'; const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); const { buildReport: buildPlatformReport } = require('./platform-audit'); const SCHEMA_VERSION = 'ecc.operator-readiness-dashboard.v1'; const DEFAULT_THRESHOLDS = Object.freeze({ maxOpenPrs: 20, maxOpenIssues: 20, maxDirtyFiles: 0, }); function usage() { console.log([ 'Usage: node scripts/operator-readiness-dashboard.js [options]', '', 'Generate the ECC operator readiness dashboard and prompt-to-artifact audit.', '', 'Options:', ' --format ', ' Output format (default: markdown)', ' --json Alias for --format json', ' --markdown Alias for --format markdown', ' --write Write json or markdown output to a file', ' --root Repository root to inspect (default: cwd)', ' --repo GitHub repo to inspect; repeatable', ' --skip-github Skip live GitHub queue/discussion checks', ' --max-open-prs PR budget passed through to platform:audit', ' --max-open-issues Issue budget passed through to platform:audit', ' --max-dirty-files Dirty-file budget passed through to platform:audit', ' --allow-untracked Ignore untracked files under path; repeatable', ' --use-env-github-token Keep GITHUB_TOKEN when invoking gh', ' --generated-at Override generatedAt for deterministic tests', ' --exit-code Return 2 when the objective is not ready', ' --help, -h Show this help', ].join('\n')); } function readValue(args, index, flagName) { const value = args[index + 1]; if (!value || value.startsWith('--')) { throw new Error(`${flagName} requires a value`); } return value; } function parseIntegerFlag(value, flagName) { const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed < 0) { throw new Error(`Invalid ${flagName}: ${value}`); } return parsed; } function normalizeRelativePrefix(value) { const normalized = String(value || '') .replace(/\\/g, '/') .replace(/^\.\/+/, '') .replace(/\/+$/, ''); return normalized ? `${normalized}/` : ''; } function parseArgs(argv) { const args = argv.slice(2); const parsed = { allowUntracked: [], exitCode: false, format: 'markdown', generatedAt: null, help: false, repos: [], root: path.resolve(process.cwd()), skipGithub: false, thresholds: { ...DEFAULT_THRESHOLDS }, useEnvGithubToken: false, writePath: null, }; 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 = readValue(args, index, arg).toLowerCase(); index += 1; continue; } if (arg.startsWith('--format=')) { parsed.format = arg.slice('--format='.length).toLowerCase(); continue; } if (arg === '--json') { parsed.format = 'json'; continue; } if (arg === '--markdown') { parsed.format = 'markdown'; continue; } if (arg === '--write') { parsed.writePath = path.resolve(readValue(args, index, arg)); index += 1; continue; } if (arg.startsWith('--write=')) { parsed.writePath = path.resolve(arg.slice('--write='.length)); continue; } if (arg === '--root') { parsed.root = path.resolve(readValue(args, index, arg)); index += 1; continue; } if (arg.startsWith('--root=')) { parsed.root = path.resolve(arg.slice('--root='.length)); continue; } if (arg === '--repo') { parsed.repos.push(readValue(args, index, arg)); index += 1; continue; } if (arg.startsWith('--repo=')) { parsed.repos.push(arg.slice('--repo='.length)); continue; } if (arg === '--skip-github') { parsed.skipGithub = true; continue; } if (arg === '--allow-untracked') { parsed.allowUntracked.push(readValue(args, index, arg)); index += 1; continue; } if (arg.startsWith('--allow-untracked=')) { parsed.allowUntracked.push(arg.slice('--allow-untracked='.length)); continue; } if (arg === '--max-open-prs') { parsed.thresholds.maxOpenPrs = parseIntegerFlag(readValue(args, index, arg), arg); index += 1; continue; } if (arg.startsWith('--max-open-prs=')) { parsed.thresholds.maxOpenPrs = parseIntegerFlag(arg.slice('--max-open-prs='.length), '--max-open-prs'); continue; } if (arg === '--max-open-issues') { parsed.thresholds.maxOpenIssues = parseIntegerFlag(readValue(args, index, arg), arg); index += 1; continue; } if (arg.startsWith('--max-open-issues=')) { parsed.thresholds.maxOpenIssues = parseIntegerFlag(arg.slice('--max-open-issues='.length), '--max-open-issues'); continue; } if (arg === '--max-dirty-files') { parsed.thresholds.maxDirtyFiles = parseIntegerFlag(readValue(args, index, arg), arg); index += 1; continue; } if (arg.startsWith('--max-dirty-files=')) { parsed.thresholds.maxDirtyFiles = parseIntegerFlag(arg.slice('--max-dirty-files='.length), '--max-dirty-files'); continue; } if (arg === '--use-env-github-token') { parsed.useEnvGithubToken = true; continue; } if (arg === '--generated-at') { parsed.generatedAt = readValue(args, index, arg); index += 1; continue; } if (arg.startsWith('--generated-at=')) { parsed.generatedAt = arg.slice('--generated-at='.length); continue; } if (arg === '--exit-code') { parsed.exitCode = true; continue; } throw new Error(`Unknown argument: ${arg}`); } if (!['text', 'json', 'markdown'].includes(parsed.format)) { throw new Error(`Invalid format: ${parsed.format}. Use text, json, or markdown.`); } if (parsed.writePath && parsed.format === 'text') { throw new Error('--write requires --json, --markdown, or --format json|markdown'); } parsed.allowUntracked = parsed.allowUntracked.map(normalizeRelativePrefix).filter(Boolean); 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 includesAll(text, needles) { return needles.every(needle => text.includes(needle)); } const LOCALIZATION_MANUAL_REVIEW_TAIL = [ '#1687 zh-CN localization tail', '#1609 Persian README translation', '#1563 zh-TW README sync', '#1564 Turkish README sync', '#1565 pt-BR README sync', ]; function hasLegacySalvageTracking({ stalePrSalvage, legacyInventory, roadmap }) { return stalePrSalvage.includes('Manual review tail') || stalePrSalvage.includes('Remaining Manual-Review Backlog') || stalePrSalvage.includes('Translator/manual review') || legacyInventory.includes('Translator/manual review') || roadmap.includes('ITO-55'); } function hasAttachedLegacyManualReviewTail({ stalePrSalvage, legacyInventory, roadmap }) { return stalePrSalvage.includes('Linear ITO-55') && legacyInventory.includes('ITO-55') && roadmap.includes('ITO-55') && LOCALIZATION_MANUAL_REVIEW_TAIL.every(item => ( stalePrSalvage.includes(item) && legacyInventory.includes(item) )); } function legacySalvageStatus(context) { if (hasAttachedLegacyManualReviewTail(context)) { return 'current'; } return hasLegacySalvageTracking(context) ? 'in_progress' : 'not_complete'; } function legacySalvageEvidence(context) { if (hasAttachedLegacyManualReviewTail(context)) { return 'legacy salvage ledger and inventory are current; all localization tails are attached to Linear ITO-55 for manual language-owner review'; } return 'legacy salvage ledger and ITO-55 tracking are present'; } function legacySalvageGap(context) { if (hasAttachedLegacyManualReviewTail(context)) { return 'repeat legacy scan before release'; } return 'final translation/manual-review tail remains'; } function hasAgentShieldEnterpriseTracking(roadmap) { return roadmap.includes('AgentShield Enterprise Iteration') && ( roadmap.includes('#78-#92') || roadmap.includes('AgentShield PR #92') || roadmap.includes('AgentShield #92') || roadmap.includes('policy promote') || roadmap.includes('checksum-verified policy promotion') || roadmap.includes('#78-#91') || roadmap.includes('AgentShield PR #91') || roadmap.includes('AgentShield #91') || roadmap.includes('checksum-backed policy export') || roadmap.includes('#78-#90') || roadmap.includes('hosted promotion judge audit traces') || roadmap.includes('operator-visible promotion output values') ); } function agentShieldEnterpriseGap(roadmap) { if (roadmap.includes('hosted promotion judge audit traces') || roadmap.includes('operator-visible promotion output values')) { return 'deepen live operator approval/readback after Marketplace/payment gates'; } if (roadmap.includes('#78-#92') || roadmap.includes('AgentShield PR #92') || roadmap.includes('AgentShield #92') || roadmap.includes('policy promote') || roadmap.includes('checksum-verified policy promotion')) { return 'workflow automation around protected rollout and richer runtime review UX pending after policy promotion shipped'; } return roadmap.includes('#78-#91') || roadmap.includes('AgentShield PR #91') || roadmap.includes('AgentShield #91') || roadmap.includes('checksum-backed policy export') ? 'workflow automation plus policy promotion/review UX pending after policy export shipped' : 'durable policy export and fleet-review workflow automation remain pending after reviewItems shipped'; } function agentShieldEnterpriseEvidence(roadmap) { if (roadmap.includes('hosted promotion judge audit traces') || roadmap.includes('operator-visible promotion output values')) { return 'AgentShield policy promotion `reviewItems` landed in `87aec47`; package-manager hardening drift detection landed in `28d08c7`; workflow action runtime pins were refreshed in `659f569`; npm age-gate guidance was corrected in `ee585cd`; package-manager hardening Action outputs landed in `1124535`; policy-promotion Action outputs and runtime-smoke job-summary evidence landed in `1593925`; ECC-Tools consumes those outputs in `8658951`, surfaces operator-readable status/pack/count/digest telemetry in `16c537f`, and renders hosted promotion judge audit traces in `05d4e82`; all are mirrored in the GA roadmap'; } return 'AgentShield enterprise PR evidence is mirrored in the GA roadmap'; } function eccToolsNextLevelEvidence(roadmap) { if (roadmap.includes('target account billing readback') || roadmap.includes('632e059')) { return 'billing announcement gate, hosted analysis lanes, AgentShield fleet-summary consumption, hosted finding evidence paths, harness-route policy linking, policy-promotion Action-output telemetry, operator-visible promotion output details, hosted promotion judge audit traces, billing announcement preflight, aggregate production billing KV readback, Wrangler OAuth readback, target-account billing readback, and provenance-aware Marketplace billing-state gates are mirrored in the GA roadmap'; } if (roadmap.includes('Wrangler OAuth readback') || roadmap.includes('42653f9')) { return 'billing announcement gate, hosted analysis lanes, AgentShield fleet-summary consumption, hosted finding evidence paths, harness-route policy linking, policy-promotion Action-output telemetry, operator-visible promotion output details, hosted promotion judge audit traces, billing announcement preflight, aggregate production billing KV readback, Wrangler OAuth readback, and provenance-aware Marketplace billing-state gates are mirrored in the GA roadmap'; } if (roadmap.includes('Marketplace webhook provenance') || roadmap.includes('2859678')) { return 'billing announcement gate, hosted analysis lanes, AgentShield fleet-summary consumption, hosted finding evidence paths, harness-route policy linking, policy-promotion Action-output telemetry, operator-visible promotion output details, hosted promotion judge audit traces, billing announcement preflight, aggregate production billing KV readback, and provenance-aware Marketplace billing-state gates are mirrored in the GA roadmap'; } if (roadmap.includes('billing:kv-readback') || roadmap.includes('95d0bec')) { return 'billing announcement gate, hosted analysis lanes, AgentShield fleet-summary consumption, hosted finding evidence paths, harness-route policy linking, policy-promotion Action-output telemetry, operator-visible promotion output details, hosted promotion judge audit traces, billing announcement preflight, and aggregate production billing KV readback are mirrored in the GA roadmap'; } if (roadmap.includes('production Marketplace readback state') || roadmap.includes('eb69412')) { return 'billing announcement gate, hosted analysis lanes, AgentShield fleet-summary consumption, hosted finding evidence paths, harness-route policy linking, policy-promotion Action-output telemetry, operator-visible promotion output details, hosted promotion judge audit traces, billing announcement preflight, and production KV readback state are mirrored in the GA roadmap'; } if (roadmap.includes('hosted promotion judge audit traces') || roadmap.includes('operator-visible promotion output values')) { return 'billing announcement gate, hosted analysis lanes, AgentShield fleet-summary consumption, hosted finding evidence paths, harness-route policy linking, policy-promotion Action-output telemetry, operator-visible promotion output details, and hosted promotion judge audit traces are mirrored in the GA roadmap'; } return 'billing announcement gate, hosted analysis lanes, AgentShield fleet-summary consumption, hosted finding evidence paths, and harness-route policy linking are mirrored in the GA roadmap'; } function eccToolsNextLevelGap(roadmap) { if (roadmap.includes('target account billing readback') || roadmap.includes('632e059')) { return 'create or verify Marketplace-managed Pro target billing-state with webhook provenance, then run `billing:kv-readback -- --account --require-ready` with working Cloudflare API auth or repaired Wrangler OAuth, followed by the live announcement gate'; } if (roadmap.includes('Wrangler OAuth readback') || roadmap.includes('42653f9')) { return 'create or verify Marketplace-managed Pro billing-state with webhook provenance, then run `billing:kv-readback -- --require-ready` with working Cloudflare API auth or repaired Wrangler OAuth, followed by the live announcement gate'; } if (roadmap.includes('Marketplace webhook provenance') || roadmap.includes('2859678')) { return 'replace the invalid Cloudflare credential, create or verify Marketplace-managed Pro billing-state with webhook provenance, then run `billing:kv-readback -- --require-ready` and the live announcement gate'; } if (roadmap.includes('billing:kv-readback') || roadmap.includes('95d0bec')) { return 'create or verify a Marketplace-managed Pro billing-state, then run the official live announcement gate'; } if (roadmap.includes('production Marketplace readback state') || roadmap.includes('eb69412')) { return 'complete Marketplace purchase/webhook readback, then run the live announcement gate'; } if (roadmap.includes('hosted promotion judge audit traces') || roadmap.includes('operator-visible promotion output values')) { return 'live Marketplace test-account readback pending'; } return 'live Marketplace test-account readback, hosted promotion telemetry, and richer operator review UX pending'; } function supplyChainLocalProtectionEvidence({ roadmap, scripts }) { if (scripts['security:advisory-sources'] === 'node scripts/ci/supply-chain-advisory-sources.js' && roadmap.includes('package-manager hardening Action outputs')) { return 'scheduled supply-chain watch emits IOC/advisory-source refresh artifacts; ECC scanner covers gh-token-monitor token-store persistence; AgentShield now detects known AI-tool persistence IOCs, npm lifecycle/token drift, unsupported npm age-key drift, and pnpm/Yarn cooldown drift; current-head watch evidence and ITO-57 May 18 Linear evidence updates are current'; } return scripts['security:advisory-sources'] === 'node scripts/ci/supply-chain-advisory-sources.js' ? 'scheduled supply-chain watch now emits IOC and advisory-source refresh artifacts' : 'scheduled supply-chain watch or advisory-source command is missing'; } function supplyChainLocalProtectionGap({ roadmap, scripts }) { if (scripts['security:advisory-sources'] === 'node scripts/ci/supply-chain-advisory-sources.js' && roadmap.includes('package-manager hardening Action outputs')) { return 'repeat advisory/source refresh and Linear sync after each significant supply-chain batch'; } return 'Linear status synchronization remains ITO-57 follow-up after each significant merge batch'; } function hasCurrentLinearProgressSync({ roadmap, progressSync }) { const hasOperatorProgressSurface = roadmap.includes('operator progress snapshot') || roadmap.includes('operator progress comment'); return roadmap.includes('Linear live sync is current') && hasOperatorProgressSurface && includesAll(progressSync, [ 'node scripts/work-items.js sync-github --repo ', 'node scripts/status.js --json', 'Linear remains the external status surface', ]); } function hasLinearProgressContract({ roadmap, progressSync }) { return includesAll(roadmap, ['ITO-44', 'ITO-59', 'Linear']) && includesAll(progressSync, ['GitHub', 'Linear', 'handoff', 'repo roadmap']); } function linearProgressStatus(context) { if (hasCurrentLinearProgressSync(context)) { return 'current'; } return hasLinearProgressContract(context) ? 'in_progress' : 'not_complete'; } function linearProgressEvidence(context) { if (hasCurrentLinearProgressSync(context)) { return 'Linear live sync and project progress surface are current; progress-sync contract defines the file-backed work-items/status path'; } return 'repo mirror and progress-sync contract are present'; } function linearProgressGap(context) { if (hasCurrentLinearProgressSync(context)) { return 'repeat Linear/project status update and local work-items sync after each significant merge batch'; } return 'recurring Linear status sync and productized realtime sync remain pending'; } function runCommand(command, args, options = {}) { const result = spawnSync(command, args, { cwd: options.cwd, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, }); if (result.error || result.status !== 0) { return null; } return (result.stdout || '').trim(); } function readPackage(rootDir) { const text = readText(rootDir, 'package.json'); if (!text.trim()) { return {}; } try { return JSON.parse(text); } catch (_error) { return {}; } } function buildRequirement(id, requirement, artifact, status, evidence, gap) { return { id, requirement, artifact, status, evidence, gap }; } function isCurrentOrComplete(status) { return status === 'current' || status === 'complete'; } function buildRequirements(rootDir, platformReport) { const roadmap = readText(rootDir, 'docs/ECC-2.0-GA-ROADMAP.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 releaseUrlLedger = readText(rootDir, 'docs/releases/2.0.0-rc.1/release-url-ledger-2026-05-18.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 observabilityReadiness = readText(rootDir, 'docs/architecture/observability-readiness.md'); const stalePrSalvage = readText(rootDir, 'docs/stale-pr-salvage-ledger.md'); const legacyInventory = readText(rootDir, 'docs/legacy-artifact-inventory.md'); const supplyChainRunbook = readText(rootDir, 'docs/security/supply-chain-incident-response.md'); const supplyChainWorkflow = readText(rootDir, '.github/workflows/supply-chain-watch.yml'); const packageJson = readPackage(rootDir); const scripts = packageJson.scripts || {}; 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 queuesCurrent = githubLive && platformReport.github.totals.openPrs <= platformReport.thresholds.maxOpenPrs && platformReport.github.totals.openIssues <= platformReport.thresholds.maxOpenIssues; const discussionsCurrent = githubLive && platformReport.github.totals.discussionsNeedingMaintainerTouch === 0 && platformReport.github.totals.discussionsMissingAcceptedAnswer === 0; return [ buildRequirement( 'public-pr-budget', 'Keep public PRs below 20', 'scripts/platform-audit.js live GitHub sweep', queuesCurrent ? 'current' : 'in_progress', githubLive ? `${platformReport.github.totals.openPrs} open PRs across ${platformReport.github.repos.length} tracked repos` : 'live GitHub queue readback was skipped or failed', queuesCurrent ? 'repeat before release' : 'run live platform:audit and drain PR queue' ), buildRequirement( 'public-issue-budget', 'Keep public issues below 20', 'scripts/platform-audit.js live GitHub sweep', queuesCurrent ? 'current' : 'in_progress', githubLive ? `${platformReport.github.totals.openIssues} open issues across ${platformReport.github.repos.length} tracked repos` : 'live GitHub queue readback was skipped or failed', queuesCurrent ? 'repeat before release' : 'run live platform:audit and drain issue queue' ), buildRequirement( 'repository-discussions', 'Respond and manage repository discussions', 'scripts/platform-audit.js discussion summary', discussionsCurrent ? 'current' : 'in_progress', githubLive ? `${platformReport.github.totals.discussionsNeedingMaintainerTouch} need maintainer touch; ${platformReport.github.totals.discussionsMissingAcceptedAnswer} answerable discussions missing accepted answer` : 'live discussion readback was skipped or failed', discussionsCurrent ? 'repeat before release' : 'respond, answer, or route remaining discussions' ), buildRequirement( 'completion-dashboard', 'Build ITO-44 completion dashboard into a repeatable command', 'npm run operator:dashboard', scripts['operator:dashboard'] === 'node scripts/operator-readiness-dashboard.js' && fileExists(rootDir, 'scripts/operator-readiness-dashboard.js') ? 'complete' : 'in_progress', scripts['operator:dashboard'] === 'node scripts/operator-readiness-dashboard.js' ? 'operator:dashboard package script exists' : 'operator:dashboard package script missing', 'keep generated dashboard attached to publication evidence' ), buildRequirement( 'ecc-preview-pack', 'ECC 2.0 preview pack ready', 'docs/releases/2.0.0-rc.1/preview-pack-manifest.md', previewPackManifestReady && previewPackSmokeReady ? 'current' : previewPackManifestReady ? 'in_progress' : 'not_complete', previewPackManifestReady && previewPackSmokeReady ? 'preview pack manifest and deterministic smoke gate are in-tree' : previewPackManifestReady ? 'preview pack manifest is in-tree' : 'preview pack manifest is incomplete', previewPackManifestReady && previewPackSmokeReady ? 'repeat clean-checkout preview-pack smoke before publication' : 'final clean-checkout release approval and publish evidence still pending' ), buildRequirement( 'hermes-specialized-skills', 'Include Hermes specialized skills safely', 'docs/HERMES-SETUP.md and skills/hermes-imports/SKILL.md', hermesArtifactsReady && previewPackSmokeReady ? 'current' : hermesArtifactsReady ? 'in_progress' : 'not_complete', hermesArtifactsReady && previewPackSmokeReady ? 'Hermes setup/import artifacts are covered by preview-pack smoke' : hermesArtifactsReady ? 'Hermes setup and import skill are present' : 'Hermes setup/import artifacts missing', hermesArtifactsReady && previewPackSmokeReady ? 'repeat preview-pack smoke before release review' : 'final preview-pack smoke and release review pending' ), buildRequirement( 'naming-and-plugin-publication', 'Prepare name-change, Claude plugin, and Codex plugin paths', 'naming-and-publication-matrix plus publication-readiness', includesAll(namingMatrix, ['Claude plugin', 'Codex plugin', 'npm package', 'Publication Paths']) && includesAll(publicationReadiness, ['Claude plugin', 'Codex plugin']) ? 'in_progress' : 'not_complete', 'naming matrix and plugin readiness gates exist', 'real tag/push, marketplace submission, and final channel choice remain approval-gated' ), buildRequirement( 'release-notes-and-notifications', 'Prepare release notes, articles, tweets, and push notifications', 'docs/releases/2.0.0-rc.1 social and release-copy files', fileExists(rootDir, 'docs/releases/2.0.0-rc.1/release-notes.md') && fileExists(rootDir, 'docs/releases/2.0.0-rc.1/x-thread.md') && fileExists(rootDir, 'docs/releases/2.0.0-rc.1/linkedin-post.md') ? 'in_progress' : 'not_complete', includesAll(releaseUrlLedger, ['Live Now', 'Approval-Gated URLs', 'Codex marketplace CLI docs']) ? 'release notes, X thread, LinkedIn draft, and URL ledger are present' : 'release notes, X thread, and LinkedIn draft are present', includesAll(releaseUrlLedger, ['Live Now', 'Approval-Gated URLs', 'Codex marketplace CLI docs']) ? 'final live release/npm/plugin/billing URLs and publish approval still pending' : 'URL-backed refresh and publish approval still pending' ), buildRequirement( 'agentshield-enterprise-iteration', 'Advance AgentShield enterprise iteration', 'AgentShield PR evidence plus enterprise roadmap', hasAgentShieldEnterpriseTracking(roadmap) ? 'in_progress' : 'not_complete', agentShieldEnterpriseEvidence(roadmap), agentShieldEnterpriseGap(roadmap) ), buildRequirement( 'ecc-tools-next-level', 'Advance ECC Tools native payments and AI-native harness-agnostic app', 'ECC Tools PR evidence, billing gate, hosted analysis lanes', includesAll(roadmap, ['ECC-Tools PR #78', 'hosted promotion', 'announcementGate']) ? 'in_progress' : 'not_complete', eccToolsNextLevelEvidence(roadmap), eccToolsNextLevelGap(roadmap) ), buildRequirement( 'legacy-salvage', 'Audit, prune, or attach legacy work', 'docs/stale-pr-salvage-ledger.md and legacy inventory', legacySalvageStatus(legacyContext), legacySalvageEvidence(legacyContext), legacySalvageGap(legacyContext) ), buildRequirement( 'linear-roadmap-and-progress', 'Keep Linear roadmap detailed and progress tracking synchronized', 'Linear project mirror plus progress-sync contract', linearProgressStatus({ roadmap, progressSync }), linearProgressEvidence({ roadmap, progressSync }), linearProgressGap({ roadmap, progressSync }) ), buildRequirement( 'observability-for-self-use', 'Provide ECC 2.0 observability for self-use', 'observability readiness gate', scripts['observability:ready'] === 'node scripts/observability-readiness.js' && includesAll(observabilityReadiness, ['observability-readiness.js']) ? 'complete' : 'in_progress', scripts['observability:ready'] === 'node scripts/observability-readiness.js' ? 'observability:ready command and readiness doc exist' : 'observability readiness command missing', 'runtime/dashboard implementation can continue after release gates' ), buildRequirement( 'supply-chain-local-protection', 'Keep Mini Shai-Hulud/TanStack protection loop current', 'supply-chain watch plus runbook plus AgentShield package-manager hardening', includesAll(supplyChainRunbook, ['TanStack', 'Mini Shai-Hulud', 'scan-supply-chain-iocs.js', 'supply-chain-advisory-sources.js']) && includesAll(supplyChainWorkflow, ['supply-chain-advisory-sources.js', 'supply-chain-advisory-sources.json']) && scripts['security:advisory-sources'] === 'node scripts/ci/supply-chain-advisory-sources.js' && fileExists(rootDir, '.github/workflows/supply-chain-watch.yml') ? 'current' : 'in_progress', supplyChainLocalProtectionEvidence({ roadmap, scripts }), supplyChainLocalProtectionGap({ roadmap, scripts }) ), ]; } function buildReport(options) { const rootDir = path.resolve(options.root); const generatedAt = options.generatedAt || new Date().toISOString(); const platformReport = buildPlatformReport({ allowUntracked: options.allowUntracked, exitCode: false, format: 'json', help: false, repos: options.repos, root: rootDir, skipGithub: options.skipGithub, thresholds: options.thresholds, useEnvGithubToken: options.useEnvGithubToken, writePath: null, }); const requirements = buildRequirements(rootDir, platformReport); const incompleteRequirements = requirements.filter(item => !isCurrentOrComplete(item.status)); const topActions = incompleteRequirements.map(item => ({ id: item.id, summary: item.requirement, fix: item.gap, })); const head = runCommand('git', ['rev-parse', 'HEAD'], { cwd: rootDir }); return { schema_version: SCHEMA_VERSION, generatedAt, root: rootDir, head, ready: incompleteRequirements.length === 0, dashboardReady: platformReport.ready, publicationReady: false, platform: { ready: platformReport.ready, branch: platformReport.git.branch, blockingDirtyCount: platformReport.git.blockingDirtyCount, ignoredDirtyCount: platformReport.git.ignoredDirty.length, openPrs: platformReport.github.totals.openPrs, openIssues: platformReport.github.totals.openIssues, discussionsNeedingMaintainerTouch: platformReport.github.totals.discussionsNeedingMaintainerTouch, discussionsMissingAcceptedAnswer: platformReport.github.totals.discussionsMissingAcceptedAnswer, githubErrors: platformReport.github.totals.errors, githubSkipped: platformReport.github.skipped, }, requirements, top_actions: topActions, next_work_order: [ 'Regenerate this dashboard from the final release commit before publication evidence is recorded.', 'Repeat ITO-57 Linear/project status sync after the next significant merge batch or advisory-source refresh.', 'Create or verify Marketplace-managed Pro target billing-state with webhook provenance, then run `billing:kv-readback -- --account --require-ready` with working Cloudflare API auth or repaired Wrangler OAuth, followed by the live announcement gate before publishing native-payments copy.', 'Resume ITO-45, ITO-46, and ITO-56 only after the generated dashboard and final release gates are refreshed.', ], }; } function markdownEscape(value) { return String(value === undefined || value === null ? '' : value) .replace(/\|/g, '\\|') .replace(/\r?\n/g, '
'); } function renderText(report) { const lines = [ `ECC Operator Readiness Dashboard: ${report.ready ? 'objective ready' : 'work remaining'}`, `Generated: ${report.generatedAt}`, `Commit: ${report.head || 'unknown'}`, `Dashboard ready: ${report.dashboardReady}`, `Publication ready: ${report.publicationReady}`, '', 'Platform:', ` PRs: ${report.platform.openPrs}`, ` Issues: ${report.platform.openIssues}`, ` Discussions needing touch: ${report.platform.discussionsNeedingMaintainerTouch}`, ` Missing accepted answers: ${report.platform.discussionsMissingAcceptedAnswer}`, ` Blocking dirty files: ${report.platform.blockingDirtyCount}`, '', 'Requirements:', ]; for (const item of report.requirements) { lines.push(` ${item.status.toUpperCase()} ${item.id}: ${item.requirement}`); } lines.push('', 'Top actions:'); if (report.top_actions.length === 0) { lines.push(' none'); } else { for (const action of report.top_actions) { lines.push(` - ${action.id}: ${action.fix}`); } } return `${lines.join('\n')}\n`; } function renderMarkdown(report) { const lines = [ '# ECC Operator Readiness Dashboard', '', 'This dashboard is generated by `npm run operator:dashboard`. It is an operator snapshot, not release approval.', '', `Generated: ${report.generatedAt}`, `Commit: ${report.head || 'unknown'}`, `Status: ${report.ready ? 'objective ready' : 'work remaining'}`, '', '## Current Status', '', '| Area | Status | Evidence |', '| --- | --- | --- |', `| PR queue | ${report.platform.openPrs < 20 && !report.platform.githubSkipped ? 'Current' : 'Needs work'} | ${report.platform.openPrs} open PRs across tracked repos |`, `| Issue queue | ${report.platform.openIssues < 20 && !report.platform.githubSkipped ? 'Current' : 'Needs work'} | ${report.platform.openIssues} open issues across tracked repos |`, `| Discussions | ${report.platform.discussionsNeedingMaintainerTouch === 0 && report.platform.discussionsMissingAcceptedAnswer === 0 && !report.platform.githubSkipped ? 'Current' : 'Needs work'} | ${report.platform.discussionsNeedingMaintainerTouch} need maintainer touch; ${report.platform.discussionsMissingAcceptedAnswer} missing accepted answer |`, `| Local worktree | ${report.platform.blockingDirtyCount === 0 ? 'Current' : 'Needs work'} | ${report.platform.blockingDirtyCount} blocking dirty files; ${report.platform.ignoredDirtyCount} ignored dirty entries |`, `| Dashboard generation | ${report.dashboardReady ? 'Current' : 'Needs work'} | platform audit ready: ${report.platform.ready}; GitHub skipped: ${report.platform.githubSkipped} |`, `| Publication | ${report.publicationReady ? 'Ready' : 'Not complete'} | release, npm, plugin, billing, and announcement gates are tracked below |`, '', '## Prompt-To-Artifact Checklist', '', '| Objective requirement | Artifact or gate | Status | Evidence | Gap |', '| --- | --- | --- | --- | --- |', ]; for (const item of report.requirements) { lines.push(`| ${markdownEscape(item.requirement)} | ${markdownEscape(item.artifact)} | ${markdownEscape(item.status)} | ${markdownEscape(item.evidence)} | ${markdownEscape(item.gap)} |`); } lines.push('', '## Top Actions', ''); if (report.top_actions.length === 0) { lines.push('- none'); } else { for (const action of report.top_actions) { lines.push(`- \`${markdownEscape(action.id)}\`: ${markdownEscape(action.fix)}`); } } lines.push('', '## Next Work Order', ''); report.next_work_order.forEach((item, index) => { lines.push(`${index + 1}. ${item}`); }); return `${lines.join('\n')}\n`; } function renderReport(report, format) { if (format === 'json') { return `${JSON.stringify(report, null, 2)}\n`; } if (format === 'text') { return renderText(report); } return renderMarkdown(report); } function writeOutput(writePath, output) { fs.mkdirSync(path.dirname(writePath), { recursive: true }); fs.writeFileSync(writePath, output, 'utf8'); } function main() { let options; try { options = parseArgs(process.argv); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); } if (options.help) { usage(); return; } const report = buildReport(options); const output = renderReport(report, options.format); if (options.writePath) { writeOutput(options.writePath, output); } process.stdout.write(output); if (options.exitCode && !report.ready) { process.exit(2); } } if (require.main === module) { main(); } module.exports = { buildReport, parseArgs, renderMarkdown, renderReport, renderText, };