@geraldmaron/construct 1.0.21 → 1.0.24

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 (150) hide show
  1. package/README.md +23 -7
  2. package/bin/construct +206 -228
  3. package/bin/construct-postinstall.mjs +25 -15
  4. package/lib/acp/server.mjs +113 -0
  5. package/lib/agent-instructions/inject.mjs +94 -0
  6. package/lib/auto-docs.mjs +10 -2
  7. package/lib/cli-commands.mjs +43 -15
  8. package/lib/comment-lint.mjs +115 -0
  9. package/lib/completions.mjs +7 -1
  10. package/lib/config/schema.mjs +4 -0
  11. package/lib/decisions/enforced-baseline.json +2 -0
  12. package/lib/docs-verify.mjs +15 -18
  13. package/lib/doctor/cli.mjs +8 -1
  14. package/lib/document-export.mjs +124 -0
  15. package/lib/embed/daemon.mjs +1 -1
  16. package/lib/embed/inbox.mjs +31 -10
  17. package/lib/embed/recommendation-store.mjs +7 -289
  18. package/lib/embed/reconcile.mjs +2 -2
  19. package/lib/features.mjs +11 -11
  20. package/lib/git-hooks-path.mjs +61 -0
  21. package/lib/home-namespace.mjs +60 -0
  22. package/lib/hooks/ci-status-check.mjs +62 -40
  23. package/lib/hooks/config-protection.mjs +4 -4
  24. package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
  25. package/lib/hooks/pre-push-gate.mjs +15 -6
  26. package/lib/hooks/session-reflect.mjs +5 -1
  27. package/lib/hooks/stop-notify.mjs +32 -17
  28. package/lib/hooks/stop-typecheck.mjs +7 -2
  29. package/lib/host-capabilities.mjs +24 -8
  30. package/lib/host-disposition.mjs +76 -0
  31. package/lib/ingest/provider-extract.mjs +1 -1
  32. package/lib/init-docs.mjs +1 -1
  33. package/lib/init-unified.mjs +320 -219
  34. package/lib/init-update.mjs +4 -84
  35. package/lib/init.mjs +9 -25
  36. package/lib/install/stage-project.mjs +8 -2
  37. package/lib/intake/git-queue.mjs +195 -0
  38. package/lib/intake/queue.mjs +9 -16
  39. package/lib/intent-classifier.mjs +1 -1
  40. package/lib/knowledge/search.mjs +52 -3
  41. package/lib/mcp/server.mjs +57 -14
  42. package/lib/mcp/tools/memory.mjs +2 -2
  43. package/lib/mcp/tools/orchestration-run.mjs +125 -0
  44. package/lib/mcp/tools/storage.mjs +2 -3
  45. package/lib/mcp-catalog.json +3 -3
  46. package/lib/mcp-manager.mjs +59 -3
  47. package/lib/model-registry.mjs +40 -33
  48. package/lib/observation-store.mjs +39 -163
  49. package/lib/opencode-config.mjs +1 -1
  50. package/lib/orchestration/events.mjs +66 -0
  51. package/lib/orchestration/runtime.mjs +72 -10
  52. package/lib/orchestration/worker.mjs +69 -21
  53. package/lib/orchestration-policy.mjs +27 -3
  54. package/lib/parity.mjs +46 -24
  55. package/lib/policy/unified-gates.mjs +96 -0
  56. package/lib/project-init-shared.mjs +0 -173
  57. package/lib/reconcile/adapter-prune.mjs +105 -0
  58. package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
  59. package/lib/reconcile/gitignore-coverage.mjs +88 -0
  60. package/lib/reconcile/index.mjs +169 -0
  61. package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
  62. package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
  63. package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
  64. package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
  65. package/lib/runtime/uv-bootstrap.mjs +27 -3
  66. package/lib/schema-infer.mjs +16 -2
  67. package/lib/server/csrf.mjs +14 -2
  68. package/lib/server/index.mjs +95 -0
  69. package/lib/service-manager.mjs +59 -252
  70. package/lib/setup-prompts.mjs +2 -1
  71. package/lib/setup.mjs +104 -480
  72. package/lib/status.mjs +3 -6
  73. package/lib/storage/admin.mjs +48 -325
  74. package/lib/storage/backend.mjs +10 -43
  75. package/lib/storage/hybrid-query.mjs +15 -196
  76. package/lib/storage/sync.mjs +36 -177
  77. package/lib/storage/vector-client.mjs +256 -235
  78. package/lib/strategy-store.mjs +35 -286
  79. package/lib/template-registry.mjs +73 -0
  80. package/lib/term-format.mjs +75 -0
  81. package/lib/uninstall/uninstall.mjs +180 -7
  82. package/lib/worker/entrypoint.mjs +6 -14
  83. package/package.json +7 -6
  84. package/personas/construct.md +7 -8
  85. package/platforms/claude/settings.template.json +30 -11
  86. package/platforms/opencode/config.template.json +2 -2
  87. package/rules/common/neurodivergent-output.md +66 -0
  88. package/rules/common/tool-invisibility.md +37 -0
  89. package/scripts/sync-specialists.mjs +427 -107
  90. package/skills/operating/orchestration-reference.md +2 -16
  91. package/specialists/policy-inventory.json +14 -0
  92. package/specialists/prompts/cx-accessibility.md +2 -6
  93. package/specialists/prompts/cx-ai-engineer.md +0 -4
  94. package/specialists/prompts/cx-architect.md +3 -5
  95. package/specialists/prompts/cx-business-strategist.md +0 -5
  96. package/specialists/prompts/cx-data-analyst.md +0 -4
  97. package/specialists/prompts/cx-data-engineer.md +0 -4
  98. package/specialists/prompts/cx-debugger.md +2 -6
  99. package/specialists/prompts/cx-designer.md +0 -8
  100. package/specialists/prompts/cx-devil-advocate.md +2 -2
  101. package/specialists/prompts/cx-docs-keeper.md +0 -13
  102. package/specialists/prompts/cx-engineer.md +0 -13
  103. package/specialists/prompts/cx-evaluator.md +2 -2
  104. package/specialists/prompts/cx-explorer.md +4 -5
  105. package/specialists/prompts/cx-legal-compliance.md +4 -5
  106. package/specialists/prompts/cx-operations.md +0 -5
  107. package/specialists/prompts/cx-orchestrator.md +0 -4
  108. package/specialists/prompts/cx-platform-engineer.md +0 -8
  109. package/specialists/prompts/cx-product-manager.md +0 -8
  110. package/specialists/prompts/cx-qa.md +3 -12
  111. package/specialists/prompts/cx-rd-lead.md +0 -5
  112. package/specialists/prompts/cx-release-manager.md +0 -8
  113. package/specialists/prompts/cx-researcher.md +5 -29
  114. package/specialists/prompts/cx-reviewer.md +2 -6
  115. package/specialists/prompts/cx-security.md +2 -11
  116. package/specialists/prompts/cx-sre.md +0 -15
  117. package/specialists/prompts/cx-test-automation.md +0 -4
  118. package/specialists/prompts/cx-trace-reviewer.md +2 -2
  119. package/specialists/prompts/cx-ux-researcher.md +0 -4
  120. package/specialists/registry.json +28 -36
  121. package/templates/distribution/run.mjs +36 -7
  122. package/templates/docs/accessibility-audit.md +56 -0
  123. package/templates/docs/architecture-review.md +59 -0
  124. package/templates/docs/code-review-report.md +46 -0
  125. package/templates/docs/construct_guide.md +15 -15
  126. package/templates/docs/debug-investigation.md +53 -0
  127. package/templates/docs/qa-report.md +48 -0
  128. package/templates/docs/security-audit-report.md +48 -0
  129. package/templates/docs/task-packet.md +49 -0
  130. package/templates/docs/verdict.md +40 -0
  131. package/db/schema/001_init.sql +0 -40
  132. package/db/schema/002_pgvector.sql +0 -182
  133. package/db/schema/003_intake.sql +0 -47
  134. package/db/schema/003_observation_reconciliation.sql +0 -14
  135. package/db/schema/004_recommendations.sql +0 -46
  136. package/db/schema/005_strategy.sql +0 -21
  137. package/db/schema/006_graph.sql +0 -24
  138. package/db/schema/007_tags.sql +0 -30
  139. package/db/schema/008_skill_usage.sql +0 -24
  140. package/db/schema/009_scheduler.sql +0 -14
  141. package/db/schema/010_cx_scores.sql +0 -51
  142. package/lib/intake/postgres-queue.mjs +0 -240
  143. package/lib/server/static/index.html +0 -1
  144. package/lib/services/local-postgres.mjs +0 -15
  145. package/lib/storage/backup.mjs +0 -347
  146. package/lib/storage/migrations.mjs +0 -187
  147. package/lib/storage/postgres-backup.mjs +0 -124
  148. package/lib/storage/sql-store.mjs +0 -45
  149. package/lib/storage/store-version.mjs +0 -115
  150. package/lib/storage/vector-store.mjs +0 -100
@@ -14,11 +14,18 @@
14
14
  * - auto: removed when --yes is passed; included in the interactive list pre-checked
15
15
  * - ask : never auto-removed; interactive list shows them unchecked
16
16
  *
17
+ * Per ADR-0027 §Consequences, shared Docker images stay by default because
18
+ * other projects may use the same pgvector base; the pgvector image is removed
19
+ * only under --with-images. The LaunchAgent registration and a Construct-set
20
+ * `core.hooksPath` are reversed because they are state install/init created and
21
+ * own; a user-customized hooksPath is left untouched.
22
+ *
17
23
  * Flags:
18
24
  * --dry-run print the plan, change nothing
19
25
  * --yes non-interactive; remove auto-risk items, skip ask-risk items
20
26
  * --yes --all non-interactive; remove both auto- and ask-risk items
21
27
  * --keep-state only remove project-scope launcher + adapters; preserve .cx/, ~/.construct, Postgres
28
+ * --with-images also remove the shared pgvector Docker image (off by default; ADR-0027)
22
29
  * --scope=project|machine|all limit which categories are evaluated (default: all)
23
30
  */
24
31
 
@@ -30,11 +37,15 @@ import { fileURLToPath } from 'node:url';
30
37
 
31
38
  import { multiSelect } from '../tty-prompts.mjs';
32
39
  import { getUserConfigDir, getUserEnvPath } from '../env-config.mjs';
40
+ import { removeTomlTables, tomlString } from '../codex-config.mjs';
33
41
 
34
42
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
35
43
  const PKG_ROOT = path.resolve(__dirname, '..', '..');
36
44
 
37
45
  const POSTGRES_CONTAINER = 'construct-postgres';
46
+ const PGVECTOR_IMAGE = 'pgvector/pgvector:pg16';
47
+ const PRESSURE_GUARD_LABEL = 'dev.construct.pressure-release';
48
+ const MEMORY_MCP_KEYS = ['memory', 'cass'];
38
49
 
39
50
  export function parseArgs(argv) {
40
51
  const args = {
@@ -42,6 +53,7 @@ export function parseArgs(argv) {
42
53
  yes: false,
43
54
  all: false,
44
55
  keepState: false,
56
+ withImages: false,
45
57
  scope: 'all',
46
58
  home: os.homedir(),
47
59
  cwd: process.cwd(),
@@ -51,6 +63,7 @@ export function parseArgs(argv) {
51
63
  else if (arg === '--yes' || arg === '-y') args.yes = true;
52
64
  else if (arg === '--all') args.all = true;
53
65
  else if (arg === '--keep-state') args.keepState = true;
66
+ else if (arg === '--with-images') args.withImages = true;
54
67
  else if (arg.startsWith('--scope=')) args.scope = arg.slice('--scope='.length);
55
68
  else if (arg.startsWith('--home=')) args.home = arg.slice('--home='.length);
56
69
  else if (arg.startsWith('--cwd=')) args.cwd = arg.slice('--cwd='.length);
@@ -61,7 +74,7 @@ export function parseArgs(argv) {
61
74
  return args;
62
75
  }
63
76
 
64
- function buildCategories({ cwd, home }) {
77
+ function buildCategories({ cwd, home, withImages = false }) {
65
78
  const dotConstruct = path.join(cwd, '.construct');
66
79
  const dotClaudeAgents = path.join(cwd, '.claude', 'agents');
67
80
  const dotClaudeCommands = path.join(cwd, '.claude', 'commands');
@@ -77,6 +90,11 @@ function buildCategories({ cwd, home }) {
77
90
  const userWorkspace = path.join(userConstructDir, 'workspace');
78
91
  const userPgComposeDir = path.join(userConstructDir, 'services', 'postgres');
79
92
  const userCxDir = path.join(userHome, '.cx');
93
+ const launchAgentPlist = path.join(userHome, 'Library', 'LaunchAgents', `${PRESSURE_GUARD_LABEL}.plist`);
94
+ const claudeSettings = path.join(userHome, '.claude', 'settings.json');
95
+ const opencodeConfig = path.join(userHome, '.config', 'opencode', 'opencode.json');
96
+ const codexConfig = path.join(userHome, '.codex', 'config.toml');
97
+ const memoryConfigPaths = { claudeSettings, opencodeConfig, codexConfig };
80
98
 
81
99
  return [
82
100
  {
@@ -209,12 +227,49 @@ function buildCategories({ cwd, home }) {
209
227
  describe: () => 'Removes the local docker-compose.yml. Run after removing the container.',
210
228
  execute: () => removePath(userPgComposeDir),
211
229
  },
230
+ {
231
+ id: 'project-git-hookspath',
232
+ scope: 'project',
233
+ risk: 'auto',
234
+ label: 'git core.hooksPath (.beads/hooks)',
235
+ detect: () => gitHooksPathIsConstruct(cwd),
236
+ describe: () => 'Unsets core.hooksPath set by `construct init` to .beads/hooks. A custom hooksPath is left untouched.',
237
+ execute: () => unsetConstructGitHooksPath(cwd),
238
+ },
239
+ {
240
+ id: 'machine-launchagent',
241
+ scope: 'machine',
242
+ risk: 'auto',
243
+ label: `LaunchAgent ${PRESSURE_GUARD_LABEL} (unload + plist)`,
244
+ detect: () => process.platform === 'darwin' && existsAny(launchAgentPlist),
245
+ describe: () => `Unregisters the pressure-release LaunchAgent (launchctl bootout) and removes ${rel(userHome, launchAgentPlist)}.`,
246
+ execute: () => removePressureGuardLaunchAgent(launchAgentPlist),
247
+ },
248
+ {
249
+ id: 'machine-memory-mcp',
250
+ scope: 'machine',
251
+ risk: 'auto',
252
+ label: 'memory MCP registration (Claude, OpenCode, Codex)',
253
+ detect: () => memoryMcpRegistered(memoryConfigPaths),
254
+ describe: () => 'Strips the Construct memory MCP bridge from ~/.claude/settings.json, ~/.config/opencode/opencode.json, and ~/.codex/config.toml. Preserves all other entries.',
255
+ execute: () => removeMemoryMcp(memoryConfigPaths),
256
+ },
257
+ {
258
+ id: 'machine-pgvector-image',
259
+ scope: 'machine',
260
+ risk: 'ask',
261
+ optIn: true,
262
+ label: `Docker image "${PGVECTOR_IMAGE}" (--with-images only)`,
263
+ detect: () => withImages && dockerImageExists(PGVECTOR_IMAGE),
264
+ describe: () => `Removes the cached ${PGVECTOR_IMAGE} image. Off unless --with-images, since other projects may share it (ADR-0027).`,
265
+ execute: () => removeDockerImage(PGVECTOR_IMAGE),
266
+ },
212
267
  ];
213
268
  }
214
269
 
215
270
  export async function runUninstall(rawArgs = []) {
216
271
  const args = parseArgs(rawArgs);
217
- const categories = buildCategories({ cwd: args.cwd, home: args.home });
272
+ const categories = buildCategories({ cwd: args.cwd, home: args.home, withImages: args.withImages });
218
273
 
219
274
  const inScope = categories.filter((cat) => {
220
275
  if (args.scope !== 'all' && cat.scope !== args.scope) return false;
@@ -235,7 +290,7 @@ export async function runUninstall(rawArgs = []) {
235
290
  }
236
291
 
237
292
  if (args.yes) {
238
- toRemove = inScope.filter((cat) => cat.risk === 'auto' || args.all).map((cat) => cat.id);
293
+ toRemove = inScope.filter((cat) => cat.risk === 'auto' || cat.optIn || args.all).map((cat) => cat.id);
239
294
  } else if (!process.stdin.isTTY) {
240
295
  console.error('[uninstall] No TTY available. Re-run with --yes (auto-risk only) or --yes --all (everything).');
241
296
  process.exit(2);
@@ -275,9 +330,9 @@ async function promptCategories(categories) {
275
330
  const options = categories.map((cat) => ({
276
331
  label: cat.label,
277
332
  value: cat.id,
278
- checked: cat.risk === 'auto',
333
+ checked: cat.risk === 'auto' || Boolean(cat.optIn),
279
334
  description: cat.describe(),
280
- meta: cat.risk === 'auto' ? 'pre-checked' : 'ask',
335
+ meta: cat.risk === 'auto' || cat.optIn ? 'pre-checked' : 'ask',
281
336
  }));
282
337
  try {
283
338
  return await multiSelect({
@@ -294,7 +349,7 @@ async function promptCategories(categories) {
294
349
  function printPlan(categories, args) {
295
350
  console.log(`[uninstall] dry-run plan (scope=${args.scope}${args.keepState ? ', keep-state' : ''}):`);
296
351
  for (const cat of categories) {
297
- const mark = cat.risk === 'auto' ? '✓' : '◐';
352
+ const mark = cat.risk === 'auto' || cat.optIn ? '✓' : '◐';
298
353
  console.log(` ${mark} ${cat.label}`);
299
354
  console.log(` ${cat.describe()}`);
300
355
  }
@@ -311,7 +366,7 @@ function printFollowups() {
311
366
  console.log('');
312
367
  console.log('Follow-ups (run by hand if you want):');
313
368
  console.log(' npm uninstall @geraldmaron/construct # drop the package itself');
314
- console.log(' docker rmi pgvector/pgvector:pg16 # remove the cached pgvector image');
369
+ console.log(' construct uninstall --with-images # also drop the shared pgvector image (kept by default)');
315
370
  console.log(' brew uninstall cm cass # if installed by `construct init`');
316
371
  }
317
372
 
@@ -421,3 +476,121 @@ function removeDockerContainer(name) {
421
476
  }
422
477
  return stop.status === 0 ? 'stopped and removed (with volume)' : 'removed (with volume)';
423
478
  }
479
+
480
+ function dockerImageExists(image) {
481
+ const probe = spawnSync('docker', ['image', 'inspect', image], {
482
+ encoding: 'utf8',
483
+ stdio: ['ignore', 'ignore', 'ignore'],
484
+ });
485
+ return probe.status === 0;
486
+ }
487
+
488
+ function removeDockerImage(image) {
489
+ const rm = spawnSync('docker', ['rmi', image], { encoding: 'utf8' });
490
+ if (rm.status !== 0) {
491
+ throw new Error((rm.stderr || '').trim() || 'docker rmi failed');
492
+ }
493
+ return `removed image ${image}`;
494
+ }
495
+
496
+ // core.hooksPath is reversed only when it still points at the value install/init
497
+ // set (.beads/hooks); a user who repointed it owns that choice and is left alone.
498
+
499
+ function gitHooksPathIsConstruct(cwd) {
500
+ const inGit = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
501
+ if (inGit.status !== 0) return false;
502
+ const current = spawnSync('git', ['config', '--get', 'core.hooksPath'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
503
+ if (current.status !== 0) return false;
504
+ return (current.stdout || '').trim() === '.beads/hooks';
505
+ }
506
+
507
+ function unsetConstructGitHooksPath(cwd) {
508
+ if (!gitHooksPathIsConstruct(cwd)) return 'core.hooksPath not set to .beads/hooks';
509
+ const result = spawnSync('git', ['config', '--unset', 'core.hooksPath'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
510
+ if (result.status !== 0) {
511
+ throw new Error((result.stderr || '').trim() || 'git config --unset failed');
512
+ }
513
+ return 'unset core.hooksPath';
514
+ }
515
+
516
+ // launchctl bootout takes the GUI domain target; unload is the legacy fallback
517
+ // for older macOS. Either deregisters the agent before the plist is deleted so a
518
+ // reinstall starts clean.
519
+
520
+ function removePressureGuardLaunchAgent(plistPath) {
521
+ if (!existsAny(plistPath)) return 'no LaunchAgent plist';
522
+ const uid = typeof process.getuid === 'function' ? process.getuid() : null;
523
+ if (process.platform === 'darwin') {
524
+ if (uid != null) {
525
+ spawnSync('launchctl', ['bootout', `gui/${uid}/${PRESSURE_GUARD_LABEL}`], { stdio: ['ignore', 'ignore', 'ignore'] });
526
+ }
527
+ spawnSync('launchctl', ['unload', plistPath], { stdio: ['ignore', 'ignore', 'ignore'] });
528
+ }
529
+ const removed = removePath(plistPath);
530
+ return removed ? 'unregistered and removed plist' : 'unregistered (plist already gone)';
531
+ }
532
+
533
+ function memoryMcpRegistered({ claudeSettings, opencodeConfig, codexConfig }) {
534
+ return claudeHasMemoryMcp(claudeSettings)
535
+ || opencodeHasMemoryMcp(opencodeConfig)
536
+ || codexHasMemoryMcp(codexConfig);
537
+ }
538
+
539
+ function claudeHasMemoryMcp(filePath) {
540
+ const settings = readJsonOrNull(filePath);
541
+ if (!settings || !settings.mcpServers) return false;
542
+ return MEMORY_MCP_KEYS.some((key) => key in settings.mcpServers);
543
+ }
544
+
545
+ function opencodeHasMemoryMcp(filePath) {
546
+ const config = readJsonOrNull(filePath);
547
+ if (!config || !config.mcp) return false;
548
+ return MEMORY_MCP_KEYS.some((key) => key in config.mcp);
549
+ }
550
+
551
+ function codexHasMemoryMcp(filePath) {
552
+ if (!fs.existsSync(filePath)) return false;
553
+ const text = fs.readFileSync(filePath, 'utf8');
554
+ return MEMORY_MCP_KEYS.some((key) => text.includes(`[mcp_servers.${key}]`) || text.includes(`[mcp_servers.${tomlString(key)}]`));
555
+ }
556
+
557
+ // The memory MCP is registered into all three host configs by `construct mcp add
558
+ // memory` during install; each host is stripped independently so a missing or
559
+ // hand-edited config never blocks the others.
560
+
561
+ function removeMemoryMcp({ claudeSettings, opencodeConfig, codexConfig }) {
562
+ const stripped = [];
563
+ if (stripMemoryFromJsonMcp(claudeSettings, 'mcpServers')) stripped.push('claude');
564
+ if (stripMemoryFromJsonMcp(opencodeConfig, 'mcp')) stripped.push('opencode');
565
+ if (stripMemoryFromCodex(codexConfig)) stripped.push('codex');
566
+ return stripped.length ? `stripped from ${stripped.join(', ')}` : 'nothing to strip';
567
+ }
568
+
569
+ function stripMemoryFromJsonMcp(filePath, mcpKey) {
570
+ const config = readJsonOrNull(filePath);
571
+ if (!config || !config[mcpKey]) return false;
572
+ let changed = false;
573
+ for (const key of MEMORY_MCP_KEYS) {
574
+ if (key in config[mcpKey]) { delete config[mcpKey][key]; changed = true; }
575
+ }
576
+ if (!changed) return false;
577
+ if (Object.keys(config[mcpKey]).length === 0) delete config[mcpKey];
578
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
579
+ return true;
580
+ }
581
+
582
+ function stripMemoryFromCodex(filePath) {
583
+ if (!fs.existsSync(filePath)) return false;
584
+ const existing = fs.readFileSync(filePath, 'utf8');
585
+ const tables = MEMORY_MCP_KEYS.flatMap((key) => [`mcp_servers.${key}`, `mcp_servers.${tomlString(key)}`]);
586
+ const cleaned = removeTomlTables(existing, tables);
587
+ if (cleaned === existing.trimEnd()) return false;
588
+ fs.writeFileSync(filePath, `${cleaned.trimEnd()}\n`, 'utf8');
589
+ return true;
590
+ }
591
+
592
+ function readJsonOrNull(filePath) {
593
+ if (!fs.existsSync(filePath)) return null;
594
+ try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
595
+ catch { return null; }
596
+ }
@@ -24,8 +24,7 @@
24
24
 
25
25
  import path from 'node:path';
26
26
 
27
- import { createSqlClient, closeSqlClient } from '../storage/backend.mjs';
28
- import { PostgresIntakeQueue } from '../intake/postgres-queue.mjs';
27
+ import { createIntakeQueue } from '../intake/queue.mjs';
29
28
  import { FilesystemTaskGraphStore } from '../task-graph/store.mjs';
30
29
  import { runJob } from './run.mjs';
31
30
  import { evidenceFromJobResult, recordEvidence, blockedPacket } from './evidence.mjs';
@@ -167,7 +166,6 @@ export async function runWorkerLoop({
167
166
  rootDir,
168
167
  workspace,
169
168
  project,
170
- sql,
171
169
  queue: injectedQueue = null,
172
170
  store: injectedStore = null,
173
171
  pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
@@ -178,10 +176,9 @@ export async function runWorkerLoop({
178
176
  } = {}) {
179
177
  if (!project) throw new Error('runWorkerLoop: project is required');
180
178
  if (!workspace) throw new Error('runWorkerLoop: workspace is required');
181
- if (!injectedQueue && !sql) throw new Error('runWorkerLoop: sql client (or injected queue) is required');
182
179
 
183
180
  const workerName = readWorkerName();
184
- const queue = injectedQueue || new PostgresIntakeQueue({ sql, project });
181
+ const queue = injectedQueue || createIntakeQueue(rootDir, process.env, { project });
185
182
  const store = injectedStore || new FilesystemTaskGraphStore(rootDir);
186
183
 
187
184
  let processed = 0;
@@ -223,19 +220,14 @@ if (invokedDirectly) {
223
220
  const pollIntervalMs = Number(args['poll-interval-ms'] || DEFAULT_POLL_INTERVAL_MS);
224
221
  const idleTimeoutSeconds = Number(args['idle-timeout-seconds'] || DEFAULT_IDLE_TIMEOUT_SECONDS);
225
222
 
226
- const sql = createSqlClient(process.env);
227
- if (!sql) {
228
- process.stderr.write('[worker] DATABASE_URL is required for the worker entrypoint.\n');
229
- process.exit(78); // EX_CONFIG
230
- }
231
-
232
223
  try {
233
224
  const summary = await runWorkerLoop({
234
- rootDir, workspace, project, sql, pollIntervalMs, idleTimeoutSeconds,
225
+ rootDir, workspace, project, pollIntervalMs, idleTimeoutSeconds,
235
226
  });
236
227
  process.stdout.write(`[worker] exiting after idle timeout — processed=${summary.processed} skipped=${summary.skipped} worker=${summary.workerName}\n`);
237
228
  process.exit(0);
238
- } finally {
239
- await closeSqlClient(sql);
229
+ } catch (err) {
230
+ process.stderr.write(`[worker] failed: ${err.stack}\n`);
231
+ process.exit(1);
240
232
  }
241
233
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geraldmaron/construct",
3
- "version": "1.0.21",
3
+ "version": "1.0.24",
4
4
  "type": "module",
5
5
  "packageManager": "npm@11.5.1",
6
6
  "description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
@@ -73,23 +73,24 @@
73
73
  "release:check": "node ./bin/construct doctor && npm test && node ./bin/construct docs:update --check && node ./bin/construct dashboard:sync --check && node ./bin/construct lint:comments && node scripts/lint-commits-pr.mjs",
74
74
  "release:gate": "node --test tests/functional/release-gate.functional.test.mjs",
75
75
  "audit:published": "node scripts/audit-published-artifact.mjs",
76
+ "version": "node scripts/sync-construct-version.mjs && git add .construct/version",
76
77
  "release:preflight": "node scripts/pre-release-check.mjs",
77
78
  "release:preflight:no-auth": "node scripts/pre-release-check.mjs --skip-auth",
78
79
  "npm:publish": "npm run release:check && npm publish --access public",
79
80
  "postinstall": "node ./bin/construct-postinstall.mjs"
80
81
  },
81
82
  "dependencies": {
83
+ "@lancedb/lancedb": "^0.30.0",
82
84
  "@modelcontextprotocol/sdk": "^1.12.0",
83
- "js-yaml": "^4.2.0",
84
- "node-webvtt": "^1.9.3",
85
- "postgres": "^3.4.9"
85
+ "apache-arrow": "^18.1.0",
86
+ "js-yaml": "^4.2.0"
86
87
  },
87
88
  "optionalDependencies": {
88
89
  "@huggingface/transformers": "^4.2.0",
89
90
  "@opentelemetry/api": "^1.9.0",
90
91
  "@opentelemetry/core": "^2.7.1",
91
92
  "@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
92
- "@opentelemetry/resources": "^1.25.0",
93
+ "@opentelemetry/resources": "^2.7.1",
93
94
  "@opentelemetry/sdk-trace-node": "^1.25.0",
94
95
  "@opentelemetry/semantic-conventions": "^1.25.0"
95
96
  },
@@ -99,7 +100,7 @@
99
100
  "devDependencies": {
100
101
  "@eslint/js": "^9.39.4",
101
102
  "c8": "^11.0.0",
102
- "eslint": "^9.39.4",
103
+ "eslint": "^10.4.1",
103
104
  "globals": "^17.6.0"
104
105
  }
105
106
  }
@@ -25,20 +25,15 @@ Use the single-writer rule whenever multiple sessions are active: if two session
25
25
 
26
26
  ## Classify before acting
27
27
 
28
- Use the code-backed orchestration policy for intent, execution track, specialist selection, escalation, and approval boundaries. Visual deliverables (wireframes, diagrams, decks) are first-class. use real visual tools, not bullet prose.
28
+ Before any non-trivial request, CALL the code-backed orchestration policy via the `orchestration_policy` MCP tool with the request text and your `fileCount` / `moduleCount` / `introducesContract` estimate. Do not classify from memory. Honor the returned `track` and `specialists`. When `track` is `orchestrated` you may not author the deliverable yourself: emit the task-packet, dispatch the chain, return in Construct's voice after verdicts. Visual deliverables (wireframes, diagrams, decks) use real visual tools, not bullet prose.
29
29
 
30
- Execution model:
31
- - **Immediate**. answer or act directly when no hidden worker is needed.
32
- - **Focused**. dispatch one bounded specialist path; return in Construct's voice.
33
- - **Orchestrated**. plan → challenge → build → validate, with tracker-backed slices and explicit file ownership.
34
-
35
- Devil's advocate is mandatory for new architectural directions, AI/agent workflow changes, security or data-integrity changes, and promoting a temporary capability to persistent.
30
+ Tracks: immediate (act directly), focused (one bounded specialist), orchestrated (plan → challenge → build → validate, tracker-backed). cx-devil-advocate is mandatory whenever `riskFlags` include architecture, security, dataIntegrity, or ai.
36
31
 
37
32
  Orchestrated dispatches emit a task-packet with `goal`, `intent`, `workCategory`, `riskFlags`, `acceptanceCriteria` before naming specialists (`specialists/contracts.json:construct-to-orchestrator`).
38
33
 
39
34
  ## Gates and contracts (org-in-a-box)
40
35
 
41
- `routeRequest` returns three artifacts; honor all three:
36
+ `orchestration_policy` returns three artifacts; honor all three:
42
37
 
43
38
  1. **Gates**. `framingChallenge`, `externalResearch`, `docAuthoring`
44
39
  2. **Contract chain**. typed handoffs from `specialists/contracts.json`. Call `agent_contract` MCP tool at handoff.
@@ -67,6 +62,10 @@ The active profile (`construct profile show`) sets the intake taxonomy. Session-
67
62
 
68
63
  Lead with the answer. One question when blocked. Confirm what changed when done.
69
64
 
65
+ **Output style**: format human-facing output (terminal, prose, dashboard) for neurodivergent readers — answer first, clear hierarchy, scannable chunks, plain language, explicit next step; never rely on color or motion alone; honor `NO_COLOR` and reduced-motion. Presentation only — never reshape machine-readable output (`--json`, parsed tokens, registries, contracts). See `rules/common/neurodivergent-output.md`.
66
+
67
+ **Tool invisibility**: deliverables are about the user's project, never Construct. Never name Construct, `cx-*` role ids, or internal orchestration mechanics in artifact content unless the subject project is Construct itself. Provenance goes in a comment, not the prose. See `rules/common/tool-invisibility.md`.
68
+
70
69
  Non-trivial work: update Beads (`bd note <id>`), `plan.md`, docs with owner / acceptance / verification. Preserve tracker ids in handoffs. Surface NEEDS_MAIN_INPUT in your voice; resume after the answer. End every session with a handoff at `.cx/handoffs/{date}-{slug}.md` and updates to `.cx/context.md`.
71
70
 
72
71
  Load-bearing state: `AGENTS.md`, `.cx/context.md`/`.json`, `docs/README.md`, `docs/architecture.md` (read at session start, update before DONE, prune stale sections). `plan.md` is local-only.
@@ -30,6 +30,18 @@
30
30
  "id": "pre:policy-engine",
31
31
  "description": "Consolidated policy gating (bootstrap rule): block mutations until session is grounded via project_context+memory_search or 3+ exploratory reads"
32
32
  },
33
+ {
34
+ "matcher": "Write|Edit|MultiEdit",
35
+ "hooks": [
36
+ {
37
+ "type": "command",
38
+ "command": "node \"$HOME/.construct/lib/hooks/orchestration-dispatch-guard.mjs\"",
39
+ "timeout": 5
40
+ }
41
+ ],
42
+ "id": "pre:edit:dispatch-guard",
43
+ "description": "Block solo-authoring a substantial deliverable when orchestration_policy classified the request as orchestrated and no specialist dispatch has run yet"
44
+ },
33
45
  {
34
46
  "matcher": "Bash",
35
47
  "hooks": [
@@ -304,6 +316,18 @@
304
316
  ],
305
317
  "id": "post:audit-trail",
306
318
  "description": "Append-only audit log of every mutation — attribution (agent/task) and tamper-evidence chain"
319
+ },
320
+ {
321
+ "matcher": "mcp__.*__orchestration_policy|mcp__.*__orchestration_run|mcp__.*__worker_run|Task",
322
+ "hooks": [
323
+ {
324
+ "type": "command",
325
+ "command": "node \"$HOME/.construct/lib/hooks/orchestration-dispatch-guard.mjs\"",
326
+ "timeout": 5
327
+ }
328
+ ],
329
+ "id": "post:orchestration:dispatch-guard",
330
+ "description": "Record orchestration_policy verdict and specialist dispatch so the PreToolUse guard can tell an orchestrated-but-undispatched deliverable write from legitimate post-dispatch synthesis"
307
331
  }
308
332
  ],
309
333
  "PostToolUseFailure": [
@@ -392,11 +416,12 @@
392
416
  {
393
417
  "type": "command",
394
418
  "command": "node \"$HOME/.construct/lib/hooks/stop-typecheck.mjs\"",
395
- "timeout": 120
419
+ "timeout": 120,
420
+ "async": true
396
421
  }
397
422
  ],
398
423
  "id": "stop:typecheck",
399
- "description": "Batch TypeScript typecheck of edited files at end of response"
424
+ "description": "Batch TypeScript typecheck of edited files at end of response (async: writes ~/.cx/pending-typecheck.txt for the next session-start to surface; does not gate the current turn)"
400
425
  },
401
426
  {
402
427
  "matcher": "*",
@@ -430,11 +455,12 @@
430
455
  {
431
456
  "type": "command",
432
457
  "command": "node \"$HOME/.construct/lib/hooks/pre-compact.mjs\"",
433
- "timeout": 15
458
+ "timeout": 15,
459
+ "async": true
434
460
  }
435
461
  ],
436
462
  "id": "stop:snapshot",
437
- "description": "Snapshot .cx/context.md at session end so resume has real progress"
463
+ "description": "Snapshot .cx/context.md at session end so resume has real progress (async: written for the NEXT session to read; does not gate the current turn)"
438
464
  },
439
465
  {
440
466
  "matcher": "*",
@@ -521,13 +547,6 @@
521
547
  "memory": {
522
548
  "type": "http",
523
549
  "url": "http://127.0.0.1:8765/"
524
- },
525
- "playwright": {
526
- "command": "npx",
527
- "args": [
528
- "-y",
529
- "@playwright/mcp@latest"
530
- ]
531
550
  }
532
551
  }
533
552
  }
@@ -20,8 +20,8 @@
20
20
  "options": {
21
21
  "baseURL": "https://openrouter.ai/api/v1",
22
22
  "headers": {
23
- "HTTP-Referer": "__OPENROUTER_REFERER__",
24
- "X-Title": "__OPENROUTER_TITLE__",
23
+ "HTTP-Referer": "https://github.com/geraldmaron/construct",
24
+ "X-Title": "Construct",
25
25
  "Authorization": "Bearer __OPENROUTER_API_KEY__"
26
26
  }
27
27
  },
@@ -0,0 +1,66 @@
1
+ ---
2
+ description: every human-facing Construct output is formatted for neurodivergent readers — without reshaping machine-readable output.
3
+ enforced_by: (persona prompt), tests/term-format.test.mjs, construct dashboard:sync --check
4
+ precedence_tier: style
5
+ ---
6
+ # Neurodivergent-Friendly Output
7
+
8
+ Construct's output is read under load — in a terminal, mid-task, often by people who process text differently. Dense walls of prose, buried conclusions, inconsistent structure, and meaning carried only by color cost every reader attention, and cost neurodivergent readers more. This rule applies to **every human-facing output** Construct produces: terminal output, agent prose to the user, reports, error messages, and the dashboard.
9
+
10
+ It is a `style` rule by design. The precedence resolver (`rules/common/precedence.md`) guarantees a style rule never overrides a higher tier — so when accessible formatting would conflict with correct machine output, correctness wins automatically. Section 0 states that boundary explicitly.
11
+
12
+ ## 0. The human/machine boundary (read this first)
13
+
14
+ Two layers, treated differently:
15
+
16
+ - **Presentation layer (human-facing)** — section titles, prose, help text, report narratives, guidance notes, error wording, dashboard markup and CSS. This is what the rest of this rule shapes.
17
+ - **Data layer (machine-readable)** — never reshape it for readability. Off-limits: any `--json` / `--plain` output; `specialists/registry.json`, `contracts.json`, `role-manifests.json`; the parsed tokens hooks emit (for example the session-start `## Working branch: **<name>**` line, the efficiency status enum `degraded` / `configured` / `healthy`, commit hashes, counts); auxiliary state JSON; any text one component parses out of another.
18
+
19
+ When a format choice would change a value, key, ordering, or token that something downstream parses, do not make it. Accessibility is presentation; it never edits the contract.
20
+
21
+ ## 1. Lead with the answer
22
+
23
+ State the conclusion first, then the support. The reader should get the outcome — what happened, what is needed, what changed — in the first line, before any reasoning. Put deliberation after the answer, or leave it out.
24
+
25
+ ## 2. One clear hierarchy
26
+
27
+ Use headings in order (H2, then H3) and do not skip or nest deeper. One `h1`-equivalent per surface. A reader should be able to scan headings alone and know the shape of the output.
28
+
29
+ ## 3. Scannable chunks
30
+
31
+ Short paragraphs. Bulleted or numbered lists in place of long sentences that pack several facts together. White space between sections. Prefer three short lines over one dense one.
32
+
33
+ ## 4. Plain, literal language
34
+
35
+ Say the thing directly. Avoid idioms, sarcasm, irony, and figurative phrasing that require inference to decode. Define a term on first use or link to where it is defined. One name for one concept — do not alternate synonyms for the same thing.
36
+
37
+ ## 5. Consistent structure and terminology
38
+
39
+ The same kind of output keeps the same shape every time. Status reports, error messages, and command help follow a predictable order so the reader learns the pattern once. Predictability lowers cognitive load more than cleverness raises it.
40
+
41
+ ## 6. Explicit state and next step
42
+
43
+ End human-facing output with the current state and the next action in plain words — what is done, what is blocked, what the reader does next. Never bury the ask. Never imply a next step through tone.
44
+
45
+ ## 7. Never let color, emoji, or motion be the only signal
46
+
47
+ - Meaning carried by color must also be carried by text, shape, or label. A red number is also labelled `error`; a green check is also the word `passed`.
48
+ - Honor `NO_COLOR`, non-TTY, and `TERM=dumb`: degrade to plain text, never to nothing.
49
+ - Respect terminal width — wrap rather than overflow.
50
+ - On the dashboard, honor `prefers-reduced-motion` and `prefers-color-scheme`; motion is never required to understand state, and animation can be fully disabled.
51
+
52
+ ## Applies to
53
+
54
+ - **Terminal / CLI** — help text, status, reports, hook display. Routed through the shared formatter (`lib/term-format.mjs`); color and width handled there, never per-call.
55
+ - **Agent prose** — every response Construct and its specialists return to the user.
56
+ - **Dashboard** — `apps/dashboard/` source (semantic landmarks, labels, focus, contrast, reduced-motion), rebuilt into `lib/server/static/`.
57
+
58
+ ## Enforcement
59
+
60
+ - Persona prompt: `personas/construct.md` carries the output-style directive; specialists inherit the standard through the front-door agent.
61
+ - `tests/term-format.test.mjs` asserts the terminal formatter honors `NO_COLOR` / non-TTY and that machine-readable `--json` output is unchanged by presentation changes.
62
+ - `construct dashboard:sync --check` keeps the built dashboard in step with the accessible source.
63
+
64
+ ## Bypass
65
+
66
+ There is no bypass. If a format choice and a machine contract conflict, the contract wins (Section 0) — change the presentation, never the data. If this rule is wrong-shaped, fix the rule, not the output.
@@ -0,0 +1,37 @@
1
+ ---
2
+ description: deliverable artifacts are about the user's project, never about Construct or its internal machinery.
3
+ enforced_by: (persona prompt), lib/comment-lint.mjs
4
+ precedence_tier: correctness
5
+ ---
6
+ # Tool Invisibility
7
+
8
+ Construct is scaffolding. The artifacts it helps produce — strategies, PRDs, ADRs, research briefs, memos, knowledge notes — belong to the user and their project. The user's document must carry no fingerprints of the tool that built it. A strategy authored through Construct should read exactly as if the user wrote it about their own product, with zero trace of the orchestration underneath.
9
+
10
+ This failed once, instructively: a strategy for a third-party project came back saying "Construct's bet is to own the layer…" and named `cx-product-manager` as a metric owner. The tooling had written itself into the user's deliverable. That is the failure this rule prevents.
11
+
12
+ ## 1. The artifact is about its subject, not its author
13
+
14
+ A deliverable is about the project it concerns. Do not make Construct — or the act of using Construct — the subject of content it produces for another project. When the strategy is for *their* platform, the bets are *theirs*, not "Construct's."
15
+
16
+ ## 2. Never name the tool or its internals in artifact content
17
+
18
+ In the body of any deliverable artifact, do not write:
19
+
20
+ - **`Construct`** as the product/company/subject (when the subject is not Construct).
21
+ - **`cx-*` specialist role ids** (`cx-business-strategist`, `cx-product-manager`, `cx-researcher`, …). These are internal routing detail — the persona already states that "internal routing and specialist dispatch are implementation detail." A metric owner is a real role on the user's team (e.g. "Product", "Eng lead"), never a `cx-*` id.
22
+ - **Internal orchestration mechanics** — task-packets, `orchestration_run`, dispatch chains, handoff contracts — as if they were part of the user's product.
23
+
24
+ Provenance belongs in a comment or a separate handoff, not in the deliverable's prose.
25
+
26
+ ## 3. The one exception: when the subject *is* Construct
27
+
28
+ When the project being worked on is Construct itself (this repo, package `@geraldmaron/construct`), naming Construct and its specialists is correct and required — the artifact is genuinely about them. The deterministic check below is disabled for the Construct repo for exactly this reason.
29
+
30
+ ## Enforcement
31
+
32
+ - **Prevention (persona + shared guidance):** every specialist and the Construct persona carry the invisibility directive, so the leak is avoided at generation time. This is the primary control; framing ("don't make the doc about Construct") is a judgment call the prompt owns.
33
+ - **Backstop (deterministic):** `lib/comment-lint.mjs` flags `cx-*` internal role-id tokens that appear in a *consuming project's* deliverable markdown — an unambiguous leak with near-zero false positives. The check is skipped when the working repo is Construct itself (package name `@geraldmaron/construct`). Severity follows `CONSTRUCT_ARTIFACT_LINT_MODE` (warn by default; block in the release gate).
34
+
35
+ ## Bypass
36
+
37
+ There is no bypass. If the check fires on a legitimate use, the use is almost certainly a leak — rewrite the artifact to be about the user's project. If the subject genuinely is Construct, the self-repo skip already covers it.