@geraldmaron/construct 1.4.2 → 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 (519) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1027 -64
  4. package/examples/distribution/sources/deck-one-pager.md +1 -1
  5. package/lib/acp/server.mjs +21 -7
  6. package/lib/adapters-sync.mjs +2 -1
  7. package/lib/audit-trail.mjs +185 -2
  8. package/lib/beads/auto-close.mjs +2 -1
  9. package/lib/beads/drift.mjs +2 -1
  10. package/lib/bridges/copilot-proxy.mjs +20 -5
  11. package/lib/certification/skill-inventory.mjs +10 -1
  12. package/lib/cli/approvals.mjs +147 -0
  13. package/lib/cli-commands.mjs +105 -14
  14. package/lib/comment-lint.mjs +127 -8
  15. package/lib/config/schema.mjs +6 -29
  16. package/lib/config/source-target-registry.mjs +70 -0
  17. package/lib/config/source-targets.mjs +179 -205
  18. package/lib/contracts/coverage.mjs +77 -0
  19. package/lib/contracts/validate.mjs +102 -13
  20. package/lib/contracts/violation-log.mjs +50 -4
  21. package/lib/db/migrate.mjs +69 -0
  22. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  23. package/lib/db/migrations/002_queue_provider.sql +50 -0
  24. package/lib/db/migrations/003_worker_registry.sql +17 -0
  25. package/lib/db/migrations/004_trace_events.sql +16 -0
  26. package/lib/db/migrations/005_shared_memory.sql +15 -0
  27. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  28. package/lib/decisions/enforced-baseline.json +0 -2
  29. package/lib/decisions/registry.mjs +3 -2
  30. package/lib/deployment/parity-contract.mjs +1 -1
  31. package/lib/deployment-mode.mjs +78 -2
  32. package/lib/diagram-export.mjs +10 -1
  33. package/lib/distill.mjs +5 -2
  34. package/lib/docs-verify.mjs +2 -1
  35. package/lib/doctor/cli.mjs +1 -0
  36. package/lib/doctor/command-on-path.mjs +24 -0
  37. package/lib/doctor/diagnosis.mjs +89 -0
  38. package/lib/doctor/embedding-health.mjs +50 -0
  39. package/lib/doctor/engine-health.mjs +93 -0
  40. package/lib/doctor/graph-validate.mjs +47 -0
  41. package/lib/doctor/index.mjs +18 -4
  42. package/lib/doctor/sidecar-providers.mjs +56 -0
  43. package/lib/doctor/watchers/consistency.mjs +93 -1
  44. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  45. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  46. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  47. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  48. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  49. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  50. package/lib/document-export.mjs +52 -27
  51. package/lib/document-extract/docling-client.mjs +9 -5
  52. package/lib/document-extract/docling-sidecar.py +30 -0
  53. package/lib/document-extract.mjs +1 -1
  54. package/lib/document-ingest.mjs +9 -0
  55. package/lib/embed/approval-queue.mjs +184 -88
  56. package/lib/embed/authority-guard.mjs +65 -2
  57. package/lib/embed/capability-jobs.mjs +365 -0
  58. package/lib/embed/capability-lifecycle.mjs +219 -0
  59. package/lib/embed/capability-loader.mjs +303 -0
  60. package/lib/embed/capability-runtime.mjs +58 -0
  61. package/lib/embed/cli.mjs +185 -8
  62. package/lib/embed/config.mjs +4 -0
  63. package/lib/embed/daemon.mjs +125 -6
  64. package/lib/embed/demand-fetch.mjs +190 -54
  65. package/lib/embed/inbox.mjs +12 -10
  66. package/lib/embed/presets/ops-triage.mjs +236 -0
  67. package/lib/embed/presets/pm-feedback.mjs +341 -0
  68. package/lib/embed/presets/tpm.mjs +401 -0
  69. package/lib/embed/providers/jira.mjs +72 -1
  70. package/lib/embed/providers/registry.mjs +140 -33
  71. package/lib/embedded-contract/capability.mjs +4 -0
  72. package/lib/embedded-contract/execution.mjs +1 -1
  73. package/lib/embedded-contract/model-resolve.mjs +53 -3
  74. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  75. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  76. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  77. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  78. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  79. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  80. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  81. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  82. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  83. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  84. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  85. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  86. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  88. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  90. package/lib/env-config.mjs +50 -9
  91. package/lib/extensions/index.mjs +27 -0
  92. package/lib/extensions/loader.mjs +120 -0
  93. package/lib/extensions/manifest-schema.mjs +141 -0
  94. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  95. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  96. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  97. package/lib/extensions/manifests/directory.manifest.json +12 -0
  98. package/lib/extensions/manifests/docling.manifest.json +37 -0
  99. package/lib/extensions/manifests/echo.manifest.json +8 -0
  100. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  101. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  102. package/lib/extensions/manifests/github.manifest.json +52 -0
  103. package/lib/extensions/manifests/linear.manifest.json +50 -0
  104. package/lib/extensions/manifests/local.manifest.json +12 -0
  105. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  106. package/lib/extensions/manifests/openai.manifest.json +12 -0
  107. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  108. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  109. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  110. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  113. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  114. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  115. package/lib/extensions/manifests/slack.manifest.json +58 -0
  116. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  117. package/lib/extensions/validate.mjs +175 -0
  118. package/lib/features.mjs +13 -9
  119. package/lib/flows/checkpoint.mjs +184 -0
  120. package/lib/flows/constants.mjs +27 -0
  121. package/lib/flows/define.mjs +146 -0
  122. package/lib/flows/engine.mjs +249 -0
  123. package/lib/flows/errors.mjs +19 -0
  124. package/lib/flows/index.mjs +27 -0
  125. package/lib/flows/joins.mjs +18 -0
  126. package/lib/flows/schema.mjs +90 -0
  127. package/lib/flows/state.mjs +31 -0
  128. package/lib/frameworks/loader.mjs +99 -0
  129. package/lib/frameworks/schema.mjs +157 -0
  130. package/lib/graph/build-from-corpus.mjs +67 -0
  131. package/lib/graph/build-from-embed.mjs +82 -0
  132. package/lib/graph/build-from-registry.mjs +164 -3
  133. package/lib/graph/cli.mjs +456 -12
  134. package/lib/graph/gap-queries.mjs +156 -0
  135. package/lib/graph/gaps.mjs +41 -0
  136. package/lib/graph/impacted.mjs +129 -0
  137. package/lib/graph/runtime-evidence.mjs +177 -0
  138. package/lib/graph/security-coverage.mjs +113 -0
  139. package/lib/graph/staleness.mjs +241 -10
  140. package/lib/graph/store.mjs +24 -7
  141. package/lib/graph/validate.mjs +161 -0
  142. package/lib/headhunt.mjs +9 -8
  143. package/lib/health-check.mjs +15 -7
  144. package/lib/hook-health.mjs +1 -1
  145. package/lib/hooks/agent-tracker.mjs +10 -4
  146. package/lib/hooks/edit-guard.mjs +3 -3
  147. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  148. package/lib/hooks/guard-bash.mjs +41 -15
  149. package/lib/hooks/mcp-health-check.mjs +11 -4
  150. package/lib/hooks/model-fallback.mjs +50 -6
  151. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  152. package/lib/hooks/pre-compact.mjs +181 -173
  153. package/lib/hooks/rule-verifier.mjs +2 -1
  154. package/lib/hooks/session-reflect.mjs +2 -1
  155. package/lib/hooks/session-start.mjs +3 -3
  156. package/lib/host-capabilities.mjs +13 -2
  157. package/lib/host-disposition.mjs +19 -7
  158. package/lib/identity.mjs +92 -0
  159. package/lib/ingest/degraded-extract.mjs +96 -0
  160. package/lib/ingest/docling-remote.mjs +2 -1
  161. package/lib/ingest/provider-extract.mjs +7 -19
  162. package/lib/ingest/sidecar-providers.mjs +196 -0
  163. package/lib/ingest-tooling.mjs +11 -5
  164. package/lib/init-unified.mjs +55 -38
  165. package/lib/init-update.mjs +2 -1
  166. package/lib/install/legacy-global-cleanup.mjs +25 -0
  167. package/lib/intake/daemon.mjs +99 -8
  168. package/lib/intake/git-queue.mjs +110 -38
  169. package/lib/intake/queue-registry.mjs +50 -0
  170. package/lib/intake/queue.mjs +133 -17
  171. package/lib/intake/session-prelude.mjs +57 -8
  172. package/lib/integrations/intake-integrations.mjs +61 -111
  173. package/lib/intent-classifier.mjs +43 -5
  174. package/lib/libreoffice-export.mjs +8 -8
  175. package/lib/logging/rotate.mjs +5 -4
  176. package/lib/mcp/broker.mjs +332 -17
  177. package/lib/mcp/denied-store.mjs +95 -0
  178. package/lib/mcp/destructive-gate.mjs +30 -0
  179. package/lib/mcp/dispatch-envelope.mjs +199 -0
  180. package/lib/mcp/memory-bridge.mjs +2 -1
  181. package/lib/mcp/server.mjs +154 -1411
  182. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  183. package/lib/mcp/tool-definitions-project.mjs +271 -0
  184. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  185. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  186. package/lib/mcp/tool-definitions.mjs +25 -0
  187. package/lib/mcp/tool-registry.mjs +107 -0
  188. package/lib/mcp/tool-safety.mjs +1 -0
  189. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  190. package/lib/mcp/tools/orchestration-run.mjs +165 -25
  191. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  192. package/lib/mcp/tools/provider-write.mjs +187 -0
  193. package/lib/mcp/tools/scope.mjs +0 -3
  194. package/lib/mcp/tools/skills.mjs +1 -1
  195. package/lib/mcp/tools/storage.mjs +0 -8
  196. package/lib/mcp/transport/auth.mjs +238 -0
  197. package/lib/mcp/transport/http.mjs +136 -0
  198. package/lib/mcp/transport/mode.mjs +31 -0
  199. package/lib/mcp/transport/stdio.mjs +22 -0
  200. package/lib/mcp-catalog.json +4 -4
  201. package/lib/mcp-manager.mjs +34 -23
  202. package/lib/mcp-platform-config.mjs +31 -7
  203. package/lib/mode-capabilities.mjs +109 -0
  204. package/lib/model-router.mjs +42 -9
  205. package/lib/models/catalog.mjs +40 -1
  206. package/lib/net-guard.mjs +247 -0
  207. package/lib/observation-store.mjs +6 -2
  208. package/lib/ollama/provision-context.mjs +2 -1
  209. package/lib/opencode-config.mjs +22 -3
  210. package/lib/opencode-runtime-plugin.mjs +120 -2
  211. package/lib/oracle/actions.mjs +204 -10
  212. package/lib/oracle/cli.mjs +24 -3
  213. package/lib/oracle/dispatch.mjs +2 -1
  214. package/lib/oracle/execute.mjs +2 -2
  215. package/lib/oracle/gaps.mjs +3 -3
  216. package/lib/oracle/heartbeat.mjs +53 -0
  217. package/lib/oracle/read-model.mjs +69 -13
  218. package/lib/oracle/reconcile.mjs +3 -46
  219. package/lib/oracle/routing.mjs +37 -31
  220. package/lib/oracle/synthesize.mjs +1 -1
  221. package/lib/orchestration/classification.mjs +434 -0
  222. package/lib/orchestration/delegation-flow.mjs +132 -0
  223. package/lib/orchestration/flow-selection.mjs +418 -0
  224. package/lib/orchestration/gates.mjs +244 -0
  225. package/lib/orchestration/host-sampling.mjs +121 -0
  226. package/lib/orchestration/policy-constants.mjs +48 -0
  227. package/lib/orchestration/provider-outcome.mjs +197 -0
  228. package/lib/orchestration/readiness.mjs +114 -13
  229. package/lib/orchestration/run-store-postgres.mjs +32 -26
  230. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  231. package/lib/orchestration/run-store.mjs +25 -12
  232. package/lib/orchestration/runtime.mjs +446 -39
  233. package/lib/orchestration/store.mjs +87 -12
  234. package/lib/orchestration/trace-store.mjs +125 -0
  235. package/lib/orchestration/web-capability.mjs +3 -2
  236. package/lib/orchestration/worker-runtime.mjs +162 -0
  237. package/lib/orchestration/worker.mjs +528 -89
  238. package/lib/orchestration-policy.mjs +67 -1111
  239. package/lib/packs/cli.mjs +144 -0
  240. package/lib/packs/core-pack.mjs +112 -0
  241. package/lib/packs/enablement.mjs +187 -0
  242. package/lib/packs/index.mjs +23 -0
  243. package/lib/packs/loader.mjs +176 -0
  244. package/lib/packs/manifest-schema.mjs +58 -0
  245. package/lib/packs/prompts.mjs +120 -0
  246. package/lib/packs/validate.mjs +208 -0
  247. package/lib/plugin-registry.mjs +4 -5
  248. package/lib/policy/audit-gate.mjs +42 -0
  249. package/lib/policy/consumption-budget.mjs +149 -0
  250. package/lib/policy/engine.mjs +81 -6
  251. package/lib/policy/role-authority.mjs +89 -0
  252. package/lib/prompt-composer.js +19 -3
  253. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  254. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  255. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  256. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  257. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  258. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  259. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  260. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  261. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  262. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  263. package/lib/providers/contract.mjs +207 -0
  264. package/lib/providers/credential-bootstrap.mjs +7 -4
  265. package/lib/providers/credential-sources.mjs +14 -2
  266. package/lib/providers/directory/index.mjs +261 -0
  267. package/lib/providers/feedback/index.mjs +392 -0
  268. package/lib/providers/filter-audit.mjs +68 -0
  269. package/lib/providers/filter-schema.mjs +54 -0
  270. package/lib/providers/github/index.mjs +31 -0
  271. package/lib/providers/instance-config.mjs +215 -0
  272. package/lib/providers/op-locate.mjs +96 -0
  273. package/lib/providers/op-run.mjs +64 -9
  274. package/lib/providers/registry.mjs +26 -3
  275. package/lib/providers/secret-audit-wiring.mjs +6 -0
  276. package/lib/providers/secret-resolver.mjs +240 -23
  277. package/lib/publish-template.mjs +6 -3
  278. package/lib/publish-tooling.mjs +4 -0
  279. package/lib/publish.mjs +32 -4
  280. package/lib/queue/pg-queue.mjs +395 -0
  281. package/lib/reflect.mjs +2 -1
  282. package/lib/registry/assemble.mjs +28 -2
  283. package/lib/registry/consolidation.mjs +8 -1
  284. package/lib/registry/custom-scaffold.mjs +200 -0
  285. package/lib/registry/custom-schema.mjs +137 -0
  286. package/lib/registry/loader.mjs +8 -3
  287. package/lib/registry/manifests/format-engines.default.json +15 -0
  288. package/lib/registry/manifests/surface-map.default.json +46 -0
  289. package/lib/registry/retired-paths.mjs +1 -0
  290. package/lib/registry/surface-map.mjs +100 -50
  291. package/lib/render-visual-check.mjs +240 -0
  292. package/lib/resources/budget.mjs +40 -17
  293. package/lib/roles/flavor-bindings.mjs +36 -14
  294. package/lib/roles/gateway.mjs +15 -8
  295. package/lib/roots.mjs +107 -0
  296. package/lib/runtime/uv-bootstrap.mjs +32 -6
  297. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  298. package/lib/runtime-env.mjs +5 -2
  299. package/lib/scheduler/index.mjs +8 -20
  300. package/lib/schema-infer.mjs +31 -2
  301. package/lib/scopes/lifecycle.mjs +29 -25
  302. package/lib/scopes/loader.mjs +49 -12
  303. package/lib/scopes/teams.mjs +1 -1
  304. package/lib/security/ingest-boundary.mjs +80 -0
  305. package/lib/security/recall-wrapper.mjs +99 -0
  306. package/lib/security/trust.mjs +132 -0
  307. package/lib/service-manager.mjs +38 -19
  308. package/lib/setup.mjs +41 -23
  309. package/lib/skills-apply.mjs +4 -2
  310. package/lib/specialists/postconditions.mjs +2 -2
  311. package/lib/state-root.mjs +147 -0
  312. package/lib/status.mjs +597 -6
  313. package/lib/storage/admin.mjs +77 -5
  314. package/lib/storage/backend-registry.mjs +73 -0
  315. package/lib/storage/backend.mjs +63 -9
  316. package/lib/storage/embeddings-openai.mjs +12 -2
  317. package/lib/storage/hybrid-query.mjs +11 -3
  318. package/lib/storage/retrieval-hardening.mjs +384 -0
  319. package/lib/storage/shared-memory.mjs +158 -0
  320. package/lib/storage/vector-client.mjs +69 -2
  321. package/lib/team/health.mjs +72 -0
  322. package/lib/telemetry/backends/local.mjs +9 -3
  323. package/lib/telemetry/client.mjs +3 -7
  324. package/lib/template-registry.mjs +10 -12
  325. package/lib/tenant/context.mjs +82 -0
  326. package/lib/tenant/isolation.mjs +129 -0
  327. package/lib/test-corpus-inventory.mjs +103 -2
  328. package/lib/uninstall/uninstall.mjs +78 -12
  329. package/lib/validator.mjs +4 -6
  330. package/lib/worker/entrypoint.mjs +2 -1
  331. package/lib/worker/run.mjs +2 -2
  332. package/lib/worker/trace.mjs +5 -11
  333. package/lib/workflow-state.mjs +52 -8
  334. package/lib/workflows/liveness.mjs +203 -0
  335. package/lib/workflows/loader.mjs +177 -0
  336. package/lib/workflows/manifest-schema.mjs +71 -0
  337. package/lib/workflows/surface-parity.mjs +118 -0
  338. package/lib/workflows/validate.mjs +127 -0
  339. package/lib/writes/control-plane.mjs +140 -0
  340. package/lib/writes/envelope.mjs +206 -0
  341. package/lib/writes/sent-log.mjs +86 -0
  342. package/lib/writes/write-intent.mjs +109 -0
  343. package/package.json +16 -8
  344. package/personas/construct.md +12 -3
  345. package/registry/capabilities.json +143 -36
  346. package/rules/common/comments.md +1 -0
  347. package/schemas/project-config.schema.json +0 -12
  348. package/schemas/unified-registry.schema.json +2 -4
  349. package/scripts/sync-specialists.mjs +284 -81
  350. package/skills/docs/adr-workflow.md +1 -1
  351. package/skills/docs/codebase-research-workflow.md +3 -3
  352. package/skills/docs/prd-workflow.md +5 -6
  353. package/skills/docs/runbook-workflow.md +2 -2
  354. package/skills/docs/user-research-workflow.md +3 -3
  355. package/skills/operating/fleet-health-routing.md +77 -0
  356. package/skills/roles/ai-engineer.md +1 -1
  357. package/skills/roles/architect.md +0 -1
  358. package/skills/roles/business-strategist.md +1 -1
  359. package/skills/roles/data-analyst.telemetry.md +1 -1
  360. package/skills/roles/data-engineer.md +1 -1
  361. package/skills/roles/data-engineer.pipeline.md +1 -1
  362. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  363. package/skills/roles/data-engineer.warehouse.md +1 -1
  364. package/skills/roles/designer.accessibility.md +1 -1
  365. package/skills/roles/designer.md +0 -1
  366. package/skills/roles/devil-advocate.md +1 -1
  367. package/skills/roles/docs-keeper.md +1 -1
  368. package/skills/roles/evaluator.md +1 -1
  369. package/skills/roles/explorer.md +1 -1
  370. package/skills/roles/platform-engineer.md +1 -1
  371. package/skills/roles/qa.ai-eval.md +1 -2
  372. package/skills/roles/qa.api-contract.md +0 -1
  373. package/skills/roles/qa.data-pipeline.md +0 -1
  374. package/skills/roles/qa.md +0 -1
  375. package/skills/roles/qa.web-ui.md +0 -1
  376. package/skills/roles/release-manager.md +1 -1
  377. package/skills/roles/researcher.md +0 -2
  378. package/skills/roles/reviewer.md +0 -3
  379. package/skills/roles/security.ai.md +1 -1
  380. package/skills/roles/security.legal-compliance.md +1 -1
  381. package/skills/roles/security.md +0 -1
  382. package/skills/roles/security.privacy.md +0 -1
  383. package/skills/roles/security.supply-chain.md +1 -1
  384. package/skills/roles/sre.md +1 -1
  385. package/skills/roles/test-automation.md +1 -1
  386. package/skills/roles/trace-reviewer.md +1 -1
  387. package/skills/roles/ux-researcher.md +1 -1
  388. package/specialists/artifact-manifest.json +36 -36
  389. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  390. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  391. package/specialists/org/contracts/any-to-debugger.json +7 -1
  392. package/specialists/org/contracts/any-to-designer.json +7 -1
  393. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  394. package/specialists/org/contracts/any-to-explorer.json +13 -4
  395. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  396. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  397. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  398. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  399. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  400. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  401. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  402. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  403. package/specialists/org/contracts/architect-to-operations.json +17 -4
  404. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  405. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  406. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  407. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  408. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  409. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  410. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  411. package/specialists/org/contracts/operations-to-user.json +53 -0
  412. package/specialists/org/contracts/operations-triage-output.json +53 -0
  413. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  414. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  415. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  416. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  417. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  418. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  419. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  420. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  421. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  422. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  423. package/specialists/org/contracts/user-to-construct.json +11 -4
  424. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  425. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  426. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  427. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  428. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  429. package/specialists/org/groups/engineering-group.json +2 -6
  430. package/specialists/org/groups/governance-group.json +2 -3
  431. package/specialists/org/groups/operations-group.json +5 -8
  432. package/specialists/org/groups/product-group.json +4 -8
  433. package/specialists/org/groups/quality-group.json +3 -7
  434. package/specialists/org/groups/strategy-group.json +6 -9
  435. package/specialists/org/models.json.example +8 -0
  436. package/specialists/org/scopes/creative.json +6 -1
  437. package/specialists/org/scopes/operations.json +7 -1
  438. package/specialists/org/scopes/research.json +6 -1
  439. package/specialists/org/scopes/rnd.json +7 -1
  440. package/specialists/org/specialists/cx-architect.json +27 -8
  441. package/specialists/org/specialists/cx-designer.json +22 -8
  442. package/specialists/org/specialists/cx-engineer.json +71 -7
  443. package/specialists/org/specialists/cx-operations.json +89 -15
  444. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  445. package/specialists/org/specialists/cx-product-manager.json +28 -9
  446. package/specialists/org/specialists/cx-qa.json +16 -6
  447. package/specialists/org/specialists/cx-researcher.json +40 -7
  448. package/specialists/org/specialists/cx-reviewer.json +61 -11
  449. package/specialists/org/specialists/cx-security.json +30 -10
  450. package/specialists/org/teams/design-team.json +3 -4
  451. package/specialists/org/teams/engineering-team.json +3 -10
  452. package/specialists/org/teams/governance-team.json +3 -5
  453. package/specialists/org/teams/operations-team.json +6 -12
  454. package/specialists/org/teams/product-management-team.json +2 -3
  455. package/specialists/org/teams/quality-team.json +4 -12
  456. package/specialists/org/teams/research-team.json +3 -3
  457. package/specialists/org/teams/strategy-team.json +7 -14
  458. package/specialists/prompts/cx-architect.md +20 -1
  459. package/specialists/prompts/cx-data-analyst.md +1 -1
  460. package/specialists/prompts/cx-designer.md +12 -4
  461. package/specialists/prompts/cx-engineer.md +15 -1
  462. package/specialists/prompts/cx-operations.md +14 -2
  463. package/specialists/prompts/cx-orchestrator.md +9 -5
  464. package/specialists/prompts/cx-product-manager.md +5 -1
  465. package/specialists/prompts/cx-qa.md +6 -2
  466. package/specialists/prompts/cx-researcher.md +25 -30
  467. package/specialists/prompts/cx-reviewer.md +19 -1
  468. package/specialists/prompts/cx-security.md +5 -1
  469. package/templates/distribution/construct-brand.typ +123 -59
  470. package/templates/distribution/construct-decision.typ +6 -3
  471. package/templates/distribution/construct-pdf.typ +11 -4
  472. package/templates/distribution/construct-prd.typ +6 -3
  473. package/templates/distribution/construct-research.typ +7 -4
  474. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  475. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  476. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  477. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  478. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  479. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  480. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  481. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  482. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  483. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  484. package/specialists/org/specialists/cx-accessibility.json +0 -69
  485. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  486. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  487. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  488. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  489. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  490. package/specialists/org/specialists/cx-evaluator.json +0 -69
  491. package/specialists/org/specialists/cx-explorer.json +0 -69
  492. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  493. package/specialists/org/specialists/cx-oracle.json +0 -46
  494. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  495. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  496. package/specialists/org/specialists/cx-release-manager.json +0 -77
  497. package/specialists/org/specialists/cx-sre.json +0 -94
  498. package/specialists/org/specialists/cx-test-automation.json +0 -69
  499. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  500. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  501. package/specialists/org/teams/accessibility-team.json +0 -39
  502. package/specialists/org/teams/ux-research-team.json +0 -39
  503. package/specialists/prompts/cx-accessibility.md +0 -55
  504. package/specialists/prompts/cx-ai-engineer.md +0 -124
  505. package/specialists/prompts/cx-business-strategist.md +0 -58
  506. package/specialists/prompts/cx-data-engineer.md +0 -55
  507. package/specialists/prompts/cx-devil-advocate.md +0 -59
  508. package/specialists/prompts/cx-docs-keeper.md +0 -164
  509. package/specialists/prompts/cx-evaluator.md +0 -49
  510. package/specialists/prompts/cx-explorer.md +0 -71
  511. package/specialists/prompts/cx-legal-compliance.md +0 -58
  512. package/specialists/prompts/cx-oracle.md +0 -98
  513. package/specialists/prompts/cx-platform-engineer.md +0 -97
  514. package/specialists/prompts/cx-rd-lead.md +0 -59
  515. package/specialists/prompts/cx-release-manager.md +0 -54
  516. package/specialists/prompts/cx-sre.md +0 -111
  517. package/specialists/prompts/cx-test-automation.md +0 -55
  518. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  519. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -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
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * lib/intake/queue-registry.mjs — kind:'queue' provider resolution.
3
+ *
4
+ * Substrate owns the queue/run-store CONTRACT; git-queue is the zero-dependency
5
+ * DEFAULT provider and Postgres is an OPTIONAL provider. Selection is explicit:
6
+ * a concrete queue backend is chosen by resolving an installed kind:'queue'
7
+ * manifest through the extension registry (construct-9oi4.7.11, supersedes the
8
+ * ADR-0051 git-vs-postgres framing). No silent postgres->git alias exists — a
9
+ * caller that names a backend with no registered kind:'queue' provider gets a
10
+ * typed error, not a swallowed downgrade.
11
+ *
12
+ * Resolution surface:
13
+ * listQueueProviders({ rootDir, env }) → manifest[] of kind:'queue', merged
14
+ * across builtin/user/project tiers (project wins).
15
+ * findQueueProvider(backend, { rootDir, env }) → manifest | null for a backend id.
16
+ */
17
+
18
+ import { loadManifestsFromDir, mergeManifests, resolveManifestDirs } from '../extensions/loader.mjs';
19
+
20
+ /**
21
+ * The queue backend ids that are always resolvable without a registered
22
+ * manifest. git is the default provider; filesystem is the solo-mode default
23
+ * provider. Both ship in-tree, so they never require an extension manifest.
24
+ */
25
+ export const BUILTIN_QUEUE_BACKENDS = ['git', 'filesystem'];
26
+
27
+ // Load every kind:'queue' manifest across the three tiers and merge by id so a
28
+ // project- or user-level Postgres queue provider overrides a builtin of the
29
+ // same id. Only manifests declaring kind:'queue' are returned.
30
+
31
+ export function listQueueProviders({ rootDir = process.cwd(), env = process.env } = {}) {
32
+ const homeDir = env?.HOME || env?.USERPROFILE || undefined;
33
+ const dirs = resolveManifestDirs({ rootDir, homeDir });
34
+ const builtin = loadManifestsFromDir(dirs.builtin).manifests;
35
+ const user = loadManifestsFromDir(dirs.user).manifests;
36
+ const project = loadManifestsFromDir(dirs.project).manifests;
37
+ return mergeManifests(builtin, user, project).filter((m) => m.kind === 'queue');
38
+ }
39
+
40
+ /**
41
+ * findQueueProvider(backend, opts)
42
+ *
43
+ * Returns the kind:'queue' manifest whose id matches `backend`, or null when no
44
+ * such provider is registered. Callers use the null result to distinguish an
45
+ * unregistered backend (a hard, explicit error) from a resolvable one.
46
+ */
47
+ export function findQueueProvider(backend, { rootDir = process.cwd(), env = process.env } = {}) {
48
+ if (!backend) return null;
49
+ return listQueueProviders({ rootDir, env }).find((m) => m.id === backend) || null;
50
+ }
@@ -3,14 +3,20 @@
3
3
  *
4
4
  * The IntakeQueue contract carries the R&D intake packets from the embed
5
5
  * daemon (writer) to the agent in the user's editor (reader/processor).
6
- * Solo mode is backed by the filesystem under `.cx/intake/`; team and
7
- * enterprise modes are backed by Postgres with row-locked worker claims.
8
- * Callers never branch on backend they ask for a queue via
9
- * `createIntakeQueue(rootDir, env)` and use the same six methods.
6
+ * Substrate owns the queue CONTRACT; git-queue is the zero-dependency default
7
+ * kind:'queue' provider and Postgres is an optional kind:'queue' provider
8
+ * selected through the extension registry (construct-9oi4.7.11). Solo mode
9
+ * resolves to the filesystem provider under `.cx/intake/`; team and enterprise
10
+ * default to the Postgres provider. Callers never branch on backend — they ask for a
11
+ * queue via `createIntakeQueue(rootDir, env)` and use the same six methods.
12
+ * The returned instance also carries `.tenantId` (ADR-0057/A7: resolved once
13
+ * from CONSTRUCT_TENANT_ID/config, 'local' default outside enterprise mode)
14
+ * so a caller can stamp it onto entries without re-resolving it.
10
15
  *
11
16
  * Queue entry shape:
12
17
  * {
13
18
  * id, createdAt, status: 'pending'|'processed'|'skipped',
19
+ * tenantId?,
14
20
  * intake: { sourcePath, outputPath, characters, knowledgeSubdir },
15
21
  * triage: { intakeType, rdStage, primaryOwner, recommendedChain,
16
22
  * recommendedAction, risk, requiresApproval, confidence,
@@ -37,19 +43,60 @@ import path from 'node:path';
37
43
 
38
44
  import { FilesystemIntakeQueue, queueRoot, pendingDir, processedDir, skippedDir } from './filesystem-queue.mjs';
39
45
  import { GitIntakeQueue } from './git-queue.mjs';
40
- import { getDeploymentMode } from '../deployment-mode.mjs';
46
+ import {
47
+ getDeploymentMode,
48
+ requireTeamCapabilityOrDegrade,
49
+ } from '../deployment-mode.mjs';
50
+ import { createSqlClient } from '../storage/backend.mjs';
51
+ import { findQueueProvider, BUILTIN_QUEUE_BACKENDS } from './queue-registry.mjs';
52
+ import { resolveTenantContext, TENANT_ID_ENV_KEY, DEFAULT_TENANT_ID } from '../tenant/context.mjs';
53
+ import { PostgresIntakeQueue } from '../queue/pg-queue.mjs';
41
54
 
42
55
  export { FilesystemIntakeQueue, GitIntakeQueue, queueRoot, pendingDir, processedDir, skippedDir };
43
56
 
44
57
  export const INTAKE_QUEUE_BACKEND_ENV_KEY = 'CONSTRUCT_INTAKE_QUEUE_BACKEND';
45
58
  export const INTAKE_PROJECT_ENV_KEY = 'CONSTRUCT_PROJECT_NAME';
46
59
  export const INTAKE_TENANT_ENV_KEY = 'CONSTRUCT_TENANT_ID';
60
+ export const INTAKE_QUEUE_NAME_ENV_KEY = 'CONSTRUCT_INTAKE_QUEUE_NAME';
61
+ export const INTAKE_QUEUE_LEASE_SECONDS_ENV_KEY = 'CONSTRUCT_QUEUE_LEASE_SECONDS';
47
62
 
48
- function resolveBackend(env) {
63
+ // Re-exported so a caller can resolve the same tenant value the factory
64
+ // stamps on the queue without importing lib/tenant/context.mjs directly.
65
+ export { TENANT_ID_ENV_KEY, DEFAULT_TENANT_ID } from '../tenant/context.mjs';
66
+
67
+ // Thrown when a caller names a queue backend that is neither a builtin provider
68
+ // (git/filesystem) nor a registered kind:'queue' extension. The reframing
69
+ // removed the silent postgres->git alias, so an unknown backend is a hard,
70
+ // explicit error rather than a swallowed downgrade.
71
+
72
+ export class UnregisteredQueueProviderError extends Error {
73
+ constructor(backend) {
74
+ super(
75
+ `Queue backend '${backend}' has no registered kind:'queue' provider. ` +
76
+ `Builtin providers: ${BUILTIN_QUEUE_BACKENDS.join(', ')}. ` +
77
+ `Install a kind:'queue' extension (e.g. a Postgres provider) to select it.`,
78
+ );
79
+ this.name = 'UnregisteredQueueProviderError';
80
+ this.backend = backend;
81
+ }
82
+ }
83
+
84
+ // Selection resolves through the registry by kind. An explicit backend override
85
+ // wins; otherwise deployment mode picks the default provider (solo→filesystem,
86
+ // team/enterprise→postgres). A non-builtin backend must resolve to a registered
87
+ // kind:'queue' provider or the call fails explicitly.
88
+
89
+ function resolveBackend(env, rootDir) {
49
90
  const override = env?.[INTAKE_QUEUE_BACKEND_ENV_KEY];
50
- if (override === 'filesystem' || override === 'git') return override;
51
- const mode = getDeploymentMode(env);
52
- return mode === 'solo' ? 'filesystem' : 'git';
91
+ const requested = override && String(override).trim()
92
+ ? String(override).trim()
93
+ : (getDeploymentMode(env, { cwd: rootDir }) === 'solo' ? 'filesystem' : 'postgres');
94
+
95
+ if (BUILTIN_QUEUE_BACKENDS.includes(requested)) return { backend: requested, provider: null };
96
+
97
+ const provider = findQueueProvider(requested, { rootDir, env });
98
+ if (!provider) throw new UnregisteredQueueProviderError(requested);
99
+ return { backend: requested, provider };
53
100
  }
54
101
 
55
102
  function resolveProject(rootDir, env) {
@@ -58,20 +105,89 @@ function resolveProject(rootDir, env) {
58
105
  return path.basename(path.resolve(rootDir)).trim() || 'construct';
59
106
  }
60
107
 
108
+ // Resolves the tenant context (config + env, validated) for queue scoping.
109
+ // Reads CONSTRUCT_TENANT_ID directly here — the factory is a plumbing
110
+ // boundary, so it does not load construct.config.json itself and passes no
111
+ // config, which mirrors solo/team's config-optional path. Enterprise callers
112
+ // that need config-sourced tenantId should resolve it once at startup via
113
+ // lib/deployment-mode.mjs#validateTenantAtStartup and pass the result through
114
+ // opts.tenantId instead of relying on this fallback.
115
+
116
+ export function resolveIntakeTenantId(env = process.env, mode = getDeploymentMode(env)) {
117
+ return resolveTenantContext({ env, mode }).tenantId;
118
+ }
119
+
61
120
  /**
62
121
  * Create an IntakeQueue instance for the given project root.
63
122
  * Backend selection: CONSTRUCT_INTAKE_QUEUE_BACKEND override wins; otherwise
64
- * solo mode → filesystem, team/enterprise → git. The git
65
- * backend uses the filesystem and git for state synchronization.
123
+ * solo mode → filesystem, team/enterprise → postgres. A backend that is neither builtin nor a registered
124
+ * kind:'queue' provider throws UnregisteredQueueProviderError there is no
125
+ * silent postgres->git alias.
126
+ *
127
+ * The returned instance carries a `.tenantId` (opts.tenantId wins, otherwise
128
+ * resolved from CONSTRUCT_TENANT_ID/config via resolveIntakeTenantId) so a
129
+ * caller building a queue entry can stamp tenantId without re-deriving it —
130
+ * the queue classes themselves stay tenant-agnostic (LMCP-H1; tenant-scoped
131
+ * storage is LMCP-H4, not this bead).
66
132
  */
67
133
  export function createIntakeQueue(rootDir, env = process.env, opts = {}) {
68
- const backend = opts.backend || resolveBackend(env);
69
- if (backend === 'filesystem') return new FilesystemIntakeQueue(rootDir);
70
- if (backend === 'git' || backend === 'postgres') {
71
- // 'postgres' aliases to 'git' so existing configs keep resolving
134
+ const resolved = opts.backend
135
+ ? (BUILTIN_QUEUE_BACKENDS.includes(opts.backend)
136
+ ? { backend: opts.backend, provider: null }
137
+ : (findQueueProvider(opts.backend, { rootDir, env })
138
+ ? { backend: opts.backend, provider: findQueueProvider(opts.backend, { rootDir, env }) }
139
+ : (() => { throw new UnregisteredQueueProviderError(opts.backend); })()))
140
+ : resolveBackend(env, rootDir);
72
141
 
142
+ const tenantId = opts.tenantId ?? resolveIntakeTenantId(env, getDeploymentMode(env, { cwd: rootDir }));
143
+
144
+ if (resolved.backend === 'filesystem') {
145
+ const queue = new FilesystemIntakeQueue(rootDir);
146
+ queue.tenantId = tenantId;
147
+ return queue;
148
+ }
149
+ if (resolved.backend === 'git') {
73
150
  const project = opts.project ?? resolveProject(rootDir, env);
74
- return new GitIntakeQueue({ project, rootDir });
151
+ const queue = new GitIntakeQueue({ project, rootDir });
152
+ queue.tenantId = tenantId;
153
+ return queue;
75
154
  }
76
- throw new Error(`Unknown intake queue backend: ${backend}`);
155
+
156
+ if (resolved.backend === 'postgres' || resolved.provider?.id === 'postgres') {
157
+ const sql = opts.sql ?? createSqlClient(env);
158
+ if (!sql) {
159
+ const mode = getDeploymentMode(env, { cwd: rootDir });
160
+ if (mode === 'solo') {
161
+ throw new Error(
162
+ `Queue backend '${resolved.backend}' requires DATABASE_URL or CONSTRUCT_DATABASE_URL; ` +
163
+ `no Postgres SQL client is configured.`,
164
+ );
165
+ }
166
+ requireTeamCapabilityOrDegrade('postgres-queue', false, env, { cwd: rootDir });
167
+ const queue = new GitIntakeQueue({ project: opts.project ?? resolveProject(rootDir, env), rootDir });
168
+ queue.tenantId = tenantId;
169
+ queue.backend = 'git';
170
+ queue.requestedBackend = resolved.backend;
171
+ queue.degraded = true;
172
+ queue.degradedReason = 'postgres-unavailable';
173
+ return queue;
174
+ }
175
+ requireTeamCapabilityOrDegrade('postgres-queue', true, env, { cwd: rootDir });
176
+ return new PostgresIntakeQueue({
177
+ sql,
178
+ project: opts.project ?? resolveProject(rootDir, env),
179
+ tenantId,
180
+ queueName: opts.queueName ?? env?.[INTAKE_QUEUE_NAME_ENV_KEY] ?? 'intake',
181
+ leaseSeconds: opts.leaseSeconds ?? env?.[INTAKE_QUEUE_LEASE_SECONDS_ENV_KEY],
182
+ });
183
+ }
184
+
185
+ // A registered kind:'queue' provider was selected but no in-tree instantiation
186
+ // path exists yet. The selection is honest — it names the resolved provider
187
+ // rather than silently downgrading to git.
188
+
189
+ throw new Error(
190
+ `Queue backend '${resolved.backend}' resolves to registered kind:'queue' provider ` +
191
+ `'${resolved.provider?.id}' but no in-tree instantiation path is wired yet.`,
192
+ );
77
193
  }