@geraldmaron/construct 1.4.1 → 1.5.0

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 (556) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1029 -65
  4. package/bin/construct-postinstall.mjs +27 -2
  5. package/config/tag-vocabulary.json +264 -0
  6. package/examples/distribution/sources/deck-one-pager.md +1 -1
  7. package/lib/acp/server.mjs +21 -7
  8. package/lib/adapters-sync.mjs +2 -1
  9. package/lib/audit-trail.mjs +185 -2
  10. package/lib/beads/auto-close.mjs +2 -1
  11. package/lib/beads/drift.mjs +2 -1
  12. package/lib/bridges/copilot-proxy.mjs +20 -5
  13. package/lib/certification/skill-inventory.mjs +10 -1
  14. package/lib/cli/approvals.mjs +147 -0
  15. package/lib/cli-commands.mjs +105 -14
  16. package/lib/comment-lint.mjs +127 -8
  17. package/lib/config/schema.mjs +7 -30
  18. package/lib/config/source-target-registry.mjs +70 -0
  19. package/lib/config/source-targets.mjs +179 -205
  20. package/lib/contracts/coverage.mjs +77 -0
  21. package/lib/contracts/validate.mjs +102 -13
  22. package/lib/contracts/violation-log.mjs +50 -4
  23. package/lib/db/migrate.mjs +69 -0
  24. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  25. package/lib/db/migrations/002_queue_provider.sql +50 -0
  26. package/lib/db/migrations/003_worker_registry.sql +17 -0
  27. package/lib/db/migrations/004_trace_events.sql +16 -0
  28. package/lib/db/migrations/005_shared_memory.sql +15 -0
  29. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  30. package/lib/decisions/enforced-baseline.json +0 -2
  31. package/lib/decisions/registry.mjs +3 -2
  32. package/lib/deployment/parity-contract.mjs +1 -1
  33. package/lib/deployment-mode.mjs +78 -2
  34. package/lib/diagram-export.mjs +10 -1
  35. package/lib/distill.mjs +5 -2
  36. package/lib/docs-verify.mjs +2 -1
  37. package/lib/doctor/cli.mjs +1 -0
  38. package/lib/doctor/command-on-path.mjs +24 -0
  39. package/lib/doctor/diagnosis.mjs +109 -0
  40. package/lib/doctor/embedding-health.mjs +50 -0
  41. package/lib/doctor/engine-health.mjs +93 -0
  42. package/lib/doctor/graph-validate.mjs +47 -0
  43. package/lib/doctor/index.mjs +23 -4
  44. package/lib/doctor/sidecar-providers.mjs +56 -0
  45. package/lib/doctor/watchers/consistency.mjs +93 -1
  46. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  47. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  48. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  49. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  50. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  51. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  52. package/lib/document-export.mjs +52 -27
  53. package/lib/document-extract/docling-client.mjs +9 -5
  54. package/lib/document-extract/docling-sidecar.py +30 -0
  55. package/lib/document-extract.mjs +1 -1
  56. package/lib/document-ingest.mjs +9 -0
  57. package/lib/embed/approval-queue.mjs +184 -88
  58. package/lib/embed/authority-guard.mjs +65 -2
  59. package/lib/embed/capability-jobs.mjs +365 -0
  60. package/lib/embed/capability-lifecycle.mjs +219 -0
  61. package/lib/embed/capability-loader.mjs +303 -0
  62. package/lib/embed/capability-runtime.mjs +58 -0
  63. package/lib/embed/cli.mjs +185 -8
  64. package/lib/embed/config.mjs +4 -0
  65. package/lib/embed/daemon.mjs +125 -6
  66. package/lib/embed/demand-fetch.mjs +190 -54
  67. package/lib/embed/inbox.mjs +12 -10
  68. package/lib/embed/presets/ops-triage.mjs +236 -0
  69. package/lib/embed/presets/pm-feedback.mjs +341 -0
  70. package/lib/embed/presets/tpm.mjs +401 -0
  71. package/lib/embed/providers/jira.mjs +72 -1
  72. package/lib/embed/providers/registry.mjs +140 -33
  73. package/lib/embed/worker.mjs +5 -0
  74. package/lib/embedded-contract/capability.mjs +4 -0
  75. package/lib/embedded-contract/execution.mjs +1 -1
  76. package/lib/embedded-contract/model-resolve.mjs +53 -3
  77. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  78. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  79. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  80. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  81. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  82. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  83. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  84. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  85. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  86. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  88. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  90. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  91. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  92. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  93. package/lib/env-config.mjs +60 -11
  94. package/lib/export-validate.mjs +34 -2
  95. package/lib/extensions/index.mjs +27 -0
  96. package/lib/extensions/loader.mjs +120 -0
  97. package/lib/extensions/manifest-schema.mjs +141 -0
  98. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  99. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  100. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  101. package/lib/extensions/manifests/directory.manifest.json +12 -0
  102. package/lib/extensions/manifests/docling.manifest.json +37 -0
  103. package/lib/extensions/manifests/echo.manifest.json +8 -0
  104. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  105. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  106. package/lib/extensions/manifests/github.manifest.json +52 -0
  107. package/lib/extensions/manifests/linear.manifest.json +50 -0
  108. package/lib/extensions/manifests/local.manifest.json +12 -0
  109. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  110. package/lib/extensions/manifests/openai.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  113. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  114. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  115. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  116. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  117. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  118. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  119. package/lib/extensions/manifests/slack.manifest.json +58 -0
  120. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  121. package/lib/extensions/validate.mjs +175 -0
  122. package/lib/features.mjs +13 -9
  123. package/lib/flows/checkpoint.mjs +184 -0
  124. package/lib/flows/constants.mjs +27 -0
  125. package/lib/flows/define.mjs +146 -0
  126. package/lib/flows/engine.mjs +249 -0
  127. package/lib/flows/errors.mjs +19 -0
  128. package/lib/flows/index.mjs +27 -0
  129. package/lib/flows/joins.mjs +18 -0
  130. package/lib/flows/schema.mjs +90 -0
  131. package/lib/flows/state.mjs +31 -0
  132. package/lib/frameworks/loader.mjs +99 -0
  133. package/lib/frameworks/schema.mjs +157 -0
  134. package/lib/graph/build-from-corpus.mjs +67 -0
  135. package/lib/graph/build-from-embed.mjs +82 -0
  136. package/lib/graph/build-from-registry.mjs +164 -3
  137. package/lib/graph/cli.mjs +456 -12
  138. package/lib/graph/gap-queries.mjs +156 -0
  139. package/lib/graph/gaps.mjs +41 -0
  140. package/lib/graph/impacted.mjs +129 -0
  141. package/lib/graph/runtime-evidence.mjs +177 -0
  142. package/lib/graph/security-coverage.mjs +113 -0
  143. package/lib/graph/staleness.mjs +241 -10
  144. package/lib/graph/store.mjs +24 -7
  145. package/lib/graph/validate.mjs +161 -0
  146. package/lib/headhunt.mjs +9 -8
  147. package/lib/health-check.mjs +15 -7
  148. package/lib/hook-health.mjs +1 -1
  149. package/lib/hooks/agent-tracker.mjs +10 -4
  150. package/lib/hooks/edit-guard.mjs +3 -3
  151. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  152. package/lib/hooks/guard-bash.mjs +41 -15
  153. package/lib/hooks/mcp-health-check.mjs +11 -4
  154. package/lib/hooks/model-fallback.mjs +50 -6
  155. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  156. package/lib/hooks/pre-compact.mjs +181 -173
  157. package/lib/hooks/rule-verifier.mjs +2 -1
  158. package/lib/hooks/session-reflect.mjs +2 -1
  159. package/lib/hooks/session-start.mjs +3 -3
  160. package/lib/host/readiness.mjs +109 -0
  161. package/lib/host-capabilities.mjs +13 -2
  162. package/lib/host-disposition.mjs +29 -8
  163. package/lib/identity.mjs +92 -0
  164. package/lib/ingest/degraded-extract.mjs +96 -0
  165. package/lib/ingest/docling-remote.mjs +2 -1
  166. package/lib/ingest/provider-extract.mjs +7 -19
  167. package/lib/ingest/sidecar-providers.mjs +196 -0
  168. package/lib/ingest-tooling.mjs +11 -5
  169. package/lib/init-unified.mjs +55 -38
  170. package/lib/init-update.mjs +2 -1
  171. package/lib/install/legacy-global-cleanup.mjs +25 -0
  172. package/lib/install/stage-project.mjs +41 -4
  173. package/lib/intake/daemon.mjs +99 -8
  174. package/lib/intake/git-queue.mjs +110 -38
  175. package/lib/intake/prepare.mjs +2 -0
  176. package/lib/intake/queue-registry.mjs +50 -0
  177. package/lib/intake/queue.mjs +133 -17
  178. package/lib/intake/session-prelude.mjs +57 -8
  179. package/lib/integrations/intake-integrations.mjs +61 -111
  180. package/lib/intent-classifier.mjs +43 -5
  181. package/lib/libreoffice-export.mjs +8 -8
  182. package/lib/logging/rotate.mjs +5 -4
  183. package/lib/mcp/broker.mjs +332 -17
  184. package/lib/mcp/denied-store.mjs +95 -0
  185. package/lib/mcp/destructive-approval.mjs +57 -0
  186. package/lib/mcp/destructive-gate.mjs +30 -0
  187. package/lib/mcp/dispatch-envelope.mjs +199 -0
  188. package/lib/mcp/memory-bridge.mjs +2 -1
  189. package/lib/mcp/server.mjs +185 -1268
  190. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  191. package/lib/mcp/tool-definitions-project.mjs +271 -0
  192. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  193. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  194. package/lib/mcp/tool-definitions.mjs +25 -0
  195. package/lib/mcp/tool-rate-limit.mjs +47 -0
  196. package/lib/mcp/tool-registry.mjs +107 -0
  197. package/lib/mcp/tool-safety.mjs +95 -0
  198. package/lib/mcp/tool-surface-parity.mjs +60 -0
  199. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  200. package/lib/mcp/tools/orchestration-run.mjs +197 -19
  201. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  202. package/lib/mcp/tools/project.mjs +25 -8
  203. package/lib/mcp/tools/provider-write.mjs +187 -0
  204. package/lib/mcp/tools/scope.mjs +0 -3
  205. package/lib/mcp/tools/skills.mjs +1 -1
  206. package/lib/mcp/tools/storage.mjs +2 -2
  207. package/lib/mcp/tools/web-search-governance.mjs +96 -0
  208. package/lib/mcp/tools/web-search.mjs +5 -76
  209. package/lib/mcp/transport/auth.mjs +238 -0
  210. package/lib/mcp/transport/http.mjs +136 -0
  211. package/lib/mcp/transport/mode.mjs +31 -0
  212. package/lib/mcp/transport/stdio.mjs +22 -0
  213. package/lib/mcp-catalog.json +4 -4
  214. package/lib/mcp-manager.mjs +34 -23
  215. package/lib/mcp-platform-config.mjs +53 -20
  216. package/lib/mode-capabilities.mjs +109 -0
  217. package/lib/model-router.mjs +42 -9
  218. package/lib/models/catalog.mjs +40 -1
  219. package/lib/net-guard.mjs +247 -0
  220. package/lib/observation-store.mjs +6 -2
  221. package/lib/ollama/provision-context.mjs +2 -1
  222. package/lib/opencode-config.mjs +22 -3
  223. package/lib/opencode-runtime-plugin.mjs +120 -2
  224. package/lib/oracle/actions.mjs +204 -10
  225. package/lib/oracle/cli.mjs +24 -3
  226. package/lib/oracle/daemon-entry.mjs +6 -0
  227. package/lib/oracle/dispatch.mjs +2 -1
  228. package/lib/oracle/execute.mjs +2 -2
  229. package/lib/oracle/gaps.mjs +3 -3
  230. package/lib/oracle/heartbeat.mjs +53 -0
  231. package/lib/oracle/read-model.mjs +69 -13
  232. package/lib/oracle/reconcile.mjs +3 -46
  233. package/lib/oracle/routing.mjs +37 -31
  234. package/lib/oracle/synthesize.mjs +1 -1
  235. package/lib/orchestration/classification.mjs +434 -0
  236. package/lib/orchestration/delegation-flow.mjs +132 -0
  237. package/lib/orchestration/flow-selection.mjs +418 -0
  238. package/lib/orchestration/gates.mjs +244 -0
  239. package/lib/orchestration/host-sampling.mjs +121 -0
  240. package/lib/orchestration/policy-constants.mjs +48 -0
  241. package/lib/orchestration/provider-outcome.mjs +197 -0
  242. package/lib/orchestration/readiness.mjs +114 -13
  243. package/lib/orchestration/run-store-postgres.mjs +32 -26
  244. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  245. package/lib/orchestration/run-store.mjs +25 -12
  246. package/lib/orchestration/runtime.mjs +508 -62
  247. package/lib/orchestration/store.mjs +87 -12
  248. package/lib/orchestration/trace-store.mjs +125 -0
  249. package/lib/orchestration/web-capability.mjs +60 -0
  250. package/lib/orchestration/worker-runtime.mjs +162 -0
  251. package/lib/orchestration/worker.mjs +763 -80
  252. package/lib/orchestration-policy.mjs +67 -1111
  253. package/lib/output-quality.mjs +61 -2
  254. package/lib/packs/cli.mjs +144 -0
  255. package/lib/packs/core-pack.mjs +112 -0
  256. package/lib/packs/enablement.mjs +187 -0
  257. package/lib/packs/index.mjs +23 -0
  258. package/lib/packs/loader.mjs +176 -0
  259. package/lib/packs/manifest-schema.mjs +58 -0
  260. package/lib/packs/prompts.mjs +120 -0
  261. package/lib/packs/validate.mjs +208 -0
  262. package/lib/path-policy.mjs +56 -0
  263. package/lib/plugin-registry.mjs +4 -5
  264. package/lib/policy/audit-gate.mjs +42 -0
  265. package/lib/policy/consumption-budget.mjs +149 -0
  266. package/lib/policy/engine.mjs +81 -6
  267. package/lib/policy/role-authority.mjs +89 -0
  268. package/lib/prompt-composer.js +19 -3
  269. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  270. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  271. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  272. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  273. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  274. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  275. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  276. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  277. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  278. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  279. package/lib/providers/contract.mjs +207 -0
  280. package/lib/providers/credential-bootstrap.mjs +7 -4
  281. package/lib/providers/credential-sources.mjs +14 -2
  282. package/lib/providers/directory/index.mjs +261 -0
  283. package/lib/providers/feedback/index.mjs +392 -0
  284. package/lib/providers/filter-audit.mjs +68 -0
  285. package/lib/providers/filter-schema.mjs +54 -0
  286. package/lib/providers/github/index.mjs +31 -0
  287. package/lib/providers/instance-config.mjs +215 -0
  288. package/lib/providers/op-locate.mjs +96 -0
  289. package/lib/providers/op-run.mjs +64 -9
  290. package/lib/providers/registry.mjs +26 -3
  291. package/lib/providers/secret-audit-wiring.mjs +41 -18
  292. package/lib/providers/secret-resolver.mjs +263 -31
  293. package/lib/publish-template.mjs +6 -3
  294. package/lib/publish-tooling.mjs +4 -0
  295. package/lib/publish.mjs +32 -4
  296. package/lib/queue/pg-queue.mjs +395 -0
  297. package/lib/reflect.mjs +2 -1
  298. package/lib/registry/assemble.mjs +28 -2
  299. package/lib/registry/catalog.mjs +6 -0
  300. package/lib/registry/consolidation.mjs +8 -1
  301. package/lib/registry/custom-scaffold.mjs +200 -0
  302. package/lib/registry/custom-schema.mjs +137 -0
  303. package/lib/registry/loader.mjs +15 -4
  304. package/lib/registry/manifests/format-engines.default.json +15 -0
  305. package/lib/registry/manifests/surface-map.default.json +46 -0
  306. package/lib/registry/retired-paths.mjs +1 -0
  307. package/lib/registry/surface-map.mjs +100 -50
  308. package/lib/registry/validate.mjs +3 -3
  309. package/lib/render-visual-check.mjs +240 -0
  310. package/lib/resources/budget.mjs +40 -17
  311. package/lib/roles/flavor-bindings.mjs +36 -14
  312. package/lib/roles/gateway.mjs +15 -8
  313. package/lib/roots.mjs +107 -0
  314. package/lib/runtime/uv-bootstrap.mjs +32 -6
  315. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  316. package/lib/runtime-env.mjs +5 -2
  317. package/lib/sandbox.mjs +1 -1
  318. package/lib/scheduler/index.mjs +8 -20
  319. package/lib/schema-infer.mjs +31 -2
  320. package/lib/scopes/lifecycle.mjs +29 -25
  321. package/lib/scopes/loader.mjs +49 -12
  322. package/lib/scopes/teams.mjs +1 -1
  323. package/lib/security/ingest-boundary.mjs +80 -0
  324. package/lib/security/recall-wrapper.mjs +99 -0
  325. package/lib/security/trust.mjs +132 -0
  326. package/lib/service-manager.mjs +93 -24
  327. package/lib/setup.mjs +41 -23
  328. package/lib/skills-apply.mjs +4 -2
  329. package/lib/specialists/postconditions.mjs +2 -2
  330. package/lib/state-root.mjs +147 -0
  331. package/lib/status.mjs +597 -6
  332. package/lib/storage/admin.mjs +84 -7
  333. package/lib/storage/backend-registry.mjs +73 -0
  334. package/lib/storage/backend.mjs +63 -9
  335. package/lib/storage/embeddings-openai.mjs +12 -2
  336. package/lib/storage/hybrid-query.mjs +11 -3
  337. package/lib/storage/retrieval-hardening.mjs +384 -0
  338. package/lib/storage/shared-memory.mjs +158 -0
  339. package/lib/storage/vector-client.mjs +69 -2
  340. package/lib/team/health.mjs +72 -0
  341. package/lib/telemetry/backends/local.mjs +9 -3
  342. package/lib/telemetry/client.mjs +3 -7
  343. package/lib/template-registry.mjs +10 -12
  344. package/lib/tenant/context.mjs +82 -0
  345. package/lib/tenant/isolation.mjs +129 -0
  346. package/lib/test-corpus-inventory.mjs +103 -2
  347. package/lib/uninstall/uninstall.mjs +78 -12
  348. package/lib/validator.mjs +4 -6
  349. package/lib/worker/entrypoint.mjs +2 -1
  350. package/lib/worker/run.mjs +2 -2
  351. package/lib/worker/trace.mjs +5 -11
  352. package/lib/workflow-state.mjs +52 -8
  353. package/lib/workflows/liveness.mjs +203 -0
  354. package/lib/workflows/loader.mjs +177 -0
  355. package/lib/workflows/manifest-schema.mjs +71 -0
  356. package/lib/workflows/surface-parity.mjs +118 -0
  357. package/lib/workflows/validate.mjs +127 -0
  358. package/lib/writes/control-plane.mjs +140 -0
  359. package/lib/writes/envelope.mjs +206 -0
  360. package/lib/writes/sent-log.mjs +86 -0
  361. package/lib/writes/write-intent.mjs +109 -0
  362. package/package.json +20 -7
  363. package/personas/construct.md +12 -3
  364. package/registry/agent-manifest.json +117 -0
  365. package/registry/capabilities.json +1987 -0
  366. package/rules/common/comments.md +1 -0
  367. package/schemas/brand-voice.schema.json +24 -0
  368. package/schemas/capability-registry.schema.json +72 -0
  369. package/schemas/certification-run.schema.json +130 -0
  370. package/schemas/demo-recording.schema.json +46 -0
  371. package/schemas/eval-dataset.schema.json +79 -0
  372. package/schemas/execution-capability-profile.schema.json +46 -0
  373. package/schemas/execution-policy.schema.json +114 -0
  374. package/schemas/improvement-proposal.schema.json +65 -0
  375. package/schemas/mcp-tool-output.schema.json +61 -0
  376. package/schemas/platform-capabilities.schema.json +83 -0
  377. package/schemas/project-config.schema.json +215 -0
  378. package/schemas/project-demo.schema.json +60 -0
  379. package/schemas/provider-behavior-matrix.schema.json +91 -0
  380. package/schemas/scope.schema.json +197 -0
  381. package/schemas/specialist-trace.schema.json +107 -0
  382. package/schemas/team.schema.json +99 -0
  383. package/schemas/unified-registry.schema.json +546 -0
  384. package/scripts/sync-specialists.mjs +336 -106
  385. package/skills/docs/adr-workflow.md +1 -1
  386. package/skills/docs/codebase-research-workflow.md +3 -3
  387. package/skills/docs/prd-workflow.md +5 -6
  388. package/skills/docs/runbook-workflow.md +2 -2
  389. package/skills/docs/user-research-workflow.md +3 -3
  390. package/skills/operating/fleet-health-routing.md +77 -0
  391. package/skills/roles/ai-engineer.md +1 -1
  392. package/skills/roles/architect.md +0 -1
  393. package/skills/roles/business-strategist.md +1 -1
  394. package/skills/roles/data-analyst.telemetry.md +1 -1
  395. package/skills/roles/data-engineer.md +1 -1
  396. package/skills/roles/data-engineer.pipeline.md +1 -1
  397. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  398. package/skills/roles/data-engineer.warehouse.md +1 -1
  399. package/skills/roles/designer.accessibility.md +1 -1
  400. package/skills/roles/designer.md +0 -1
  401. package/skills/roles/devil-advocate.md +1 -1
  402. package/skills/roles/docs-keeper.md +1 -1
  403. package/skills/roles/evaluator.md +1 -1
  404. package/skills/roles/explorer.md +1 -1
  405. package/skills/roles/platform-engineer.md +1 -1
  406. package/skills/roles/qa.ai-eval.md +1 -2
  407. package/skills/roles/qa.api-contract.md +0 -1
  408. package/skills/roles/qa.data-pipeline.md +0 -1
  409. package/skills/roles/qa.md +0 -1
  410. package/skills/roles/qa.web-ui.md +0 -1
  411. package/skills/roles/release-manager.md +1 -1
  412. package/skills/roles/researcher.md +0 -2
  413. package/skills/roles/reviewer.md +0 -3
  414. package/skills/roles/security.ai.md +1 -1
  415. package/skills/roles/security.legal-compliance.md +1 -1
  416. package/skills/roles/security.md +0 -1
  417. package/skills/roles/security.privacy.md +0 -1
  418. package/skills/roles/security.supply-chain.md +1 -1
  419. package/skills/roles/sre.md +1 -1
  420. package/skills/roles/test-automation.md +1 -1
  421. package/skills/roles/trace-reviewer.md +1 -1
  422. package/skills/roles/ux-researcher.md +1 -1
  423. package/specialists/artifact-manifest.json +36 -36
  424. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  425. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  426. package/specialists/org/contracts/any-to-debugger.json +7 -1
  427. package/specialists/org/contracts/any-to-designer.json +7 -1
  428. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  429. package/specialists/org/contracts/any-to-explorer.json +13 -4
  430. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  431. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  432. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  433. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  434. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  435. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  436. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  437. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  438. package/specialists/org/contracts/architect-to-operations.json +17 -4
  439. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  440. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  441. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  442. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  443. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  444. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  445. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  446. package/specialists/org/contracts/operations-to-user.json +53 -0
  447. package/specialists/org/contracts/operations-triage-output.json +53 -0
  448. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  449. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  450. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  451. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  452. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  453. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  454. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  455. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  456. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  457. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  458. package/specialists/org/contracts/user-to-construct.json +11 -4
  459. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  460. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  461. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  462. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  463. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  464. package/specialists/org/groups/engineering-group.json +2 -6
  465. package/specialists/org/groups/governance-group.json +2 -3
  466. package/specialists/org/groups/operations-group.json +5 -8
  467. package/specialists/org/groups/product-group.json +4 -8
  468. package/specialists/org/groups/quality-group.json +3 -7
  469. package/specialists/org/groups/strategy-group.json +6 -9
  470. package/specialists/org/models.json.example +8 -0
  471. package/specialists/org/scopes/creative.json +6 -1
  472. package/specialists/org/scopes/operations.json +7 -1
  473. package/specialists/org/scopes/research.json +6 -1
  474. package/specialists/org/scopes/rnd.json +7 -1
  475. package/specialists/org/specialists/cx-architect.json +27 -8
  476. package/specialists/org/specialists/cx-designer.json +22 -8
  477. package/specialists/org/specialists/cx-engineer.json +71 -7
  478. package/specialists/org/specialists/cx-operations.json +89 -15
  479. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  480. package/specialists/org/specialists/cx-product-manager.json +28 -9
  481. package/specialists/org/specialists/cx-qa.json +16 -6
  482. package/specialists/org/specialists/cx-researcher.json +41 -7
  483. package/specialists/org/specialists/cx-reviewer.json +61 -11
  484. package/specialists/org/specialists/cx-security.json +30 -10
  485. package/specialists/org/teams/design-team.json +3 -4
  486. package/specialists/org/teams/engineering-team.json +3 -10
  487. package/specialists/org/teams/governance-team.json +3 -5
  488. package/specialists/org/teams/operations-team.json +6 -12
  489. package/specialists/org/teams/product-management-team.json +2 -3
  490. package/specialists/org/teams/quality-team.json +4 -12
  491. package/specialists/org/teams/research-team.json +3 -3
  492. package/specialists/org/teams/strategy-team.json +7 -14
  493. package/specialists/prompts/cx-architect.md +20 -1
  494. package/specialists/prompts/cx-data-analyst.md +1 -1
  495. package/specialists/prompts/cx-designer.md +12 -4
  496. package/specialists/prompts/cx-engineer.md +15 -1
  497. package/specialists/prompts/cx-operations.md +14 -2
  498. package/specialists/prompts/cx-orchestrator.md +9 -5
  499. package/specialists/prompts/cx-product-manager.md +5 -1
  500. package/specialists/prompts/cx-qa.md +6 -2
  501. package/specialists/prompts/cx-researcher.md +27 -31
  502. package/specialists/prompts/cx-reviewer.md +19 -1
  503. package/specialists/prompts/cx-security.md +5 -1
  504. package/templates/distribution/construct-brand.typ +123 -59
  505. package/templates/distribution/construct-decision.typ +6 -3
  506. package/templates/distribution/construct-pdf.typ +11 -4
  507. package/templates/distribution/construct-prd.typ +6 -3
  508. package/templates/distribution/construct-research.typ +7 -4
  509. package/vendor/pandoc-ext/README.md +3 -0
  510. package/vendor/pandoc-ext/diagram.lua +687 -0
  511. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  512. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  513. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  514. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  515. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  516. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  517. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  518. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  519. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  520. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  521. package/specialists/org/specialists/cx-accessibility.json +0 -69
  522. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  523. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  524. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  525. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  526. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  527. package/specialists/org/specialists/cx-evaluator.json +0 -69
  528. package/specialists/org/specialists/cx-explorer.json +0 -69
  529. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  530. package/specialists/org/specialists/cx-oracle.json +0 -46
  531. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  532. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  533. package/specialists/org/specialists/cx-release-manager.json +0 -77
  534. package/specialists/org/specialists/cx-sre.json +0 -94
  535. package/specialists/org/specialists/cx-test-automation.json +0 -69
  536. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  537. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  538. package/specialists/org/teams/accessibility-team.json +0 -39
  539. package/specialists/org/teams/ux-research-team.json +0 -39
  540. package/specialists/prompts/cx-accessibility.md +0 -55
  541. package/specialists/prompts/cx-ai-engineer.md +0 -124
  542. package/specialists/prompts/cx-business-strategist.md +0 -58
  543. package/specialists/prompts/cx-data-engineer.md +0 -55
  544. package/specialists/prompts/cx-devil-advocate.md +0 -59
  545. package/specialists/prompts/cx-docs-keeper.md +0 -164
  546. package/specialists/prompts/cx-evaluator.md +0 -49
  547. package/specialists/prompts/cx-explorer.md +0 -71
  548. package/specialists/prompts/cx-legal-compliance.md +0 -58
  549. package/specialists/prompts/cx-oracle.md +0 -98
  550. package/specialists/prompts/cx-platform-engineer.md +0 -97
  551. package/specialists/prompts/cx-rd-lead.md +0 -59
  552. package/specialists/prompts/cx-release-manager.md +0 -54
  553. package/specialists/prompts/cx-sre.md +0 -111
  554. package/specialists/prompts/cx-test-automation.md +0 -55
  555. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  556. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -26,6 +26,7 @@ import { missingIgnorePatterns, isConstructPackageRepo } from './host-dispositio
26
26
  import { HOST_KEYS, displayNameToKey } from './platforms/capabilities.mjs';
27
27
  import { DOC_LANES, LANE_ORDER, DOC_PRESETS } from './init/doc-lanes.mjs';
28
28
  import { initWorkflow } from './workflow-state.mjs';
29
+ import { isMainModule } from './roots.mjs';
29
30
 
30
31
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
31
32
  const ROOT_DIR = path.join(__dirname, "..");
@@ -45,6 +46,7 @@ const withPostmortemsFlag = args.includes("--with-postmortems");
45
46
  const withArchitectureFlag = args.includes("--with-architecture");
46
47
  const withReadmeFlag = args.includes("--with-readme");
47
48
  const withDevcontainerFlag = args.includes("--devcontainer");
49
+ const withSeedIndexFlag = args.includes("--seed-index");
48
50
 
49
51
  // Behavior flags
50
52
  const verbose = args.includes("--verbose") || args.includes("-v");
@@ -56,6 +58,10 @@ const skipInteractive = !interactive;
56
58
  // templates/ already exist (issue #97).
57
59
  const forceScaffold = args.includes("--force");
58
60
 
61
+ // --no-beads skips tracker initialization entirely and marks beads as disabled
62
+ // in the result. Use when the host environment cannot run bd (e.g. ephemeral CI).
63
+ const noBeads = args.includes("--no-beads");
64
+
59
65
  // `bd init` is git-native and unconditionally commits its bootstrap; ADR-0027
60
66
  // §3 requires init to leave the host repo's commit history untouched unless the
61
67
  // user opts in. --commit-bootstrap keeps that commit; the default withholds it.
@@ -986,41 +992,49 @@ async function main() {
986
992
  skipped.push('.cx/construct_guide.md');
987
993
  }
988
994
 
989
- // Auto-init beads
995
+ // Auto-init beads. Fail-closed: a tracker failure leaves the project in an
996
+ // inconsistent state (no hooks, no issue tracking) — better to stop early than
997
+ // silently proceed. Pass --no-beads to opt out for ephemeral/CI environments.
990
998
  const beadsMeta = path.join(target, ".beads", "metadata.json");
991
- if (!fs.existsSync(beadsMeta) && !isToolRepo) {
999
+ let beadsReady = false;
1000
+ if (noBeads) {
1001
+ console.log(' ⚠️ Skipping task tracker initialization (--no-beads).');
1002
+ skipped.push('.beads/ (--no-beads)');
1003
+ } else if (!fs.existsSync(beadsMeta) && !isToolRepo) {
992
1004
  console.log('');
993
1005
  console.log('Initializing the task tracker (beads — local-first, git-synced, works offline)…');
994
1006
  fs.mkdirSync(path.join(target, ".beads"), { recursive: true });
995
- try {
996
- const beads = initializeBeadsTracker(target, { commitBootstrap });
997
- created.push(".beads/ (initialized)");
998
- const prefixNote = beads?.prefix ? ` issues prefixed \`${beads.prefix}-\`` : "";
999
- console.log(` ✓ Task tracker ready${prefixNote}. Run \`bd quickstart\` to start.`);
1000
- if (beads?.withheldCommit) {
1001
- console.log(" Bootstrap files left uncommitted (commit them yourself, or re-run with --commit-bootstrap).");
1002
- }
1003
- } catch (e) {
1004
- console.warn("⚠️ Beads init failed:", e.message);
1007
+ const beads = initializeBeadsTracker(target, { commitBootstrap });
1008
+ created.push(".beads/ (initialized)");
1009
+ const prefixNote = beads?.prefix ? ` — issues prefixed \`${beads.prefix}-\`` : "";
1010
+ console.log(` ✓ Task tracker ready${prefixNote}. Run \`bd quickstart\` to start.`);
1011
+ if (beads?.withheldCommit) {
1012
+ console.log(" Bootstrap files left uncommitted (commit them yourself, or re-run with --commit-bootstrap).");
1005
1013
  }
1006
1014
  console.log('');
1015
+ beadsReady = true;
1016
+ } else if (fs.existsSync(beadsMeta)) {
1017
+ beadsReady = true;
1007
1018
  }
1008
1019
 
1009
1020
  // Wire the project's git hooks to .beads/hooks so Construct's pre-commit
1010
1021
  // secret-scan and policy gates activate. Owned by init, not install (ADR-0027
1011
1022
  // §3): install must never mutate the cwd repo. Idempotent — no-op when
1012
1023
  // .beads/hooks/pre-commit is absent or a non-default custom path is set.
1024
+ // Skipped when beads is disabled or was not successfully initialized.
1013
1025
 
1014
- try {
1015
- const { ensureGitHooksPath } = await import('./git-hooks-path.mjs');
1016
- const hooks = ensureGitHooksPath({ cwd: target });
1017
- if (hooks.status === 'set') {
1018
- created.push('git core.hooksPath .beads/hooks');
1019
- if (!quiet) console.log(` ✓ ${hooks.message}`);
1020
- } else if (hooks.status === 'warning' && !quiet) {
1021
- console.log(` ⚠️ ${hooks.message}`);
1022
- }
1023
- } catch { /* git-hooks wiring is advisory; init proceeds */ }
1026
+ if (beadsReady) {
1027
+ try {
1028
+ const { ensureGitHooksPath } = await import('./git-hooks-path.mjs');
1029
+ const hooks = ensureGitHooksPath({ cwd: target });
1030
+ if (hooks.status === 'set') {
1031
+ created.push('git core.hooksPath → .beads/hooks');
1032
+ if (!quiet) console.log(` ✓ ${hooks.message}`);
1033
+ } else if (hooks.status === 'warning' && !quiet) {
1034
+ console.log(` ⚠️ ${hooks.message}`);
1035
+ }
1036
+ } catch { /* git-hooks wiring is advisory; init proceeds */ }
1037
+ }
1024
1038
 
1025
1039
  // Inject Construct's integration guidance into AGENTS.md and CLAUDE.md as a
1026
1040
  // versioned, hash-stamped marker block (ADR-0027 §2). Runs after bd init so
@@ -1154,22 +1168,25 @@ async function main() {
1154
1168
  }
1155
1169
  }
1156
1170
 
1157
- // Seed the vector index from the project's existing docs so the agent
1158
- // can compare new intake against established PRDs/RFCs/ADRs from day
1159
- // one. Best-effort: skipped silently when Postgres + embedding model
1160
- // aren't ready yet (DATABASE_URL unset, no ONNX cache). User can re-run
1161
- // `construct ingest` or `construct init` later to seed manually.
1171
+ // Vector-index seeding (embedding the project's existing docs so the agent
1172
+ // can compare new intake against established PRDs/RFCs/ADRs) is opt-in via
1173
+ // --seed-index (construct-rf26.17): the default init path performs zero
1174
+ // embedding work and never touches LanceDB, so a project that never uses
1175
+ // semantic search never pays for the model cache or the index directory.
1176
+ // `construct ingest` provisions both lazily on first real use.
1177
+
1178
+ if (withSeedIndexFlag) {
1179
+ try {
1180
+ const { syncFileStateToSql } = await import('./storage/sync.mjs');
1181
+ const projectName = path.basename(target);
1182
+ const seed = await syncFileStateToSql(target, { env: process.env, project: projectName });
1183
+ if (seed?.status === 'ok' && (seed.documentsSynced || 0) > 0) {
1184
+ console.log(`\n🔍 Indexed existing project material: ${seed.documentsSynced} doc(s), ${seed.embeddingsSynced || 0} embeddings.`);
1185
+ console.log(` The agent can now compare new intake (inbox/) against your existing PRDs, ADRs, and notes.`);
1186
+ }
1187
+ } catch { /* silent — corpus seeding is best-effort, not a setup blocker */ }
1188
+ }
1162
1189
 
1163
- try {
1164
- const { syncFileStateToSql } = await import('./storage/sync.mjs');
1165
- const projectName = path.basename(target);
1166
- const seed = await syncFileStateToSql(target, { env: process.env, project: projectName });
1167
- if (seed?.status === 'ok' && (seed.documentsSynced || 0) > 0) {
1168
- console.log(`\n🔍 Indexed existing project material: ${seed.documentsSynced} doc(s), ${seed.embeddingsSynced || 0} embeddings.`);
1169
- console.log(` The agent can now compare new intake (inbox/) against your existing PRDs, ADRs, and notes.`);
1170
- }
1171
- } catch { /* silent — corpus seeding is best-effort, not a setup blocker */ }
1172
-
1173
1190
  // ── Devcontainer recipe ────────────────────────────────────────────────
1174
1191
  if (withDevcontainerFlag) {
1175
1192
  const dcDir = path.join(target, '.devcontainer');
@@ -1252,7 +1269,7 @@ async function main() {
1252
1269
  }
1253
1270
  }
1254
1271
 
1255
- if (import.meta.url === `file://${process.argv[1]}`) {
1272
+ if (isMainModule(import.meta.url)) {
1256
1273
  main().catch((error) => {
1257
1274
  console.error(`Error: ${error.message}`);
1258
1275
  process.exit(1);
@@ -21,6 +21,7 @@ import {
21
21
  needsConstructGuideUpdate,
22
22
  writeConstructGuideProposal,
23
23
  } from "./init-update-guide.mjs";
24
+ import { isMainModule } from './roots.mjs';
24
25
 
25
26
  const args = process.argv.slice(2);
26
27
  const dryRun = args.includes("--dry-run");
@@ -226,7 +227,7 @@ async function main() {
226
227
  console.log("To replace .cx/construct_guide.md with the shipped template: construct init:update --apply-guide");
227
228
  }
228
229
 
229
- if (import.meta.url === `file://${process.argv[1]}`) {
230
+ if (isMainModule(import.meta.url)) {
230
231
  main().catch((error) => {
231
232
  console.error(`Error: ${error.message}`);
232
233
  process.exit(1);
@@ -62,6 +62,30 @@ function stripClaudeSettings(homeDir, opts) {
62
62
  if (changed) writeJson(filePath, settings, { ...opts, label: '~/.claude/settings.json Construct entries' });
63
63
  }
64
64
 
65
+ // construct-ranh moved Claude Code's MCP write path off ~/.claude/settings.json
66
+ // (never read by the CLI for server definitions) to ~/.claude.json's top-level
67
+ // `mcpServers`. A post-fix install registers MCP servers there, so this must be
68
+ // stripped alongside settings.json for the memory/cass/construct-mcp bridges to
69
+ // be fully removed.
70
+
71
+ function stripClaudeUserConfig(homeDir, opts) {
72
+ const filePath = path.join(homeDir, '.claude.json');
73
+ const config = readJson(filePath);
74
+ if (!config || typeof config !== 'object') return;
75
+ if (!config.mcpServers || typeof config.mcpServers !== 'object') return;
76
+
77
+ let changed = false;
78
+ for (const key of CONSTRUCT_MCP_KEYS) {
79
+ if (key in config.mcpServers) {
80
+ delete config.mcpServers[key];
81
+ changed = true;
82
+ }
83
+ }
84
+ if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
85
+
86
+ if (changed) writeJson(filePath, config, { ...opts, label: '~/.claude.json Construct MCP entries' });
87
+ }
88
+
65
89
  function stripOpenCode(homeDir, opts) {
66
90
  const filePath = path.join(homeDir, '.config', 'opencode', 'opencode.json');
67
91
  const config = readJson(filePath);
@@ -173,6 +197,7 @@ export function cleanupLegacyGlobalConstruct({ homeDir, dryRun = false } = {}) {
173
197
  });
174
198
 
175
199
  stripClaudeSettings(homeDir, opts);
200
+ stripClaudeUserConfig(homeDir, opts);
176
201
  stripOpenCode(homeDir, opts);
177
202
  stripVsCode(homeDir, opts);
178
203
  stripCodex(homeDir, opts);
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { spawnSync } from 'node:child_process';
14
- import { existsSync, copyFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
14
+ import { existsSync, readFileSync, copyFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
15
15
  import path from 'node:path';
16
16
 
17
17
  export function stageProjectAdapters({ projectRoot, packageRoot, pkgVersion, log, hosts = null }) {
@@ -27,7 +27,7 @@ export function stageProjectAdapters({ projectRoot, packageRoot, pkgVersion, log
27
27
 
28
28
  if (!existsSync(syncScript)) {
29
29
  emit(`sync-specialists.mjs not found at ${syncScript}; skipping adapter sync`);
30
- return { staged: true, synced: false };
30
+ return finishStage({ projectRoot, pkgVersion, synced: false, reason: 'sync-script-missing' });
31
31
  }
32
32
 
33
33
  // A `hosts` array restricts which adapter sets sync writes (construct-4xy6);
@@ -45,9 +45,46 @@ export function stageProjectAdapters({ projectRoot, packageRoot, pkgVersion, log
45
45
 
46
46
  if (result.status !== 0) {
47
47
  emit(`sync failed (exit ${result.status}); project left in a clean state`);
48
- return { staged: true, synced: false };
48
+ return finishStage({ projectRoot, pkgVersion, synced: false, reason: `sync-exit-${result.status}` });
49
49
  }
50
- return { staged: true, synced: true };
50
+ return finishStage({ projectRoot, pkgVersion, synced: true, reason: null });
51
+ }
52
+
53
+ const STAGE_STATE_FILE = 'stage-state.json';
54
+
55
+ // Record the staging outcome under .construct/ so a half-stage (launcher present,
56
+ // adapters not synced) is detectable and repairable instead of re-derived from
57
+ // filesystem heuristics. The marker write is best-effort — a state-write failure must
58
+ // never turn a successful stage into a reported failure.
59
+
60
+ function finishStage({ projectRoot, pkgVersion, synced, reason }) {
61
+ const dotConstruct = path.join(projectRoot, '.construct');
62
+ try {
63
+ mkdirSync(dotConstruct, { recursive: true });
64
+ const state = { staged: true, synced, reason, pkgVersion: pkgVersion ?? null, ts: new Date().toISOString() };
65
+ writeFileSync(path.join(dotConstruct, STAGE_STATE_FILE), JSON.stringify(state, null, 2) + '\n');
66
+ } catch { /* marker is advisory */ }
67
+ return { staged: true, synced };
68
+ }
69
+
70
+ export function readStageState(projectRoot) {
71
+ try {
72
+ return JSON.parse(readFileSync(path.join(projectRoot, '.construct', STAGE_STATE_FILE), 'utf8'));
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ // Re-drive a half-staged project to a synced state. stageProjectAdapters is idempotent,
79
+ // so re-running the sync is the repair; an already-synced project is a no-op.
80
+
81
+ export function repairStagedProject({ projectRoot, packageRoot, pkgVersion, log, hosts = null }) {
82
+ const state = readStageState(projectRoot);
83
+ if (state && state.synced === true) {
84
+ return { staged: true, synced: true, repaired: false };
85
+ }
86
+ const result = stageProjectAdapters({ projectRoot, packageRoot, pkgVersion, log, hosts });
87
+ return { ...result, repaired: result.synced === true };
51
88
  }
52
89
 
53
90
  function ensureProjectLauncher({ projectRoot, templateDir, pkgVersion }) {
@@ -6,6 +6,13 @@
6
6
  * Packets past their TTL move to `.cx/intake/dead-letter/`; failed
7
7
  * classification retries up to the budget then dead-letters with the reason.
8
8
  *
9
+ * Every tick also sweeps the persisted `.cx/intake/pending/` packets
10
+ * (sweepPendingPackets): a packet whose `intake.sourcePath` is missing from
11
+ * disk, or whose age exceeds classifyPacket's TTL, moves to
12
+ * `.cx/intake/dead-letter/` with a `deadLetterReason`. This covers persisted
13
+ * packets, not just in-flight inbox files, so the TTL/dead-letter contract
14
+ * above applies to the whole pending queue.
15
+ *
9
16
  * Atomic handoff (ADR-0045 §C): only complete top-level files are enqueued.
10
17
  * Dotfiles and the `inbox/.staging/` assembly directory are skipped, so a file
11
18
  * a writer is still staging is invisible until it is renamed into `inbox/`.
@@ -14,18 +21,19 @@
14
21
  * idle shutdown, heartbeat, killswitch, single-writer lock) applies.
15
22
  */
16
23
 
17
- import { readdirSync, statSync, readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'node:fs';
24
+ import { readdirSync, statSync, readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'node:fs';
18
25
  import { join, basename } from 'node:path';
19
26
 
20
27
  import { createDaemon, classifyPacket } from '../daemons/contract.mjs';
21
28
  import { memoryCapMbFor } from '../resources/process-budget.mjs';
29
+ import { resolveStatePath } from '../state-root.mjs';
22
30
 
23
31
  const KILLSWITCH_ENV = 'CONSTRUCT_INTAKE_DAEMON';
24
32
 
25
33
  function inboxDir(cwd) { return join(cwd, 'inbox'); }
26
34
  function pendingDir(cwd) { return join(cwd, '.cx', 'intake', 'pending'); }
27
35
  function deadLetterDir(cwd) { return join(cwd, '.cx', 'intake', 'dead-letter'); }
28
- function heartbeatPath(cwd) { return join(cwd, '.cx', 'runtime', 'intake-daemon.heartbeat'); }
36
+ function heartbeatPath(cwd) { return resolveStatePath(cwd, 'runtime', 'intake-daemon.heartbeat'); }
29
37
 
30
38
  function ensureDir(dir) {
31
39
  try { mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
@@ -60,9 +68,55 @@ function listInboxFiles(cwd) {
60
68
  .filter((full) => isSizeStable(full));
61
69
  }
62
70
 
71
+ function pendingPacketFiles(cwd) {
72
+ const dir = pendingDir(cwd);
73
+ if (!existsSync(dir)) return [];
74
+ try { return readdirSync(dir).filter((name) => name.endsWith('.json')); }
75
+ catch { return []; }
76
+ }
77
+
78
+ function readPacketFile(cwd, name) {
79
+ try { return JSON.parse(readFileSync(join(pendingDir(cwd), name), 'utf8')); }
80
+ catch { return null; }
81
+ }
82
+
83
+ function packetSourcePath(packet) {
84
+ return packet?.intake?.sourcePath || packet?.sourcePath || null;
85
+ }
86
+
87
+ function packetFirstSeenAt(packet) {
88
+ return packet?.firstSeenAt || packet?.createdAt || null;
89
+ }
90
+
91
+ // processInboxFile renames its source into a `.<name>.consumed` marker inside
92
+ // pendingDir once a packet is written, so the original sourcePath disappears
93
+ // the moment ingestion succeeds. That disappearance is expected, not an
94
+ // orphan — the sweep must not dead-letter a packet on the very next tick
95
+ // just because its own consumption already moved the source out of the way.
96
+
97
+ function hasConsumedMarker(cwd, sourcePath) {
98
+ return existsSync(join(pendingDir(cwd), `.${basename(sourcePath)}.consumed`));
99
+ }
100
+
101
+ // A pending packet is keyed by the source file it came from, independent of
102
+ // which ingest path wrote it (flat `sourcePath` here, nested
103
+ // `intake.sourcePath` for packets from lib/intake/prepare.mjs). Finding a
104
+ // live match lets re-ingestion refresh in place instead of writing a sibling
105
+ // packet for the same source.
106
+
107
+ function findExistingPendingBySource(cwd, sourcePath) {
108
+ for (const name of pendingPacketFiles(cwd)) {
109
+ const data = readPacketFile(cwd, name);
110
+ if (data && packetSourcePath(data) === sourcePath) return { name, data };
111
+ }
112
+ return null;
113
+ }
114
+
63
115
  /**
64
116
  * Process one inbox file: classify, persist packet to pending, remove the
65
117
  * inbox source on success, or move to dead-letter on persistent failure.
118
+ * A file whose sourcePath already has a live pending packet refreshes that
119
+ * packet in place rather than writing a second one for the same source.
66
120
  *
67
121
  * Returns one of:
68
122
  * { didWork: true, route: 'pending'|'dead-letter', packetId }
@@ -76,10 +130,11 @@ export async function processInboxFile(filePath, { cwd, classify, now = () => ne
76
130
  try { body = readFileSync(filePath, 'utf8'); }
77
131
  catch (err) { return { didWork: false, reason: `read failed: ${err.message}` }; }
78
132
 
79
- const id = `intake-${now().getTime()}-${Math.random().toString(36).slice(2, 8)}`;
133
+ const existing = findExistingPendingBySource(cwd, filePath);
134
+ const id = existing?.data?.id || `intake-${now().getTime()}-${Math.random().toString(36).slice(2, 8)}`;
80
135
  const packet = {
81
136
  id,
82
- firstSeenAt: new Date(statSync(filePath).mtimeMs).toISOString(),
137
+ firstSeenAt: existing?.data?.firstSeenAt || new Date(statSync(filePath).mtimeMs).toISOString(),
83
138
  sourcePath: filePath,
84
139
  excerpt: body.slice(0, 500),
85
140
  attempts: 0,
@@ -101,13 +156,15 @@ export async function processInboxFile(filePath, { cwd, classify, now = () => ne
101
156
  return { didWork: false, reason: `classify failed, will retry: ${err.message}` };
102
157
  }
103
158
 
104
- const packetPath = join(pendingDir(cwd), `${id}.json`);
159
+ packet.lastSeenAt = now().toISOString();
160
+ const packetName = existing?.name || `${id}.json`;
161
+ const packetPath = join(pendingDir(cwd), packetName);
105
162
  writeFileSync(packetPath, JSON.stringify(packet, null, 2));
106
163
  try {
107
164
  const consumedPath = join(pendingDir(cwd), `.${basename(filePath)}.consumed`);
108
165
  renameSync(filePath, consumedPath);
109
166
  } catch { /* ignore consume failure */ }
110
- return { didWork: true, route: 'pending', packetId: id };
167
+ return { didWork: true, route: 'pending', packetId: id, refreshed: !!existing };
111
168
  }
112
169
 
113
170
  function persistDeadLetter(filePath, packet, reason, cwd) {
@@ -117,6 +174,40 @@ function persistDeadLetter(filePath, packet, reason, cwd) {
117
174
  return { didWork: true, route: 'dead-letter', packetId: packet.id, reason };
118
175
  }
119
176
 
177
+ /**
178
+ * Sweep persisted `.cx/intake/pending/` packets: one whose source file is
179
+ * missing from disk, or whose age exceeds classifyPacket's TTL, moves to
180
+ * `.cx/intake/dead-letter/` with a `deadLetterReason`. Runs every tick
181
+ * alongside the inbox scan so packets written by any ingest path (not just
182
+ * processInboxFile) are covered — the daemon's TTL/dead-letter contract
183
+ * otherwise only reaches packets still in flight through the inbox.
184
+ *
185
+ * Returns the list of { id, reason } packets moved.
186
+ */
187
+ export function sweepPendingPackets(cwd, { now = () => new Date() } = {}) {
188
+ const swept = [];
189
+ for (const name of pendingPacketFiles(cwd)) {
190
+ const packet = readPacketFile(cwd, name);
191
+ if (!packet) continue;
192
+
193
+ const sourcePath = packetSourcePath(packet);
194
+ const sourceMissing = !!sourcePath && !existsSync(sourcePath) && !hasConsumedMarker(cwd, sourcePath);
195
+ const decision = classifyPacket({ firstSeenAt: packetFirstSeenAt(packet), attempts: packet.attempts || 0 });
196
+
197
+ let reason = null;
198
+ if (sourceMissing) reason = 'source-missing';
199
+ else if (decision.route === 'dead-letter') reason = 'ttl-expired';
200
+ if (!reason) continue;
201
+
202
+ ensureDir(deadLetterDir(cwd));
203
+ const dlPath = join(deadLetterDir(cwd), name);
204
+ writeFileSync(dlPath, JSON.stringify({ ...packet, deadLetterReason: reason, deadLetteredAt: now().toISOString() }, null, 2));
205
+ try { unlinkSync(join(pendingDir(cwd), name)); } catch { /* leave the dead-letter copy even if cleanup fails */ }
206
+ swept.push({ id: packet.id || name.replace(/\.json$/, ''), reason });
207
+ }
208
+ return swept;
209
+ }
210
+
120
211
  /**
121
212
  * Build a DaemonRunner for the intake daemon. Caller is responsible for
122
213
  * calling .run() — typically construct intake daemon start.
@@ -132,9 +223,9 @@ export function buildIntakeDaemon({ cwd = process.cwd(), intervalMs = 60_000, cl
132
223
  maxIdleTicks: 6,
133
224
  memoryCapMb: memoryCapMbFor('intake', cwd),
134
225
  async tick() {
226
+ const swept = sweepPendingPackets(cwd);
135
227
  const files = listInboxFiles(cwd);
136
- if (files.length === 0) return { didWork: false };
137
- let workedAny = false;
228
+ let workedAny = swept.length > 0;
138
229
  for (const file of files) {
139
230
  const result = await processInboxFile(file, { cwd, classify: classifyFn });
140
231
  if (result.didWork) workedAny = true;
@@ -4,6 +4,18 @@
4
4
  * Backs team/enterprise modes with the filesystem and Git for state
5
5
  * synchronization and conflict resolution, with no central database server.
6
6
  *
7
+ * git-queue is the zero-dependency DEFAULT kind:'queue' provider
8
+ * (construct-9oi4.7.11); substrate owns the queue contract and Postgres is an
9
+ * optional provider selected through the extension registry.
10
+ *
11
+ * Correctness envelope: single-writer-preferred, eventual consistency.
12
+ * Concurrent claims from multiple agents may result in duplicate claim
13
+ * attempts; the dolt commit history provides reconciliation. A push failure is
14
+ * NOT swallowed or ratified as success: claim() returns a typed disposition
15
+ * ({ durable: false, disposition: 'local-only', reason }) so callers see that
16
+ * the claim is local-only until the push is confirmed in the dolt remote,
17
+ * rather than mistaking it for a durable claim.
18
+ *
7
19
  * Structure:
8
20
  * .cx/team-inbox/
9
21
  * pending/ - JSON files for unclaimed tasks
@@ -34,9 +46,11 @@ function timestamp() {
34
46
  }
35
47
 
36
48
  export class GitIntakeQueue {
37
- constructor({ project, rootDir = process.cwd() } = {}) {
49
+ constructor({ project, rootDir = process.cwd(), _exec } = {}) {
38
50
  this.project = project;
39
51
  this.inboxRoot = path.join(rootDir, '.cx', 'team-inbox');
52
+ // Allow tests to inject a fake exec without monkey-patching the module.
53
+ this._exec = _exec || execSync;
40
54
  this._ensureDirs();
41
55
  }
42
56
 
@@ -48,8 +62,8 @@ export class GitIntakeQueue {
48
62
 
49
63
  _gitAddAndCommit(filePath, message) {
50
64
  try {
51
- execSync(`git add "${filePath}"`, { stdio: 'ignore' });
52
- execSync(`git commit -m "${message}"`, { stdio: 'ignore' });
65
+ this._exec(`git add "${filePath}"`, { stdio: 'ignore' });
66
+ this._exec(`git commit -m "${message}"`, { stdio: 'ignore' });
53
67
  } catch (err) {
54
68
  // It's okay if commit fails because of no changes
55
69
  }
@@ -93,17 +107,24 @@ export class GitIntakeQueue {
93
107
  }
94
108
 
95
109
  async read(id) {
96
- for (const dir of ['pending', 'claimed', 'processed', 'skipped', 'quarantine']) {
97
- // Note: 'claimed' has subdirs, we'd need to search recursively for claimed.
98
- // For simplicity in this implementation, we assume we know where it is or search pending first.
99
- const searchPath = dir === 'claimed'
100
- ? path.join(this.inboxRoot, dir) // needs recursive search
101
- : path.join(this.inboxRoot, dir, `${id}.json`);
102
-
103
- if (dir !== 'claimed' && fs.existsSync(searchPath)) {
110
+ for (const dir of ['pending', 'processed', 'skipped', 'quarantine']) {
111
+ const searchPath = path.join(this.inboxRoot, dir, `${id}.json`);
112
+ if (fs.existsSync(searchPath)) {
104
113
  return JSON.parse(fs.readFileSync(searchPath, 'utf8'));
105
114
  }
106
115
  }
116
+
117
+ // 'claimed' uses per-worker subdirectories — search recursively.
118
+ const claimedBase = path.join(this.inboxRoot, 'claimed');
119
+ if (fs.existsSync(claimedBase)) {
120
+ for (const worker of fs.readdirSync(claimedBase)) {
121
+ const candidate = path.join(claimedBase, worker, `${id}.json`);
122
+ if (fs.existsSync(candidate)) {
123
+ return JSON.parse(fs.readFileSync(candidate, 'utf8'));
124
+ }
125
+ }
126
+ }
127
+
107
128
  return null;
108
129
  }
109
130
 
@@ -111,8 +132,13 @@ export class GitIntakeQueue {
111
132
  if (!claimedBy) throw new Error('claim: claimedBy is required');
112
133
 
113
134
  // Pull latest to minimize conflicts
114
-
115
- try { execSync('git pull --rebase', { stdio: 'ignore' }); } catch (e) {}
135
+ try {
136
+ this._exec('git pull --rebase', { stdio: 'ignore' });
137
+ } catch (pullErr) {
138
+ // Pull failure is non-fatal — we proceed with local state.
139
+ // Log a reconciliation record so operators can detect divergence.
140
+ console.warn(`[git-queue] pull failed before claim (proceeding with local state): ${pullErr.message}`);
141
+ }
116
142
 
117
143
  const pendingDir = path.join(this.inboxRoot, 'pending');
118
144
  const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.json')).sort();
@@ -125,31 +151,34 @@ export class GitIntakeQueue {
125
151
  fs.mkdirSync(workerDir, { recursive: true });
126
152
  const claimedPath = path.join(workerDir, fileName);
127
153
 
128
- try {
129
- // Atomic move on filesystem
130
- fs.renameSync(pendingPath, claimedPath);
131
-
132
- // Update metadata
133
- const data = JSON.parse(fs.readFileSync(claimedPath, 'utf8'));
134
- data.status = 'claimed';
135
- data.claimedBy = claimedBy;
136
- data.claimedAt = new Date().toISOString();
137
- fs.writeFileSync(claimedPath, JSON.stringify(data, null, 2));
138
-
139
- // Commit and push the claim so peers see it
140
-
141
- execSync(`git add .cx/team-inbox/pending/${fileName} .cx/team-inbox/claimed/${claimedBy}/${fileName}`, { stdio: 'ignore' });
142
- execSync(`git commit -m "Claim task ${data.id} by ${claimedBy}"`, { stdio: 'ignore' });
143
- execSync('git push', { stdio: 'ignore' });
144
-
145
- return data;
146
- } catch (err) {
147
- // A push failure means another worker claimed it first or Git is out of
148
- // sync; the local move is left for the next 'git pull --rebase' to reconcile.
154
+ // Atomic move on filesystem
155
+ fs.renameSync(pendingPath, claimedPath);
156
+
157
+ // Update metadata
158
+ const data = JSON.parse(fs.readFileSync(claimedPath, 'utf8'));
159
+ data.status = 'claimed';
160
+ data.claimedBy = claimedBy;
161
+ data.claimedAt = new Date().toISOString();
162
+ fs.writeFileSync(claimedPath, JSON.stringify(data, null, 2));
149
163
 
150
- console.error(`Failed to claim ${fileName}: ${err.message}`);
151
- return null;
164
+ // Commit and push the claim so peers see it. A push failure is reported as a
165
+ // typed non-durable disposition on the returned claim rather than swallowed:
166
+ // the local move stands for the next 'git pull --rebase' to reconcile, but
167
+ // the caller is told the claim is local-only, not durable.
168
+ data.durable = true;
169
+ data.disposition = 'pushed';
170
+ try {
171
+ this._exec(`git add .cx/team-inbox/pending/${fileName} .cx/team-inbox/claimed/${claimedBy}/${fileName}`, { stdio: 'ignore' });
172
+ this._exec(`git commit -m "Claim task ${data.id} by ${claimedBy}"`, { stdio: 'ignore' });
173
+ this._exec('git push', { stdio: 'ignore' });
174
+ } catch (pushErr) {
175
+ data.durable = false;
176
+ data.disposition = 'local-only';
177
+ data.dispositionReason = pushErr.message;
178
+ console.warn(`[git-queue] push failed after claim of ${data.id} by ${claimedBy} (claim is local-only until reconciled): ${pushErr.message}`);
152
179
  }
180
+
181
+ return data;
153
182
  }
154
183
 
155
184
  async markProcessed(id, { processedBy = 'unknown', notes = '' } = {}) {
@@ -187,10 +216,53 @@ export class GitIntakeQueue {
187
216
  fs.writeFileSync(processedPath, JSON.stringify(data, null, 2));
188
217
 
189
218
  this._gitAddAndCommit(this.inboxRoot, `Mark task ${id} as processed`);
190
- try { execSync('git push', { stdio: 'ignore' }); } catch (e) {}
219
+ try { this._exec('git push', { stdio: 'ignore' }); } catch (e) {}
191
220
 
192
221
  return { id };
193
222
  }
194
223
 
195
- // markSkipped and reopen would follow similar logic...
224
+ /**
225
+ * Mark an issue as skipped in local state WITHOUT a push.
226
+ *
227
+ * Used for graceful degradation when a push fails or when the caller wants
228
+ * to skip without touching the remote. The skip is advisory — it survives
229
+ * only as a local file move until the next reconciliation pull.
230
+ *
231
+ * @param {string} issueId
232
+ * @returns {{ id: string, skippedAt: string }}
233
+ */
234
+ async markSkipped(issueId) {
235
+ if (!issueId) throw new Error('markSkipped: issueId is required');
236
+
237
+ // Locate the file across pending and claimed directories.
238
+ let foundPath = null;
239
+
240
+ const pendingCandidate = path.join(this.inboxRoot, 'pending', `${issueId}.json`);
241
+ if (fs.existsSync(pendingCandidate)) {
242
+ foundPath = pendingCandidate;
243
+ } else {
244
+ const claimedBase = path.join(this.inboxRoot, 'claimed');
245
+ if (fs.existsSync(claimedBase)) {
246
+ for (const worker of fs.readdirSync(claimedBase)) {
247
+ const candidate = path.join(claimedBase, worker, `${issueId}.json`);
248
+ if (fs.existsSync(candidate)) { foundPath = candidate; break; }
249
+ }
250
+ }
251
+ }
252
+
253
+ if (!foundPath) throw new Error(`markSkipped: no entry ${issueId} found`);
254
+
255
+ const data = JSON.parse(fs.readFileSync(foundPath, 'utf8'));
256
+ data.status = 'skipped';
257
+ data.skippedAt = new Date().toISOString();
258
+
259
+ const skippedPath = path.join(this.inboxRoot, 'skipped', `${issueId}.json`);
260
+ fs.renameSync(foundPath, skippedPath);
261
+ fs.writeFileSync(skippedPath, JSON.stringify(data, null, 2));
262
+
263
+ // No push — this is a local-only operation (graceful degradation).
264
+ return { id: issueId, skippedAt: data.skippedAt };
265
+ }
266
+
267
+ // reopen would follow similar logic...
196
268
  }
@@ -123,6 +123,8 @@ export async function prepareIntakeForIngestedFile({
123
123
  const tagSuggestions = suggestTags(triage, related, vocab);
124
124
 
125
125
  const baseEntry = {
126
+ // OWASP LLM01: externally-ingested document bodies are untrusted data, not instructions
127
+ trust: 'untrusted',
126
128
  intake: {
127
129
  sourcePath: ingestedFile.sourcePath,
128
130
  outputPath: ingestedFile.outputPath,