@geraldmaron/construct 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -149,6 +149,8 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
149
149
  | `construct ask` | One-shot ask against the active knowledge index |
150
150
  | `construct bootstrap` | Import seed observation corpus into local memory store for cold-start acceleration |
151
151
  | `construct customer` | Manage customer profiles for product intelligence |
152
+ | `construct demo` | Record reproducible terminal demos via VHS/asciinema (optional system binaries; ADR-0001) |
153
+ | `construct diagram` | Render code-driven diagrams via D2/Graphviz (optional system binaries; ADR-0001) |
152
154
  | `construct distill` | Distill documents with query-focused chunking |
153
155
  | `construct drop` | Ingest file from Downloads/Desktop |
154
156
  | `construct export` | Export markdown to PDF/DOCX/HTML via Pandoc + Typst (optional system binaries; ADR-0024) |
package/bin/construct CHANGED
@@ -1229,7 +1229,7 @@ async function resolveServiceSelection(args, selectable) {
1229
1229
  return null;
1230
1230
  }
1231
1231
 
1232
- async function cmdUp(args = []) {
1232
+ async function cmdDev(args = []) {
1233
1233
  const { SELECTABLE_SERVICES } = await import('../lib/service-manager.mjs');
1234
1234
 
1235
1235
  // --only=a,b,c picks services non-interactively; --select opens a checklist.
@@ -1365,7 +1365,7 @@ async function cmdUp(args = []) {
1365
1365
  opLog.close(failed.length ? 'degraded' : 'ok', { failed: failed.map((r) => r.name) });
1366
1366
  }
1367
1367
 
1368
- async function cmdDown() {
1368
+ async function cmdStop() {
1369
1369
  const { stopServices } = await import('../lib/service-manager.mjs');
1370
1370
  const result = await stopServices({ homeDir: HOME, rootDir: ROOT_DIR });
1371
1371
  for (const svc of result.results) {
@@ -1384,7 +1384,7 @@ async function cmdDown() {
1384
1384
  } catch { /* best effort */ }
1385
1385
  }
1386
1386
 
1387
- async function cmdServe() {
1387
+ async function cmdDashboard() {
1388
1388
  if (restArgsCache.includes('--token')) {
1389
1389
  const { generateToken, setDashboardToken, getDashboardToken } = await import('../lib/server/auth.mjs');
1390
1390
  const existing = getDashboardToken();
@@ -1951,7 +1951,7 @@ async function cmdHosts() {
1951
1951
  printHostCapabilities();
1952
1952
  }
1953
1953
  }
1954
- async function cmdSetup(args) {
1954
+ async function cmdInstall(args) {
1955
1955
  const { startOpLog } = await import('../lib/op-log.mjs');
1956
1956
  const opLog = startOpLog('install', { homeDir: HOME });
1957
1957
  opLog.event('args', { args });
@@ -2402,7 +2402,7 @@ async function cmdIntakeProcess(args, cwd) {
2402
2402
  errorln('Choose one:');
2403
2403
  errorln(' construct intake process --wait Block until the holder finishes (default 30s).');
2404
2404
  errorln(' construct intake process --wait=<sec> Block for a custom window.');
2405
- errorln(' construct down Stop background services, then retry.');
2405
+ errorln(' construct stop Stop background services, then retry.');
2406
2406
  process.exit(2);
2407
2407
  }
2408
2408
  throw err;
@@ -5209,11 +5209,11 @@ async function readStdin() {
5209
5209
 
5210
5210
  const handlers = new Map([
5211
5211
  // Core
5212
- ['dev', cmdUp],
5213
- ['dashboard', cmdServe],
5214
- ['stop', cmdDown],
5212
+ ['dev', cmdDev],
5213
+ ['dashboard', cmdDashboard],
5214
+ ['stop', cmdStop],
5215
5215
  ['status', cmdStatus],
5216
- ['install', cmdSetup],
5216
+ ['install', cmdInstall],
5217
5217
  ['config', cmdConfig],
5218
5218
  ['intake', cmdIntake],
5219
5219
  ['recommendations', cmdRecommendations],
@@ -5838,6 +5838,14 @@ const handlers = new Map([
5838
5838
  const { runWireframeCli } = await import('../lib/wireframe.mjs');
5839
5839
  return runWireframeCli(args);
5840
5840
  }],
5841
+ ['diagram', async (args) => {
5842
+ const { runDiagramCli } = await import('../lib/diagram.mjs');
5843
+ return runDiagramCli(args);
5844
+ }],
5845
+ ['demo', async (args) => {
5846
+ const { runDemoCli } = await import('../lib/demo.mjs');
5847
+ return runDemoCli(args);
5848
+ }],
5841
5849
  ['skills', async (args) => {
5842
5850
  const sub = args[0];
5843
5851
  if (sub === 'scope' || !sub) {
@@ -6355,6 +6363,67 @@ if (command === '--version' || command === '-V') {
6355
6363
  process.exit(0);
6356
6364
  }
6357
6365
 
6366
+ // Unknown-flag guidance for commands that fully declare their flags (strictFlags
6367
+ // in the spec). A stray or typo'd flag on these is a user error, not a no-op:
6368
+ // reject it with the nearest declared flag and the command's help, matching how
6369
+ // install (lib/setup.mjs KNOWN_FLAGS) already behaves, so no flag is silently
6370
+ // swallowed. Opt-in keeps commands that accept undeclared flags unaffected.
6371
+
6372
+ function validateKnownFlags(name, argv) {
6373
+ const spec = CLI_COMMANDS.find((c) => c.name === name);
6374
+ if (!spec || !spec.strictFlags) return;
6375
+
6376
+ const exact = new Set(['--help', '-h']);
6377
+ const prefixes = [];
6378
+ for (const opt of spec.options || []) {
6379
+ for (const part of String(opt.flag).split(',')) {
6380
+ const token = part.trim().split('=')[0].split(/\s/)[0];
6381
+ if (!token.startsWith('-')) continue;
6382
+ if (/-<[^>]+>$/.test(token)) prefixes.push(token.replace(/<[^>]+>$/, ''));
6383
+ else exact.add(token);
6384
+ }
6385
+ }
6386
+
6387
+ const declared = [...exact].filter((f) => f.startsWith('--'));
6388
+ const dist = (a, b) => {
6389
+ const row = Array(b.length + 1).fill(0).map((_, i) => i);
6390
+ for (let i = 1; i <= a.length; i += 1) {
6391
+ let prev = i;
6392
+ for (let j = 1; j <= b.length; j += 1) {
6393
+ const cur = a[i - 1] === b[j - 1] ? row[j - 1] : 1 + Math.min(row[j - 1], row[j], prev);
6394
+ row[j - 1] = prev;
6395
+ prev = cur;
6396
+ }
6397
+ row[b.length] = prev;
6398
+ }
6399
+ return row[b.length];
6400
+ };
6401
+
6402
+ const unknown = [];
6403
+ for (const tok of argv) {
6404
+ if (tok === '--') break;
6405
+ if (!/^--?[A-Za-z]/.test(tok)) continue;
6406
+ const bare = tok.split('=')[0];
6407
+ if (exact.has(bare) || prefixes.some((p) => bare.startsWith(p))) continue;
6408
+ unknown.push(bare);
6409
+ }
6410
+ if (unknown.length === 0) return;
6411
+
6412
+ for (const flag of unknown) {
6413
+ errorln(`Unknown flag: ${flag}`);
6414
+ let best = 4;
6415
+ let suggestion = null;
6416
+ for (const cand of declared) {
6417
+ const d = dist(flag, cand);
6418
+ if (d < best) { best = d; suggestion = cand; }
6419
+ }
6420
+ if (suggestion) errorln(`Did you mean: ${suggestion}?`);
6421
+ }
6422
+ println('');
6423
+ println(`${COLORS.dim}Run 'construct ${name} --help' for valid options${COLORS.reset}`);
6424
+ process.exit(1);
6425
+ }
6426
+
6358
6427
  const handler = handlers.get(command);
6359
6428
  if (!handler) {
6360
6429
  errorln(`Unknown command: ${command}`);
@@ -6433,7 +6502,9 @@ if (command !== 'hook') {
6433
6502
 
6434
6503
  const probe = await maybeFirstInvocationProbe({ command, homeDir: HOME, env: process.env });
6435
6504
  if (probe?.runSetup) {
6436
- await cmdSetup([]);
6505
+ await cmdInstall([]);
6437
6506
  }
6438
6507
 
6508
+ validateKnownFlags(command, rest);
6509
+
6439
6510
  await handler(rest);
@@ -1,22 +1,68 @@
1
1
  /**
2
- * lib/audit-trail.mjs — reads the append-only audit log produced by
3
- * lib/hooks/audit-trail.mjs and renders it for `construct audit trail`.
2
+ * lib/audit-trail.mjs — owns the append-only audit log written by
3
+ * lib/hooks/audit-trail.mjs and rendered for `construct audit trail`.
4
4
  *
5
5
  * The log is JSONL at ~/.cx/audit-trail.jsonl. Each line carries a
6
6
  * prev_line_hash pointing at the SHA-256 of the previous line — tampering
7
7
  * (reorder, delete, edit) breaks the chain and is surfaced by --verify.
8
+ *
9
+ * A serial hash chain cannot survive concurrent appends: a read-prev-hash +
10
+ * append done outside a critical section lets two processes (hook, daemon,
11
+ * parallel tool calls in one session) both chain off the same predecessor, so
12
+ * the second link points at a stale tail. appendAuditRecord
13
+ * closes that window by computing prev_line_hash and appending inside a single
14
+ * cross-process file lock, making every writer effectively serialized.
15
+ *
16
+ * Corrupted links cannot be re-derived, so a `chain_reset` boundary line seals
17
+ * the damaged prefix as immutable legacy and re-bases the live chain;
18
+ * verifyChain validates only from the last boundary forward. See
19
+ * docs/concepts/observability.mdx.
8
20
  */
9
- import { readFileSync, existsSync, statSync } from 'node:fs';
21
+ import { readFileSync, existsSync, statSync, mkdirSync } from 'node:fs';
10
22
  import { createHash } from 'node:crypto';
11
- import { join } from 'node:path';
23
+ import { join, dirname } from 'node:path';
12
24
  import { homedir } from 'node:os';
13
25
 
26
+ import { appendBounded, readLastLineAcrossSegments } from './logging/rotate.mjs';
27
+ import { withFileLockSync } from './storage/file-lock.mjs';
28
+
14
29
  const AUDIT_FILE = join(homedir(), '.cx', 'audit-trail.jsonl');
15
30
 
16
31
  function sha256(input) {
17
32
  return createHash('sha256').update(input).digest('hex');
18
33
  }
19
34
 
35
+ // prev_line_hash and the append must be one atomic step or concurrent writers
36
+ // race; the file lock serializes them across processes.
37
+
38
+ export function appendAuditRecord(partial, { file = AUDIT_FILE } = {}) {
39
+ return withFileLockSync(file, () => {
40
+ let prev = null;
41
+ try {
42
+ const last = readLastLineAcrossSegments(file);
43
+ if (last !== null) prev = sha256(last);
44
+ } catch { /* first record, or unreadable tail */ }
45
+ const record = { ...partial, prev_line_hash: prev };
46
+ mkdirSync(dirname(file), { recursive: true });
47
+ appendBounded('audit-trail', file, JSON.stringify(record) + '\n');
48
+ return record;
49
+ });
50
+ }
51
+
52
+ // A reset boundary seals the records it follows as legacy excluded from
53
+ // verification and re-bases the live chain from this line.
54
+
55
+ export function writeChainReset({ file = AUDIT_FILE, reason = 'manual reset' } = {}) {
56
+ return appendAuditRecord({
57
+ ts: new Date().toISOString(),
58
+ chain_reset: true,
59
+ reason,
60
+ agent: 'construct',
61
+ tool: null,
62
+ target: null,
63
+ }, { file });
64
+ }
65
+
20
66
  export function readAuditTrail({ limit = 50, since = null, agent = null, tool = null } = {}) {
21
67
  if (!existsSync(AUDIT_FILE)) return [];
22
68
  const lines = readFileSync(AUDIT_FILE, 'utf8').split('\n').filter(Boolean);
@@ -34,19 +80,29 @@ export function readAuditTrail({ limit = 50, since = null, agent = null, tool =
34
80
  return filtered.slice(-limit);
35
81
  }
36
82
 
37
- export function verifyChain() {
38
- if (!existsSync(AUDIT_FILE)) return { ok: true, verified: 0, broken: [] };
39
- const lines = readFileSync(AUDIT_FILE, 'utf8').split('\n').filter(Boolean);
83
+ export function verifyChain(file = AUDIT_FILE) {
84
+ if (!existsSync(file)) return { ok: true, verified: 0, broken: [], legacy: 0 };
85
+ const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
86
+
87
+ // Validation starts at the last reset boundary; the prefix it seals is legacy
88
+ // and exempt, so concurrency damage predating the migration can't fail the chain.
89
+
90
+ let start = 0;
91
+ for (let i = 0; i < lines.length; i += 1) {
92
+ try { if (JSON.parse(lines[i]).chain_reset) start = i; } catch { /* legacy malformed */ }
93
+ }
94
+
40
95
  const broken = [];
41
96
  let prevHash = null;
42
- for (let i = 0; i < lines.length; i += 1) {
97
+ for (let i = start; i < lines.length; i += 1) {
43
98
  const line = lines[i];
44
99
  let row;
45
100
  try { row = JSON.parse(line); } catch {
46
101
  broken.push({ line: i + 1, reason: 'malformed JSON' });
102
+ prevHash = sha256(line);
47
103
  continue;
48
104
  }
49
- if (i > 0 && row.prev_line_hash !== prevHash) {
105
+ if (i > start && !row.chain_reset && row.prev_line_hash !== prevHash) {
50
106
  broken.push({
51
107
  line: i + 1,
52
108
  expected: prevHash,
@@ -56,7 +112,7 @@ export function verifyChain() {
56
112
  }
57
113
  prevHash = sha256(line);
58
114
  }
59
- return { ok: broken.length === 0, verified: lines.length, broken };
115
+ return { ok: broken.length === 0, verified: lines.length - start, broken, legacy: start };
60
116
  }
61
117
 
62
118
  function formatRow(row) {
@@ -74,6 +130,9 @@ export async function runAuditTrailCli(args = []) {
74
130
  for (let i = 0; i < args.length; i += 1) {
75
131
  const arg = args[i];
76
132
  if (arg === '--verify') flags.add('verify');
133
+ else if (arg === '--reset') flags.add('reset');
134
+ else if (arg.startsWith('--reason=')) options.reason = arg.slice(9);
135
+ else if (arg === '--reason') options.reason = args[++i];
77
136
  else if (arg === '--json') flags.add('json');
78
137
  else if (arg === '--all') flags.add('all');
79
138
  else if (arg.startsWith('--limit=')) options.limit = Number(arg.slice(8));
@@ -97,6 +156,8 @@ Options:
97
156
  --agent <name> Filter to a specific agent (e.g. cx-engineer)
98
157
  --tool <name> Filter to a specific tool (Edit, Write, MultiEdit, NotebookEdit, Bash)
99
158
  --verify Verify the tamper-evidence chain and exit
159
+ --reset Seal the current chain as legacy and re-base a fresh chain
160
+ --reason <text> Reason recorded on the --reset boundary
100
161
  --json Emit raw JSONL lines instead of formatted output
101
162
  -h, --help Show this message
102
163
 
@@ -105,6 +166,12 @@ Log location: ${AUDIT_FILE}
105
166
  return;
106
167
  }
107
168
 
169
+ if (flags.has('reset')) {
170
+ writeChainReset({ reason: options.reason || 'manual reset' });
171
+ console.log(`Audit chain re-based at ${AUDIT_FILE}. Prior records sealed as legacy; verification resumes from the new boundary.`);
172
+ return;
173
+ }
174
+
108
175
  if (flags.has('verify')) {
109
176
  const result = verifyChain();
110
177
  if (result.ok) {
@@ -23,6 +23,10 @@ export const CLI_COMMANDS = [
23
23
  core: true,
24
24
  description: 'Start services for development',
25
25
  usage: 'construct dev [--select] [--only=postgres,dashboard,...]',
26
+ examples: [
27
+ { cmd: 'construct dev', desc: 'Start the default service set' },
28
+ { cmd: 'construct dev --only=dashboard', desc: 'Start just the dashboard' },
29
+ ],
26
30
  options: [
27
31
  { flag: '--select', desc: 'Pick which services to start from an interactive checklist' },
28
32
  { flag: '--only=<a,b,c>', desc: 'Start only the named services (postgres, dashboard, telemetry, memory, opencode)' },
@@ -35,6 +39,10 @@ export const CLI_COMMANDS = [
35
39
  core: true,
36
40
  description: 'Start the local dashboard/orchestration daemon (or --token to mint a dashboard token)',
37
41
  usage: 'construct dashboard [--token]',
42
+ examples: [
43
+ { cmd: 'construct dashboard', desc: 'Launch the local dashboard daemon' },
44
+ { cmd: 'construct dashboard --token', desc: 'Mint a dashboard access token' },
45
+ ],
38
46
  },
39
47
  {
40
48
  name: 'stop',
@@ -43,6 +51,9 @@ export const CLI_COMMANDS = [
43
51
  core: true,
44
52
  description: 'Stop all running services',
45
53
  usage: 'construct stop',
54
+ examples: [
55
+ { cmd: 'construct stop', desc: 'Stop every running service' },
56
+ ],
46
57
  },
47
58
  {
48
59
  name: 'status',
@@ -51,6 +62,11 @@ export const CLI_COMMANDS = [
51
62
  core: true,
52
63
  description: 'Show system health and credentials',
53
64
  usage: 'construct status',
65
+ strictFlags: true,
66
+ examples: [
67
+ { cmd: 'construct status', desc: 'Human-readable health summary' },
68
+ { cmd: 'construct status --json', desc: 'Full payload for scripting' },
69
+ ],
54
70
  options: [
55
71
  { flag: '--json', desc: 'Output full status payload as JSON' },
56
72
  ],
@@ -61,10 +77,15 @@ export const CLI_COMMANDS = [
61
77
  category: 'Core',
62
78
  core: true,
63
79
  description: 'Machine setup (scoped per ADR-0029): --scope=project|user|both, default project',
64
- usage: 'construct install [--scope=project|user|both] [--yes] [--no-docker] [--no-launch-agent] [--reconfigure] [--with-docling]',
80
+ usage: 'construct install [--scope=project|user|both] [--yes] [--dry-run] [--no-docker] [--no-launch-agent] [--reconfigure] [--with-docling]',
81
+ examples: [
82
+ { cmd: 'construct install --dry-run', desc: 'Preview the plan before writing anything' },
83
+ { cmd: 'construct install --scope=user --yes', desc: 'Non-interactive user-scope install' },
84
+ ],
65
85
  options: [
66
86
  { flag: '--scope=<s>', desc: 'project (default, no-op + guidance) | user (writes ~/.construct/, MCP, ~/.claude/* via consent) | both' },
67
87
  { flag: '--yes', desc: 'Apply defaults without prompts (only meaningful with --scope=user|both)' },
88
+ { flag: '--dry-run', desc: 'Preview the install plan (scopes, files, services) without writing anything' },
68
89
  { flag: '--no-docker', desc: 'Skip Docker-based service setup (local Postgres)' },
69
90
  { flag: '--no-launch-agent', desc: 'Skip background macOS LaunchAgent registration' },
70
91
  { flag: '--reconfigure', desc: 'Re-prompt for service consent, ignoring cached answers' },
@@ -78,6 +99,10 @@ export const CLI_COMMANDS = [
78
99
  core: true,
79
100
  description: 'Project setup (once per repo): scaffold .cx/, AGENTS.md, plan.md, adapters',
80
101
  usage: 'construct init [path] [options]',
102
+ examples: [
103
+ { cmd: 'construct init', desc: 'Scaffold the current repo interactively' },
104
+ { cmd: 'construct init --yes --with-adrs', desc: 'Non-interactive init with the ADR doc lane' },
105
+ ],
81
106
  options: [
82
107
  { flag: '--yes', desc: 'Accept all defaults (non-interactive)' },
83
108
  { flag: '--no-start', desc: 'Do not start services after init' },
@@ -103,6 +128,10 @@ export const CLI_COMMANDS = [
103
128
  core: true,
104
129
  description: 'Sync agent adapters to AI tools',
105
130
  usage: 'construct sync [--project] [--dry-run] [--no-docs] [--compress-personas]',
131
+ examples: [
132
+ { cmd: 'construct sync', desc: 'Sync adapters to detected AI tools' },
133
+ { cmd: 'construct sync --dry-run', desc: 'Preview adapter changes without writing' },
134
+ ],
106
135
  options: [
107
136
  { flag: '--project', desc: 'Write project-local Claude adapters into the current repo only' },
108
137
  { flag: '--dry-run', desc: 'Preview adapter changes without writing files' },
@@ -117,6 +146,10 @@ export const CLI_COMMANDS = [
117
146
  core: true,
118
147
  description: 'View and process the active profile\'s intake queue (queue label varies by profile)',
119
148
  usage: 'construct intake list|show|done|skip|reopen|integrate|classify',
149
+ examples: [
150
+ { cmd: 'construct intake list', desc: 'See pending packets' },
151
+ { cmd: 'construct intake done <id> --output=path', desc: 'Mark processed and stamp the artifact' },
152
+ ],
120
153
  subcommands: [
121
154
  { name: 'list', desc: 'List pending packets' },
122
155
  { name: 'show <id>', desc: 'Show one packet (triage, related docs, excerpt, tag suggestions)' },
@@ -134,6 +167,10 @@ export const CLI_COMMANDS = [
134
167
  core: true,
135
168
  description: 'View and manage artifact recommendations',
136
169
  usage: 'construct recommendations list|show|dismiss|stats',
170
+ examples: [
171
+ { cmd: 'construct recommendations list', desc: 'See open recommendations' },
172
+ { cmd: 'construct recommendations dismiss <id>', desc: 'Dismiss one by id' },
173
+ ],
137
174
  },
138
175
  {
139
176
  name: 'integrations',
@@ -180,6 +217,10 @@ export const CLI_COMMANDS = [
180
217
  core: true,
181
218
  description: 'Isolated tmpdir-based environment for QA / specialist dry-runs',
182
219
  usage: 'construct sandbox create|list|delete|prune [--profile=<id>]',
220
+ examples: [
221
+ { cmd: 'construct sandbox create --profile=<id>', desc: 'Spin up an isolated sandbox' },
222
+ { cmd: 'construct sandbox prune --days=7', desc: 'Remove sandboxes older than a week' },
223
+ ],
183
224
  subcommands: [
184
225
  { name: 'create [--profile=<id>]', desc: 'Create a new sandbox under ~/.cx/sandboxes/' },
185
226
  { name: 'list', desc: 'List existing sandboxes, newest first' },
@@ -194,6 +235,10 @@ export const CLI_COMMANDS = [
194
235
  core: true,
195
236
  description: 'Manage the active org profile and its lifecycle (draft, promote, archive, health)',
196
237
  usage: 'construct profile show|list|set|create|drafts|archive|health',
238
+ examples: [
239
+ { cmd: 'construct profile list', desc: 'List curated profiles' },
240
+ { cmd: 'construct profile set <id>', desc: 'Switch the active profile' },
241
+ ],
197
242
  subcommands: [
198
243
  { name: 'show', desc: 'Show the active profile' },
199
244
  { name: 'list', desc: 'List curated profiles' },
@@ -225,6 +270,10 @@ export const CLI_COMMANDS = [
225
270
  core: true,
226
271
  description: 'Documentation commands',
227
272
  usage: 'construct docs check|verify|update',
273
+ examples: [
274
+ { cmd: 'construct docs verify', desc: 'Validate the docs tree' },
275
+ { cmd: 'construct docs update', desc: 'Regenerate AUTO-managed regions' },
276
+ ],
228
277
  subcommands: [
229
278
  { name: 'check', desc: 'Check for missing how-to guides' },
230
279
  { name: 'verify', desc: 'Validate documentation quality' },
@@ -238,6 +287,10 @@ export const CLI_COMMANDS = [
238
287
  core: true,
239
288
  description: 'Check installation health',
240
289
  usage: 'construct doctor [<status|logs|tick|report|consistency|watch|stop|credentials>] [--fix-legacy-agents]',
290
+ examples: [
291
+ { cmd: 'construct doctor', desc: 'Run all health checks once' },
292
+ { cmd: 'construct doctor report', desc: 'Print the latest health report' },
293
+ ],
241
294
  subcommands: [
242
295
  { name: 'status', desc: 'Doctor daemon status' },
243
296
  { name: 'logs', desc: 'Tail doctor daemon logs' },
@@ -488,6 +541,7 @@ export const CLI_COMMANDS = [
488
541
  core: false,
489
542
  description: 'Release dev-agent memory pressure by cleaning stale helper and bridge processes',
490
543
  usage: 'construct cleanup [--dry-run] [--quiet] [--pressure-release] [--pressure-only] [--disk-only]',
544
+ strictFlags: true,
491
545
  options: [
492
546
  { flag: '--dry-run', desc: 'Show what would be cleaned without changing anything' },
493
547
  { flag: '--quiet', desc: 'Minimal output' },
@@ -560,6 +614,36 @@ export const CLI_COMMANDS = [
560
614
  description: 'Generate wireframes from description',
561
615
  usage: 'construct wireframe <description>',
562
616
  },
617
+ {
618
+ name: 'diagram',
619
+ emoji: '📊',
620
+ category: 'Work',
621
+ core: false,
622
+ description: 'Render code-driven diagrams via D2/Graphviz (optional system binaries; ADR-0001)',
623
+ usage: 'construct diagram <description> [--type=architecture|flow|sequence|state|er|class] [--format=svg|png] [--theme=<name>] [--out=<path>] [--source-only]',
624
+ strictFlags: true,
625
+ options: [
626
+ { flag: '--type=<t>', desc: 'architecture (default) | flow | sequence | state | er | class' },
627
+ { flag: '--format=<f>', desc: 'svg (default) | png' },
628
+ { flag: '--theme=<name>', desc: 'D2 theme name (e.g. neutral, sketch, cool-classics)' },
629
+ { flag: '--out=<path>', desc: 'Output path (default: .cx/diagrams/<slug>-<ts>.<ext>)' },
630
+ { flag: '--source-only', desc: 'Always write the source file; skip rendering' },
631
+ ],
632
+ },
633
+ {
634
+ name: 'demo',
635
+ emoji: '🎬',
636
+ category: 'Work',
637
+ core: false,
638
+ description: 'Record reproducible terminal demos via VHS/asciinema (optional system binaries; ADR-0001)',
639
+ usage: 'construct demo <name> [--format=gif|mp4|webm] [--out=<path>] [--source-only]',
640
+ strictFlags: true,
641
+ options: [
642
+ { flag: '--format=<f>', desc: 'gif (default) | mp4 | webm (VHS only)' },
643
+ { flag: '--out=<path>', desc: 'Output path (default: .cx/demos/<name>-<ts>.<ext>)' },
644
+ { flag: '--source-only', desc: 'Always write the .tape source; skip recording' },
645
+ ],
646
+ },
563
647
  {
564
648
  name: 'ollama',
565
649
  emoji: '🦙',
@@ -1035,5 +1119,28 @@ export function formatCommandHelp(name, { colors = false } = {}) {
1035
1119
  }
1036
1120
  lines.push('');
1037
1121
  }
1122
+
1123
+ if (spec.examples && spec.examples.length > 0) {
1124
+ lines.push(`${c.bold}Examples${c.reset}`);
1125
+ for (const ex of spec.examples) {
1126
+ lines.push(` ${ex.cmd}`);
1127
+ if (ex.desc) lines.push(` ${c.dim}${ex.desc}${c.reset}`);
1128
+ }
1129
+ lines.push('');
1130
+ }
1131
+
1132
+ // Every help block closes with the obvious next action: an explicit `next`,
1133
+ // else "run a subcommand" for grouped commands, plus a See also pointer to the
1134
+ // full reference — so no surface is a dead end.
1135
+
1136
+ const firstSub = spec.subcommands?.[0]?.name.split(/\s+/)[0];
1137
+ const next = spec.next
1138
+ || (firstSub ? `Run \`construct ${spec.name} ${firstSub}\` to start; add --help to any subcommand for details.` : null);
1139
+ if (next) {
1140
+ lines.push(`${c.dim}Next:${c.reset} ${next}`);
1141
+ lines.push('');
1142
+ }
1143
+ lines.push(`${c.dim}See also:${c.reset} run \`construct --help\` for all commands; full reference in docs/reference/cli/.`);
1144
+ lines.push('');
1038
1145
  return lines.join('\n');
1039
1146
  }