@geraldmaron/construct 1.0.24 → 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.
Files changed (87) hide show
  1. package/README.md +2 -0
  2. package/bin/construct +266 -13
  3. package/lib/agent-instructions/inject.mjs +25 -4
  4. package/lib/audit-rules.mjs +127 -0
  5. package/lib/audit-skills.mjs +43 -1
  6. package/lib/audit-trail.mjs +77 -10
  7. package/lib/beads-client.mjs +9 -0
  8. package/lib/beads-optimistic.mjs +23 -71
  9. package/lib/bridges/copilot-proxy.mjs +116 -0
  10. package/lib/cli-commands.mjs +112 -1
  11. package/lib/comment-lint.mjs +1 -1
  12. package/lib/config/schema.mjs +1 -1
  13. package/lib/demo.mjs +245 -0
  14. package/lib/diagram.mjs +300 -0
  15. package/lib/doctor/index.mjs +1 -1
  16. package/lib/doctor/watchers/process-pressure.mjs +1 -1
  17. package/lib/doctor/watchers/service-health.mjs +1 -1
  18. package/lib/document-extract/docling-client.mjs +16 -6
  19. package/lib/document-extract/docling-sidecar.py +32 -2
  20. package/lib/document-extract.mjs +85 -10
  21. package/lib/document-ingest.mjs +97 -7
  22. package/lib/embed/roadmap.mjs +16 -1
  23. package/lib/embed/semantic.mjs +5 -2
  24. package/lib/engine/consolidate.mjs +160 -3
  25. package/lib/engine/contradiction-judge.mjs +71 -0
  26. package/lib/engine/contradiction.mjs +74 -0
  27. package/lib/handoffs/cleanup.mjs +1 -1
  28. package/lib/hooks/audit-trail.mjs +14 -28
  29. package/lib/host-capabilities.mjs +30 -0
  30. package/lib/ingest/docling-remote.mjs +90 -0
  31. package/lib/ingest/strategy.mjs +1 -1
  32. package/lib/init-unified.mjs +9 -13
  33. package/lib/logging/rotate.mjs +18 -0
  34. package/lib/mcp/server.mjs +124 -12
  35. package/lib/mcp/tool-budget.mjs +53 -0
  36. package/lib/mcp-catalog.json +1 -1
  37. package/lib/model-router.mjs +52 -1
  38. package/lib/ollama/capability-store.mjs +78 -0
  39. package/lib/ollama/provision-context.mjs +344 -0
  40. package/lib/opencode-config.mjs +148 -0
  41. package/lib/opencode-telemetry.mjs +7 -0
  42. package/lib/orchestration-policy.mjs +41 -6
  43. package/lib/persona-sections.mjs +66 -0
  44. package/lib/platforms/capabilities.mjs +100 -0
  45. package/lib/prompt-composer.js +14 -9
  46. package/lib/reconcile/agent-instructions-rewrap.mjs +8 -4
  47. package/lib/reflect/extractor.mjs +14 -1
  48. package/lib/reflect/salience.mjs +65 -0
  49. package/lib/rules-delivery.mjs +122 -0
  50. package/lib/runtime/uv-bootstrap.mjs +32 -17
  51. package/lib/server/index.mjs +1 -1
  52. package/lib/server/langfuse-login.mjs +3 -3
  53. package/lib/service-manager.mjs +42 -4
  54. package/lib/session-store.mjs +1 -1
  55. package/lib/setup.mjs +58 -2
  56. package/lib/specialists/prompt-schema.mjs +162 -0
  57. package/lib/specialists/scaffold.mjs +109 -0
  58. package/lib/storage/embeddings-engine.mjs +19 -5
  59. package/lib/storage/embeddings-local.mjs +6 -3
  60. package/lib/storage/file-lock.mjs +18 -11
  61. package/lib/telemetry/beads-fallback.mjs +40 -0
  62. package/lib/telemetry/hook-calls.mjs +138 -0
  63. package/package.json +4 -2
  64. package/personas/construct.md +12 -12
  65. package/platforms/capabilities.json +76 -0
  66. package/platforms/opencode/sync-config.mjs +121 -25
  67. package/rules/common/neurodivergent-output.md +1 -1
  68. package/rules/web/coding-style.md +8 -0
  69. package/rules/web/design-quality.md +8 -0
  70. package/rules/web/hooks.md +8 -0
  71. package/rules/web/patterns.md +8 -0
  72. package/rules/web/performance.md +8 -0
  73. package/rules/web/security.md +8 -0
  74. package/rules/web/testing.md +8 -0
  75. package/scripts/sync-specialists.mjs +277 -63
  76. package/skills/docs/init-project.md +1 -1
  77. package/specialists/prompts/cx-architect.md +20 -0
  78. package/specialists/prompts/cx-test-automation.md +12 -0
  79. package/templates/docs/construct_guide.md +2 -2
  80. package/lib/ingest/chunker.mjs +0 -94
  81. package/lib/ingest/pipeline.mjs +0 -53
  82. package/lib/ingest/store.mjs +0 -82
  83. package/lib/mode-commands.mjs +0 -122
  84. package/lib/policy/unified-gates.mjs +0 -96
  85. package/lib/profiles/validate-custom.mjs +0 -114
  86. package/lib/services/telemetry-backend.mjs +0 -177
  87. package/lib/storage/fusion.mjs +0 -95
@@ -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) {
@@ -252,6 +252,15 @@ export async function runBd(args, options = {}) {
252
252
  * Used as fallback when optimistic locking fails.
253
253
  */
254
254
  async function runWithLegacyLock(args, opts, cwd, commandDesc) {
255
+ // Telemetry: every legacy-fallback firing is recorded so the lock+queue removal
256
+ // decision rests on observed data — if this never fires over a representative
257
+ // window, the whole exclusive-lock path is removable; if it fires, the entries
258
+ // name the bd errors that actually need handling (bead construct-nhn5).
259
+ try {
260
+ const { logBeadsFallback } = await import('./telemetry/beads-fallback.mjs');
261
+ logBeadsFallback({ command: commandDesc || args[0] || 'unknown' });
262
+ } catch { /* telemetry is best-effort */ }
263
+
255
264
  // Clean up any stale locks before trying
256
265
  cleanupStaleLock({ cwd });
257
266
  cleanupStaleQueue({ cwd });
@@ -1,14 +1,14 @@
1
1
  /**
2
- * lib/beads-optimistic.mjs — Optimistic locking and concurrent read support for beads.
2
+ * lib/beads-optimistic.mjs — concurrent reads + retry-on-conflict writes for beads.
3
3
  *
4
- * Addresses the beads lock contention issue by:
5
- * 1. Allowing concurrent reads (no lock required)
6
- * 2. Using optimistic locking for writes (version-based conflict detection)
7
- * 3. Automatic retry with exponential backoff on conflict
8
- * 4. Read-replicas for team mode (when available)
9
- *
10
- * Replaces the exclusive lock model with a more scalable approach
11
- * while maintaining consistency guarantees.
4
+ * Dolt commits are atomic, so it is the serializer: a write is executed and, on a
5
+ * transient conflict, retried with exponential backoff. There is no separate
6
+ * read-then-compare version check that spanned two `bd` processes (a `bd show`
7
+ * to read the commit hash, then a `bd update`) and guarded nothing, since `bd
8
+ * update` accepts no expected version, so the window between the read and the
9
+ * write left the "optimistic lock" unable to actually detect a conflicting commit
10
+ * (bead construct-iufy). Reads run lock-free; `getBeadVersion` remains as an
11
+ * advisory reader, not a write-path primitive.
12
12
  */
13
13
 
14
14
  import { spawnSync } from 'node:child_process';
@@ -34,8 +34,9 @@ const DEFAULT_RETRY_OPTIONS = {
34
34
  // ---------------------------------------------------------------------------
35
35
 
36
36
  /**
37
- * Read the current version of a bead from the database.
38
- * In Dolt, this uses the commit hash as a logical version.
37
+ * Read the current version of a bead from the database (the Dolt commit hash).
38
+ * Advisory only exposed for callers that want to observe a bead's version; the
39
+ * write path does not gate on it (see file header).
39
40
  */
40
41
  export async function getBeadVersion(beadId, cwd = process.cwd()) {
41
42
  try {
@@ -56,75 +57,37 @@ export async function getBeadVersion(beadId, cwd = process.cwd()) {
56
57
  }
57
58
 
58
59
  /**
59
- * Execute a write operation with optimistic locking.
60
- *
60
+ * Execute a bead write, retrying on a transient conflict. Dolt's atomic commit is
61
+ * the serializer; this does not pre-read or compare a version (see file header).
62
+ *
61
63
  * @param {Object} options
62
- * @param {string} options.beadId - The bead being modified
63
64
  * @param {Function} options.execute - Async function that performs the write
64
- * @param {string} options.expectedVersion - Version we expect the bead to have
65
65
  * @param {Object} options.retry - Retry configuration
66
66
  * @returns {Promise<{success: boolean, result: any, attempts: number}>}
67
67
  */
68
68
  export async function optimisticWrite({
69
- beadId,
70
69
  execute,
71
- expectedVersion,
72
70
  retry = DEFAULT_RETRY_OPTIONS,
73
- cwd = process.cwd(),
74
71
  } = {}) {
75
72
  let attempts = 0;
76
73
  let delay = retry.baseDelayMs;
77
-
74
+
78
75
  while (attempts < retry.maxRetries) {
79
76
  attempts++;
80
-
81
- // Verify version hasn't changed (optimistic check)
82
- const currentVersion = await getBeadVersion(beadId, cwd);
83
-
84
- if (expectedVersion && currentVersion !== expectedVersion) {
85
- // Conflict detected - another writer modified the bead
86
- if (attempts >= retry.maxRetries) {
87
- return {
88
- success: false,
89
- error: `Optimistic lock conflict after ${attempts} attempts. Bead ${beadId} was modified by another process.`,
90
- attempts,
91
- currentVersion,
92
- expectedVersion,
93
- };
94
- }
95
-
96
- // Wait with exponential backoff and jitter
97
- const jitter = Math.random() * 50;
98
- await new Promise(r => setTimeout(r, delay + jitter));
99
- delay = Math.min(delay * retry.backoffMultiplier, retry.maxDelayMs);
100
- continue;
101
- }
102
-
103
77
  try {
104
- // Execute the write operation
105
78
  const result = await execute();
106
79
  return { success: true, result, attempts };
107
80
  } catch (error) {
108
81
  if (attempts >= retry.maxRetries) {
109
- return {
110
- success: false,
111
- error: error.message,
112
- attempts,
113
- };
82
+ return { success: false, error: error.message, attempts };
114
83
  }
115
-
116
- // Retry on transient errors
117
84
  const jitter = Math.random() * 50;
118
- await new Promise(r => setTimeout(r, delay + jitter));
85
+ await new Promise((r) => setTimeout(r, delay + jitter));
119
86
  delay = Math.min(delay * retry.backoffMultiplier, retry.maxDelayMs);
120
87
  }
121
88
  }
122
-
123
- return {
124
- success: false,
125
- error: `Max retries (${retry.maxRetries}) exceeded`,
126
- attempts,
127
- };
89
+
90
+ return { success: false, error: `Max retries (${retry.maxRetries}) exceeded`, attempts };
128
91
  }
129
92
 
130
93
  // ---------------------------------------------------------------------------
@@ -187,21 +150,10 @@ export async function updateBeadOptimistic(
187
150
  options = {}
188
151
  ) {
189
152
  const { actor = 'construct', cwd = process.cwd(), notes } = options;
190
-
191
- // Get current version before attempting update
192
- const currentVersion = await getBeadVersion(beadId, cwd);
193
-
194
- if (!currentVersion) {
195
- return {
196
- success: false,
197
- error: `Bead ${beadId} not found`,
198
- };
199
- }
200
-
153
+
154
+ // A `bd update` on a missing bead exits non-zero and surfaces as a failed
155
+ // result, so no separate existence pre-check is needed.
201
156
  return optimisticWrite({
202
- beadId,
203
- expectedVersion: currentVersion,
204
- cwd,
205
157
  execute: async () => {
206
158
  const args = ['update', beadId];
207
159
 
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/bridges/copilot-proxy.mjs — Minimal GitHub Copilot to OpenAI-compatible bridge.
4
+ *
5
+ * Translates standard OpenAI /v1/chat/completions requests to the Copilot API
6
+ * using the user's active `gh` CLI session.
7
+ */
8
+ import http from 'node:http';
9
+ import { execSync } from 'node:child_process';
10
+
11
+ const PORT = parseInt(process.argv.find(a => a.startsWith('--port='))?.split('=')[1] || '5174', 10);
12
+
13
+ let cachedSession = null;
14
+
15
+ async function getCopilotToken() {
16
+ if (cachedSession && cachedSession.expires_at > (Date.now() / 1000) + 60) {
17
+ return cachedSession.token;
18
+ }
19
+
20
+ try {
21
+ const ghToken = execSync('gh auth token', { encoding: 'utf8' }).trim();
22
+ const res = await fetch('https://api.github.com/copilot_internal/v2/token', {
23
+ headers: {
24
+ 'Authorization': `Bearer ${ghToken}`,
25
+ 'Accept': 'application/json',
26
+ }
27
+ });
28
+
29
+ if (!res.ok) throw new Error(`Failed to get session token: ${res.statusText}`);
30
+
31
+ cachedSession = await res.json();
32
+ return cachedSession.token;
33
+ } catch (err) {
34
+ console.error('Error fetching Copilot token:', err.message);
35
+ return null;
36
+ }
37
+ }
38
+
39
+ const server = http.createServer(async (req, res) => {
40
+ // CORS
41
+ res.setHeader('Access-Control-Allow-Origin', '*');
42
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
43
+ res.setHeader('Access-Control-Allow-Headers', '*');
44
+
45
+ if (req.method === 'OPTIONS') {
46
+ res.writeHead(204);
47
+ res.end();
48
+ return;
49
+ }
50
+
51
+ if (req.url === '/v1/chat/completions' && req.method === 'POST') {
52
+ const token = await getCopilotToken();
53
+ if (!token) {
54
+ res.writeHead(401, { 'Content-Type': 'application/json' });
55
+ res.end(JSON.stringify({ error: 'Failed to authenticate with GitHub Copilot via gh CLI.' }));
56
+ return;
57
+ }
58
+
59
+ let body = '';
60
+ req.on('data', chunk => { body += chunk; });
61
+ req.on('end', async () => {
62
+ try {
63
+ const payload = JSON.parse(body);
64
+
65
+ // Map common model names to Copilot equivalents if needed
66
+ // For now, we pass them through or default to gpt-4o
67
+ const model = payload.model?.includes('gpt-4o') ? 'gpt-4o' :
68
+ payload.model?.includes('claude-3.5-sonnet') ? 'claude-3.5-sonnet' :
69
+ 'gpt-4o';
70
+
71
+ const copilotRes = await fetch('https://api.githubcopilot.com/chat/completions', {
72
+ method: 'POST',
73
+ headers: {
74
+ 'Authorization': `Bearer ${token}`,
75
+ 'Content-Type': 'application/json',
76
+ 'Accept': 'application/json',
77
+ 'X-Github-Api-Version': '2023-07-07',
78
+ 'Editor-Version': 'vscode/1.90.0', // Spoof VS Code for compatibility
79
+ },
80
+ body: JSON.stringify({
81
+ ...payload,
82
+ model
83
+ })
84
+ });
85
+
86
+ res.writeHead(copilotRes.status, {
87
+ 'Content-Type': copilotRes.headers.get('Content-Type'),
88
+ });
89
+
90
+ if (payload.stream) {
91
+ const reader = copilotRes.body.getReader();
92
+ while (true) {
93
+ const { done, value } = await reader.read();
94
+ if (done) break;
95
+ res.write(value);
96
+ }
97
+ res.end();
98
+ } else {
99
+ const data = await copilotRes.text();
100
+ res.end(data);
101
+ }
102
+ } catch (err) {
103
+ res.writeHead(500, { 'Content-Type': 'application/json' });
104
+ res.end(JSON.stringify({ error: err.message }));
105
+ }
106
+ });
107
+ return;
108
+ }
109
+
110
+ res.writeHead(404);
111
+ res.end('Not Found');
112
+ });
113
+
114
+ server.listen(PORT, '127.0.0.1', () => {
115
+ console.log(`Copilot Bridge listening on http://127.0.0.1:${PORT}`);
116
+ });
@@ -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,13 +77,19 @@ 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]',
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' },
92
+ { flag: '--with-docling', desc: 'Eagerly provision the docling document-extraction venv now (heavy, ~10 min; else lazy on first ingest)' },
71
93
  ],
72
94
  },
73
95
  {
@@ -77,6 +99,10 @@ export const CLI_COMMANDS = [
77
99
  core: true,
78
100
  description: 'Project setup (once per repo): scaffold .cx/, AGENTS.md, plan.md, adapters',
79
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
+ ],
80
106
  options: [
81
107
  { flag: '--yes', desc: 'Accept all defaults (non-interactive)' },
82
108
  { flag: '--no-start', desc: 'Do not start services after init' },
@@ -102,6 +128,10 @@ export const CLI_COMMANDS = [
102
128
  core: true,
103
129
  description: 'Sync agent adapters to AI tools',
104
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
+ ],
105
135
  options: [
106
136
  { flag: '--project', desc: 'Write project-local Claude adapters into the current repo only' },
107
137
  { flag: '--dry-run', desc: 'Preview adapter changes without writing files' },
@@ -116,6 +146,10 @@ export const CLI_COMMANDS = [
116
146
  core: true,
117
147
  description: 'View and process the active profile\'s intake queue (queue label varies by profile)',
118
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
+ ],
119
153
  subcommands: [
120
154
  { name: 'list', desc: 'List pending packets' },
121
155
  { name: 'show <id>', desc: 'Show one packet (triage, related docs, excerpt, tag suggestions)' },
@@ -133,6 +167,10 @@ export const CLI_COMMANDS = [
133
167
  core: true,
134
168
  description: 'View and manage artifact recommendations',
135
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
+ ],
136
174
  },
137
175
  {
138
176
  name: 'integrations',
@@ -179,6 +217,10 @@ export const CLI_COMMANDS = [
179
217
  core: true,
180
218
  description: 'Isolated tmpdir-based environment for QA / specialist dry-runs',
181
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
+ ],
182
224
  subcommands: [
183
225
  { name: 'create [--profile=<id>]', desc: 'Create a new sandbox under ~/.cx/sandboxes/' },
184
226
  { name: 'list', desc: 'List existing sandboxes, newest first' },
@@ -193,6 +235,10 @@ export const CLI_COMMANDS = [
193
235
  core: true,
194
236
  description: 'Manage the active org profile and its lifecycle (draft, promote, archive, health)',
195
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
+ ],
196
242
  subcommands: [
197
243
  { name: 'show', desc: 'Show the active profile' },
198
244
  { name: 'list', desc: 'List curated profiles' },
@@ -224,6 +270,10 @@ export const CLI_COMMANDS = [
224
270
  core: true,
225
271
  description: 'Documentation commands',
226
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
+ ],
227
277
  subcommands: [
228
278
  { name: 'check', desc: 'Check for missing how-to guides' },
229
279
  { name: 'verify', desc: 'Validate documentation quality' },
@@ -237,6 +287,10 @@ export const CLI_COMMANDS = [
237
287
  core: true,
238
288
  description: 'Check installation health',
239
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
+ ],
240
294
  subcommands: [
241
295
  { name: 'status', desc: 'Doctor daemon status' },
242
296
  { name: 'logs', desc: 'Tail doctor daemon logs' },
@@ -487,6 +541,7 @@ export const CLI_COMMANDS = [
487
541
  core: false,
488
542
  description: 'Release dev-agent memory pressure by cleaning stale helper and bridge processes',
489
543
  usage: 'construct cleanup [--dry-run] [--quiet] [--pressure-release] [--pressure-only] [--disk-only]',
544
+ strictFlags: true,
490
545
  options: [
491
546
  { flag: '--dry-run', desc: 'Show what would be cleaned without changing anything' },
492
547
  { flag: '--quiet', desc: 'Minimal output' },
@@ -559,6 +614,36 @@ export const CLI_COMMANDS = [
559
614
  description: 'Generate wireframes from description',
560
615
  usage: 'construct wireframe <description>',
561
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
+ },
562
647
  {
563
648
  name: 'ollama',
564
649
  emoji: '🦙',
@@ -936,6 +1021,9 @@ export const CLI_COMMANDS = [
936
1021
  { name: 'lint:contracts', category: 'Internal', core: false, internal: true, description: 'Internal lint: specialist contracts', usage: 'construct lint:contracts' },
937
1022
  { name: 'lint:research', category: 'Internal', core: false, internal: true, description: 'Internal lint: research artifacts', usage: 'construct lint:research' },
938
1023
  { name: 'lint:templates', category: 'Internal', core: false, internal: true, description: 'Internal lint: shipped templates', usage: 'construct lint:templates' },
1024
+ { name: 'lint:prompts', category: 'Internal', core: false, internal: true, description: 'Internal lint: specialist prompt frontmatter + sections', usage: 'construct lint:prompts' },
1025
+ { name: 'specialist', category: 'Internal', core: false, internal: true, description: 'Maintainer tool: scaffold, edit, and lint specialist prompts', usage: 'construct specialist <create|edit|lint>' },
1026
+ { name: 'registry:status', category: 'Internal', core: false, internal: true, description: 'Dev: capability-matrix registry inspector', usage: 'construct registry:status' },
939
1027
  { name: 'evaluator:rubrics', category: 'Internal', core: false, internal: true, description: 'Internal: list registered evaluator rubrics', usage: 'construct evaluator:rubrics' },
940
1028
  { name: 'activation:status', category: 'Internal', core: false, internal: true, description: 'Internal: agent activation telemetry', usage: 'construct activation:status' },
941
1029
  { name: 'prune', category: 'Internal', core: false, internal: true, description: 'Internal: prune ephemeral storage entries', usage: 'construct prune' },
@@ -1031,5 +1119,28 @@ export function formatCommandHelp(name, { colors = false } = {}) {
1031
1119
  }
1032
1120
  lines.push('');
1033
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('');
1034
1145
  return lines.join('\n');
1035
1146
  }
@@ -86,7 +86,7 @@ function isConstructSelfRepo(rootDir) {
86
86
 
87
87
  function requiresHeader(rel) {
88
88
  const ext = path.extname(rel);
89
- if (['.yaml', '.yml', '.json', '.jsonl', '.toml'].includes(ext)) return { required: false, type: null };
89
+ if (['.yaml', '.yml', '.json', '.jsonl', '.toml', '.txt'].includes(ext)) return { required: false, type: null };
90
90
  const jsMatch = JS_HEADER_GLOBS.some(r => r.test(rel));
91
91
  const mdMatch = MD_HEADER_GLOBS.some(r => r.test(rel));
92
92
  // Exclude static assets under lib/server/static from requiring headers