diff --git a/README.md b/README.md index 0deac4f1..7644ed38 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ This repo is the raw code only. The guides explain everything. - **Media and launch tooling** — `manim-video`, `remotion-video-creation`, and upgraded social publishing surfaces make technical explainers and launch content part of the same system. - **Framework and product surface growth** — `nestjs-patterns`, richer Codex/OpenCode install surfaces, and expanded cross-harness packaging keep the repo usable beyond Claude Code alone. - **ECC 2.0 alpha is in-tree** — the Rust control-plane prototype in `ecc2/` now builds locally and exposes `dashboard`, `start`, `sessions`, `status`, `stop`, `resume`, and `daemon` commands. It is usable as an alpha, not yet a general release. +- **Operator status snapshots** — `ecc status --markdown --write status.md` turns the local state store into a portable handoff covering active sessions, skill-run health, install health, and pending governance events. - **Ecosystem hardening** — AgentShield, ECC Tools cost controls, billing portal work, and website refreshes continue to ship around the core plugin instead of drifting into separate silos. ### v1.9.0 — Selective Install & Language Expansion (Mar 2026) diff --git a/scripts/ecc.js b/scripts/ecc.js index 18a8240d..f3473295 100755 --- a/scripts/ecc.js +++ b/scripts/ecc.js @@ -108,6 +108,7 @@ Examples: ecc repair --dry-run ecc auto-update --dry-run ecc status --json + ecc status --markdown --write status.md ecc sessions ecc sessions session-active --json ecc session-inspect claude:latest diff --git a/scripts/status.js b/scripts/status.js index 13d86ef3..ff23338c 100644 --- a/scripts/status.js +++ b/scripts/status.js @@ -1,12 +1,14 @@ #!/usr/bin/env node 'use strict'; +const fs = require('fs'); const os = require('os'); +const path = require('path'); const { createStateStore } = require('./lib/state-store'); function showHelp(exitCode = 0) { console.log(` -Usage: node scripts/status.js [--db ] [--json] [--limit ] +Usage: node scripts/status.js [--db ] [--json|--markdown] [--write ] [--limit ] Query the ECC SQLite state store for active sessions, recent skill runs, install health, and pending governance events. @@ -19,6 +21,8 @@ function parseArgs(argv) { const parsed = { dbPath: null, json: false, + markdown: false, + writePath: null, help: false, limit: 5, }; @@ -31,6 +35,11 @@ function parseArgs(argv) { index += 1; } else if (arg === '--json') { parsed.json = true; + } else if (arg === '--markdown') { + parsed.markdown = true; + } else if (arg === '--write') { + parsed.writePath = args[index + 1] || null; + index += 1; } else if (arg === '--limit') { parsed.limit = args[index + 1] || null; index += 1; @@ -41,6 +50,22 @@ function parseArgs(argv) { } } + if (parsed.json && parsed.markdown) { + throw new Error('Choose only one output format: --json or --markdown'); + } + + if (args.includes('--db') && !parsed.dbPath) { + throw new Error('Missing value for --db'); + } + + if (args.includes('--write') && !parsed.writePath) { + throw new Error('Missing value for --write'); + } + + if (args.includes('--limit') && !parsed.limit) { + throw new Error('Missing value for --limit'); + } + return parsed; } @@ -129,6 +154,108 @@ function printHuman(payload) { printGovernance(payload.governance); } +function formatPercent(value) { + return value === null ? 'n/a' : `${value}%`; +} + +function formatCode(value) { + return `\`${String(value || '').replace(/`/g, '\\`')}\``; +} + +function renderMarkdown(payload) { + const lines = [ + '# ECC Status', + '', + `Generated: ${payload.generatedAt}`, + `Database: ${formatCode(payload.dbPath)}`, + '', + '## Active Sessions', + '', + `Active sessions: ${payload.activeSessions.activeCount}`, + ]; + + if (payload.activeSessions.sessions.length === 0) { + lines.push('- none'); + } else { + for (const session of payload.activeSessions.sessions) { + lines.push(`- ${formatCode(session.id)} [${session.harness}/${session.adapterId}] ${session.state}`); + lines.push(` - Repo: ${session.repoRoot || '(unknown)'}`); + lines.push(` - Started: ${session.startedAt || '(unknown)'}`); + lines.push(` - Workers: ${session.workerCount}`); + } + } + + const skillSummary = payload.skillRuns.summary; + lines.push( + '', + '## Skill Runs', + '', + `Window size: ${payload.skillRuns.windowSize}`, + `Success: ${skillSummary.successCount}`, + `Failure: ${skillSummary.failureCount}`, + `Unknown: ${skillSummary.unknownCount}`, + `Success rate: ${formatPercent(skillSummary.successRate)}`, + `Failure rate: ${formatPercent(skillSummary.failureRate)}` + ); + + if (payload.skillRuns.recent.length === 0) { + lines.push('', 'Recent runs: none'); + } else { + lines.push('', 'Recent runs:'); + for (const skillRun of payload.skillRuns.recent.slice(0, 5)) { + lines.push(`- ${formatCode(skillRun.id)} ${skillRun.outcome} ${skillRun.skillId}@${skillRun.skillVersion}`); + } + } + + lines.push( + '', + '## Install Health', + '', + `Install health: ${payload.installHealth.status}`, + `Targets recorded: ${payload.installHealth.totalCount}`, + `Healthy: ${payload.installHealth.healthyCount}`, + `Warning: ${payload.installHealth.warningCount}` + ); + + if (payload.installHealth.installations.length === 0) { + lines.push('', 'Installations: none'); + } else { + lines.push('', 'Installations:'); + for (const installation of payload.installHealth.installations.slice(0, 5)) { + lines.push(`- ${formatCode(installation.targetId)} ${installation.status}`); + lines.push(` - Root: ${installation.targetRoot}`); + lines.push(` - Profile: ${installation.profile || '(custom)'}`); + lines.push(` - Modules: ${installation.moduleCount}`); + lines.push(` - Source version: ${installation.sourceVersion || '(unknown)'}`); + } + } + + lines.push( + '', + '## Governance', + '', + `Pending governance events: ${payload.governance.pendingCount}` + ); + + if (payload.governance.events.length === 0) { + lines.push('- none'); + } else { + for (const event of payload.governance.events) { + lines.push(`- ${formatCode(event.id)} ${event.eventType}`); + lines.push(` - Session: ${event.sessionId || '(none)'}`); + lines.push(` - Created: ${event.createdAt}`); + } + } + + return `${lines.join('\n')}\n`; +} + +function writeOutput(writePath, output) { + const absolutePath = path.resolve(writePath); + fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); + fs.writeFileSync(absolutePath, output, 'utf8'); +} + async function main() { let store = null; @@ -153,8 +280,21 @@ async function main() { }; if (options.json) { - console.log(JSON.stringify(payload, null, 2)); + const output = `${JSON.stringify(payload, null, 2)}\n`; + if (options.writePath) { + writeOutput(options.writePath, output); + } + process.stdout.write(output); + } else if (options.markdown) { + const output = renderMarkdown(payload); + if (options.writePath) { + writeOutput(options.writePath, output); + } + process.stdout.write(output); } else { + if (options.writePath) { + throw new Error('--write requires --json or --markdown'); + } printHuman(payload); } } catch (error) { @@ -174,4 +314,5 @@ if (require.main === module) { module.exports = { main, parseArgs, + renderMarkdown, }; diff --git a/tests/lib/state-store.test.js b/tests/lib/state-store.test.js index 8c41beb8..b8a9b6d3 100644 --- a/tests/lib/state-store.test.js +++ b/tests/lib/state-store.test.js @@ -576,6 +576,31 @@ async function runTests() { } })) passed += 1; else failed += 1; + if (await test('status CLI can emit and write markdown operator snapshots', async () => { + const testDir = createTempDir('ecc-state-cli-'); + const dbPath = path.join(testDir, 'state.db'); + const outputPath = path.join(testDir, 'status.md'); + + try { + await seedStore(dbPath); + + const result = runNode(STATUS_SCRIPT, ['--db', dbPath, '--markdown', '--write', outputPath]); + assert.strictEqual(result.status, 0, result.stderr); + assert.ok(fs.existsSync(outputPath)); + + const written = fs.readFileSync(outputPath, 'utf8'); + assert.strictEqual(result.stdout, written); + assert.match(written, /^# ECC Status/m); + assert.match(written, /Database: `[^`]+state\.db`/); + assert.match(written, /- `session-active` \[claude\/dmux-tmux\] active/); + assert.match(written, /Success rate: 66\.7%/); + assert.match(written, /Install health: healthy/); + assert.match(written, /Pending governance events: 1/); + } finally { + cleanupTempDir(testDir); + } + })) passed += 1; else failed += 1; + if (await test('sessions CLI supports list and detail views in human-readable and --json output', async () => { const testDir = createTempDir('ecc-state-cli-'); const dbPath = path.join(testDir, 'state.db');