@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
@@ -0,0 +1,395 @@
1
+ /**
2
+ * lib/queue/pg-queue.mjs — optional Postgres kind:'queue' provider.
3
+ *
4
+ * Claiming uses a single UPDATE fed by SELECT ... FOR UPDATE SKIP LOCKED so
5
+ * parallel workers contend inside Postgres instead of racing in process memory.
6
+ */
7
+
8
+ import crypto from 'node:crypto';
9
+
10
+ import { applyMigrations } from '../db/migrate.mjs';
11
+
12
+ export const PG_QUEUE_DEFAULT_NAME = 'intake';
13
+ export const PG_QUEUE_DEFAULT_LEASE_SECONDS = 120;
14
+ export const PG_QUEUE_DEFAULT_MAX_ATTEMPTS = 3;
15
+
16
+ export function newQueueId(prefix = 'pkt') {
17
+ return `${prefix}-${Date.now().toString(36)}-${crypto.randomBytes(4).toString('hex')}`;
18
+ }
19
+
20
+ function nowIso() {
21
+ return new Date().toISOString();
22
+ }
23
+
24
+ function normalizeEntry(entry, id) {
25
+ return {
26
+ ...entry,
27
+ id,
28
+ status: entry?.status || 'pending',
29
+ createdAt: entry?.createdAt || nowIso(),
30
+ };
31
+ }
32
+
33
+ function rowToEntry(row) {
34
+ return row?.payload || null;
35
+ }
36
+
37
+ function leaseInterval(sql, seconds) {
38
+ return sql`${Number(seconds)} * interval '1 second'`;
39
+ }
40
+
41
+ function backoffInterval(sql, seconds) {
42
+ return sql`${Math.max(0, Number(seconds) || 0)} * interval '1 second'`;
43
+ }
44
+
45
+ export class PostgresIntakeQueue {
46
+ constructor({
47
+ sql,
48
+ project,
49
+ tenantId = 'local',
50
+ queueName = PG_QUEUE_DEFAULT_NAME,
51
+ leaseSeconds = PG_QUEUE_DEFAULT_LEASE_SECONDS,
52
+ } = {}) {
53
+ if (!sql) throw new Error('PostgresIntakeQueue: sql client is required');
54
+ if (!project) throw new Error('PostgresIntakeQueue: project is required');
55
+ this.sql = sql;
56
+ this.project = project;
57
+ this.tenantId = tenantId;
58
+ this.queueName = queueName;
59
+ this.leaseSeconds = Number(leaseSeconds) || PG_QUEUE_DEFAULT_LEASE_SECONDS;
60
+ this._ensured = null;
61
+ }
62
+
63
+ async ensureSchema() {
64
+ if (!this._ensured) this._ensured = applyMigrations(this.sql);
65
+ return this._ensured;
66
+ }
67
+
68
+ async enqueue(entry) {
69
+ if (!entry?.intake?.sourcePath) throw new Error('enqueue: entry.intake.sourcePath is required');
70
+ await this.ensureSchema();
71
+ const id = entry.id || newQueueId();
72
+ const payload = normalizeEntry(entry, id);
73
+ await this.sql`
74
+ INSERT INTO construct_queue_items (
75
+ project, tenant_id, queue_name, item_id, status, payload, created_at, updated_at, max_attempts
76
+ ) VALUES (
77
+ ${this.project}, ${this.tenantId}, ${this.queueName}, ${id}, 'pending',
78
+ ${this.sql.json(payload)}, ${payload.createdAt}, now(),
79
+ ${Number(entry.maxAttempts) > 0 ? Math.floor(Number(entry.maxAttempts)) : PG_QUEUE_DEFAULT_MAX_ATTEMPTS}
80
+ )
81
+ ON CONFLICT (project, tenant_id, queue_name, item_id) DO UPDATE SET
82
+ payload = EXCLUDED.payload,
83
+ status = EXCLUDED.status,
84
+ max_attempts = EXCLUDED.max_attempts,
85
+ updated_at = now()
86
+ `;
87
+ return { id };
88
+ }
89
+
90
+ async listPending() {
91
+ await this.ensureSchema();
92
+ const rows = await this.sql`
93
+ SELECT payload FROM construct_queue_items
94
+ WHERE project = ${this.project}
95
+ AND tenant_id = ${this.tenantId}
96
+ AND queue_name = ${this.queueName}
97
+ AND status = 'pending'
98
+ ORDER BY created_at ASC, item_id ASC
99
+ `;
100
+ return rows.map(rowToEntry);
101
+ }
102
+
103
+ async count() {
104
+ await this.ensureSchema();
105
+ const rows = await this.sql`
106
+ SELECT COUNT(*)::int AS count FROM construct_queue_items
107
+ WHERE project = ${this.project}
108
+ AND tenant_id = ${this.tenantId}
109
+ AND queue_name = ${this.queueName}
110
+ AND status = 'pending'
111
+ `;
112
+ return Number(rows[0]?.count || 0);
113
+ }
114
+
115
+ async read(id) {
116
+ if (!id) return null;
117
+ await this.ensureSchema();
118
+ const rows = await this.sql`
119
+ SELECT payload FROM construct_queue_items
120
+ WHERE project = ${this.project}
121
+ AND tenant_id = ${this.tenantId}
122
+ AND queue_name = ${this.queueName}
123
+ AND item_id = ${id}
124
+ LIMIT 1
125
+ `;
126
+ return rowToEntry(rows[0]);
127
+ }
128
+
129
+ async claim({ claimedBy, leaseSeconds = this.leaseSeconds } = {}) {
130
+ if (!claimedBy) throw new Error('claim: claimedBy is required');
131
+ await this.ensureSchema();
132
+ const claimId = newQueueId('claim');
133
+ const rows = await this.sql`
134
+ WITH candidate AS (
135
+ SELECT item_id
136
+ FROM construct_queue_items
137
+ WHERE project = ${this.project}
138
+ AND tenant_id = ${this.tenantId}
139
+ AND queue_name = ${this.queueName}
140
+ AND available_at <= now()
141
+ AND (
142
+ status = 'pending'
143
+ OR (status = 'claimed' AND lease_expires_at <= now() AND attempt < max_attempts)
144
+ )
145
+ ORDER BY created_at ASC, item_id ASC
146
+ FOR UPDATE SKIP LOCKED
147
+ LIMIT 1
148
+ ),
149
+ updated AS (
150
+ UPDATE construct_queue_items q
151
+ SET status = 'claimed',
152
+ attempt = q.attempt + 1,
153
+ claimed_by = ${claimedBy},
154
+ lease_expires_at = now() + ${leaseInterval(this.sql, leaseSeconds)},
155
+ updated_at = now(),
156
+ payload = jsonb_set(
157
+ jsonb_set(
158
+ jsonb_set(q.payload, '{status}', to_jsonb('claimed'::text), true),
159
+ '{claimedBy}', to_jsonb(${claimedBy}::text), true
160
+ ),
161
+ '{claimedAt}', to_jsonb(now()::text), true
162
+ )
163
+ FROM candidate
164
+ WHERE q.project = ${this.project}
165
+ AND q.tenant_id = ${this.tenantId}
166
+ AND q.queue_name = ${this.queueName}
167
+ AND q.item_id = candidate.item_id
168
+ RETURNING q.item_id, q.attempt, q.lease_expires_at, q.payload
169
+ )
170
+ INSERT INTO construct_queue_claims (
171
+ claim_id, project, tenant_id, queue_name, item_id, worker_id, attempt, lease_expires_at
172
+ )
173
+ SELECT ${claimId}, ${this.project}, ${this.tenantId}, ${this.queueName},
174
+ item_id, ${claimedBy}, attempt, lease_expires_at
175
+ FROM updated
176
+ RETURNING item_id, attempt, lease_expires_at,
177
+ (SELECT payload FROM updated WHERE updated.item_id = construct_queue_claims.item_id) AS payload
178
+ `;
179
+ const row = rows[0];
180
+ if (!row) return null;
181
+ return {
182
+ ...row.payload,
183
+ id: row.payload?.id || row.item_id,
184
+ status: 'claimed',
185
+ claimedBy,
186
+ attempt: row.attempt,
187
+ leaseExpiresAt: row.lease_expires_at,
188
+ claimId,
189
+ };
190
+ }
191
+
192
+ async heartbeat(id, { workerId, leaseSeconds = this.leaseSeconds } = {}) {
193
+ if (!id) throw new Error('heartbeat: id is required');
194
+ if (!workerId) throw new Error('heartbeat: workerId is required');
195
+ await this.ensureSchema();
196
+ const rows = await this.sql`
197
+ UPDATE construct_queue_items
198
+ SET lease_expires_at = now() + ${leaseInterval(this.sql, leaseSeconds)},
199
+ updated_at = now()
200
+ WHERE project = ${this.project}
201
+ AND tenant_id = ${this.tenantId}
202
+ AND queue_name = ${this.queueName}
203
+ AND item_id = ${id}
204
+ AND status = 'claimed'
205
+ AND claimed_by = ${workerId}
206
+ AND lease_expires_at > now()
207
+ AND COALESCE((payload->>'cancelRequested')::boolean, false) = false
208
+ RETURNING item_id, lease_expires_at
209
+ `;
210
+ if (!rows[0]) {
211
+ const current = await this.read(id);
212
+ if (current?.cancelRequested) return { id, renewed: false, cancelled: true, reason: current.cancellationReason || null };
213
+ return { id, renewed: false };
214
+ }
215
+ return { id, renewed: true, leaseExpiresAt: rows[0].lease_expires_at };
216
+ }
217
+
218
+ async fail(id, { workerId, reason, backoffSeconds = 0 } = {}) {
219
+ if (!id) throw new Error('fail: id is required');
220
+ await this.ensureSchema();
221
+ const rows = await this.sql`
222
+ UPDATE construct_queue_items
223
+ SET status = CASE WHEN attempt >= max_attempts THEN 'dead_letter' ELSE 'pending' END,
224
+ available_at = CASE WHEN attempt >= max_attempts THEN available_at ELSE now() + ${backoffInterval(this.sql, backoffSeconds)} END,
225
+ terminal_reason = ${reason || null},
226
+ claimed_by = NULL,
227
+ lease_expires_at = NULL,
228
+ updated_at = now(),
229
+ payload = jsonb_set(
230
+ jsonb_set(
231
+ jsonb_set(payload, '{status}', to_jsonb(CASE WHEN attempt >= max_attempts THEN 'dead_letter'::text ELSE 'pending'::text END), true),
232
+ '{lastFailureReason}', to_jsonb(${reason || ''}::text), true
233
+ ),
234
+ '{failedBy}', to_jsonb(${workerId || ''}::text), true
235
+ )
236
+ WHERE project = ${this.project}
237
+ AND tenant_id = ${this.tenantId}
238
+ AND queue_name = ${this.queueName}
239
+ AND item_id = ${id}
240
+ AND status = 'claimed'
241
+ AND (${workerId || null} IS NULL OR claimed_by = ${workerId || null})
242
+ RETURNING item_id, status, attempt, max_attempts, available_at
243
+ `;
244
+ if (!rows[0]) return null;
245
+ return {
246
+ id,
247
+ status: rows[0].status,
248
+ attempt: rows[0].attempt,
249
+ maxAttempts: rows[0].max_attempts,
250
+ deadLettered: rows[0].status === 'dead_letter',
251
+ availableAt: rows[0].available_at,
252
+ };
253
+ }
254
+
255
+ async requestCancellation(id, { requestedBy, reason } = {}) {
256
+ if (!id) throw new Error('requestCancellation: id is required');
257
+ await this.ensureSchema();
258
+ const rows = await this.sql`
259
+ UPDATE construct_queue_items
260
+ SET terminal_reason = ${reason || null},
261
+ updated_at = now(),
262
+ payload = jsonb_set(
263
+ jsonb_set(
264
+ jsonb_set(payload, '{cancelRequested}', 'true'::jsonb, true),
265
+ '{cancellationReason}', to_jsonb(${reason || ''}::text), true
266
+ ),
267
+ '{cancelRequestedBy}', to_jsonb(${requestedBy || ''}::text), true
268
+ )
269
+ WHERE project = ${this.project}
270
+ AND tenant_id = ${this.tenantId}
271
+ AND queue_name = ${this.queueName}
272
+ AND item_id = ${id}
273
+ AND status IN ('pending', 'claimed', 'awaiting_approval')
274
+ RETURNING item_id
275
+ `;
276
+ return rows[0] ? { id, cancellationRequested: true } : null;
277
+ }
278
+
279
+ async markProcessed(id, { processedBy, notes, executionKey } = {}) {
280
+ if (!id) throw new Error('markProcessed: id is required');
281
+ await this.ensureSchema();
282
+ if (executionKey) {
283
+ const current = await this.read(id);
284
+ if (current?.status === 'processed' && current.executionKey === executionKey) {
285
+ return { id, idempotent: true };
286
+ }
287
+ }
288
+ const rows = await this.sql`
289
+ UPDATE construct_queue_items
290
+ SET status = 'processed',
291
+ processed_at = now(),
292
+ processed_by = ${processedBy || null},
293
+ terminal_reason = ${notes || null},
294
+ claimed_by = NULL,
295
+ lease_expires_at = NULL,
296
+ updated_at = now(),
297
+ payload = jsonb_set(
298
+ jsonb_set(
299
+ jsonb_set(payload, '{status}', to_jsonb('processed'::text), true),
300
+ '{processedAt}', to_jsonb(now()::text), true
301
+ ),
302
+ '{executionKey}', to_jsonb(${executionKey || ''}::text), true
303
+ )
304
+ WHERE project = ${this.project}
305
+ AND tenant_id = ${this.tenantId}
306
+ AND queue_name = ${this.queueName}
307
+ AND item_id = ${id}
308
+ AND status <> 'processed'
309
+ RETURNING item_id
310
+ `;
311
+ return rows[0] ? { id } : null;
312
+ }
313
+
314
+ async markSkipped(id, { skippedBy, reason } = {}) {
315
+ if (!id) throw new Error('markSkipped: id is required');
316
+ await this.ensureSchema();
317
+ const rows = await this.sql`
318
+ UPDATE construct_queue_items
319
+ SET status = 'skipped',
320
+ skipped_at = now(),
321
+ skipped_by = ${skippedBy || null},
322
+ terminal_reason = ${reason || null},
323
+ claimed_by = NULL,
324
+ lease_expires_at = NULL,
325
+ updated_at = now(),
326
+ payload = jsonb_set(
327
+ jsonb_set(payload, '{status}', to_jsonb('skipped'::text), true),
328
+ '{skippedAt}', to_jsonb(now()::text), true
329
+ )
330
+ WHERE project = ${this.project}
331
+ AND tenant_id = ${this.tenantId}
332
+ AND queue_name = ${this.queueName}
333
+ AND item_id = ${id}
334
+ RETURNING item_id
335
+ `;
336
+ return rows[0] ? { id } : null;
337
+ }
338
+
339
+ async reopen(id) {
340
+ if (!id) throw new Error('reopen: id is required');
341
+ await this.ensureSchema();
342
+ const rows = await this.sql`
343
+ UPDATE construct_queue_items
344
+ SET status = 'pending',
345
+ claimed_by = NULL,
346
+ lease_expires_at = NULL,
347
+ processed_at = NULL,
348
+ processed_by = NULL,
349
+ skipped_at = NULL,
350
+ skipped_by = NULL,
351
+ terminal_reason = NULL,
352
+ updated_at = now(),
353
+ payload = jsonb_set(payload, '{status}', to_jsonb('pending'::text), true)
354
+ WHERE project = ${this.project}
355
+ AND tenant_id = ${this.tenantId}
356
+ AND queue_name = ${this.queueName}
357
+ AND item_id = ${id}
358
+ RETURNING item_id
359
+ `;
360
+ return rows[0] ? { id, from: 'postgres' } : null;
361
+ }
362
+
363
+ async queueStats() {
364
+ await this.ensureSchema();
365
+ const rows = await this.sql`
366
+ SELECT status, COUNT(*)::int AS count
367
+ FROM construct_queue_items
368
+ WHERE project = ${this.project}
369
+ AND tenant_id = ${this.tenantId}
370
+ AND queue_name = ${this.queueName}
371
+ GROUP BY status
372
+ `;
373
+ const byStatus = {
374
+ pending: 0,
375
+ claimed: 0,
376
+ processed: 0,
377
+ skipped: 0,
378
+ awaiting_approval: 0,
379
+ dead_letter: 0,
380
+ };
381
+ for (const row of rows) {
382
+ if (Object.hasOwn(byStatus, row.status)) byStatus[row.status] = Number(row.count || 0);
383
+ }
384
+ return {
385
+ project: this.project,
386
+ tenantId: this.tenantId,
387
+ queueName: this.queueName,
388
+ byStatus,
389
+ pending: byStatus.pending,
390
+ claimed: byStatus.claimed,
391
+ deadLetter: byStatus.dead_letter,
392
+ total: Object.values(byStatus).reduce((sum, n) => sum + n, 0),
393
+ };
394
+ }
395
+ }
package/lib/reflect.mjs CHANGED
@@ -14,6 +14,7 @@ import { KNOWLEDGE_ROOT, KNOWLEDGE_SUBDIRS, inferKnowledgeTarget } from './knowl
14
14
  import { extractSessionObservation } from './reflect/extractor.mjs';
15
15
  import { readContextState, contextSummaryLine } from './context-state.mjs';
16
16
  import { configDir } from './config/xdg.mjs';
17
+ import { isMainModule } from './roots.mjs';
17
18
 
18
19
  const HOME = process.env.HOME || process.env.USERPROFILE;
19
20
  const USER_ENV_PATH = join(configDir(HOME), 'config.env');
@@ -253,7 +254,7 @@ function parseTranscript(path) {
253
254
  /**
254
255
  * CLI entry point for direct execution.
255
256
  */
256
- if (import.meta.url === `file://${process.argv[1]}`) {
257
+ if (isMainModule(import.meta.url)) {
257
258
  const args = process.argv.slice(2);
258
259
  runReflectCli(args).catch((err) => {
259
260
  console.error(`Error: ${err.message}`);
@@ -1,12 +1,21 @@
1
1
  /**
2
2
  * lib/registry/assemble.mjs — Runtime merge of modular org files into registry v3.
3
3
  *
4
- * Walks specialists/org/{groups,teams,specialists,contracts,policies}, merges
5
- * .cx/org overlay fragments, and returns the assembled object for validation.
4
+ * Walks specialists/org/{groups,teams,specialists,contracts,policies}, then
5
+ * merges two user/project extension tiers on top, builtin -> user -> project
6
+ * (ADR-0052's precedence model, applied here to the org registry): a home-level
7
+ * custom org tree (~/.construct/org/**, construct-rf26.13 — a user's personal
8
+ * custom specialists/teams shared across all their projects), then the existing
9
+ * .cx/org project overlay (highest precedence). Later tiers win on id collision.
6
10
  */
7
11
 
8
12
  import fs from 'node:fs';
9
13
  import path from 'node:path';
14
+ import { homeDir } from '../paths.mjs';
15
+
16
+ function customOrgRoot() {
17
+ return path.join(homeDir(), '.construct', 'org');
18
+ }
10
19
 
11
20
  function readJsonDir(dir) {
12
21
  const out = {};
@@ -66,6 +75,8 @@ export function orgDirMtime(rootDir) {
66
75
  }
67
76
  };
68
77
  walk(orgRoot);
78
+ const homeOrg = customOrgRoot();
79
+ if (fs.existsSync(homeOrg)) walk(homeOrg);
69
80
  const overlay = path.join(rootDir, '.cx', 'org');
70
81
  if (fs.existsSync(overlay)) walk(overlay);
71
82
  return max;
@@ -85,6 +96,21 @@ export function assembleRegistry(rootDir) {
85
96
  const contracts = readJsonDir(path.join(orgRoot, 'contracts'));
86
97
  const policies = readJsonDir(path.join(orgRoot, 'policies'));
87
98
 
99
+ const homeOrgRoot = customOrgRoot();
100
+ if (fs.existsSync(homeOrgRoot)) {
101
+ for (const [id, val] of Object.entries(readJsonDir(path.join(homeOrgRoot, 'groups')))) {
102
+ groups[id] = { ...groups[id], ...val };
103
+ }
104
+ for (const [id, val] of Object.entries(readJsonDir(path.join(homeOrgRoot, 'teams')))) {
105
+ squads[id] = { ...squads[id], ...val };
106
+ }
107
+ for (const [id, val] of Object.entries(readJsonDir(path.join(homeOrgRoot, 'specialists')))) {
108
+ specialists[id] = { ...specialists[id], ...val };
109
+ }
110
+ mergeOverlaySection(contracts, readJsonDir(path.join(homeOrgRoot, 'contracts')));
111
+ mergeOverlaySection(policies, readJsonDir(path.join(homeOrgRoot, 'policies')));
112
+ }
113
+
88
114
  if (fs.existsSync(overlayRoot)) {
89
115
  for (const [id, val] of Object.entries(readJsonDir(path.join(overlayRoot, 'groups')))) {
90
116
  groups[id] = { ...groups[id], ...val };
@@ -9,6 +9,7 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { loadRegistry } from '../registry/loader.mjs';
12
+ import { bindingForSpecialist } from '../roles/flavor-bindings.mjs';
12
13
 
13
14
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
14
15
  const REPO_ROOT = path.resolve(MODULE_DIR, '..', '..');
@@ -81,12 +82,18 @@ export function triageBoundOrphans({ rootDir = REPO_ROOT } = {}) {
81
82
  } else if (base && profileRoles.has(base)) {
82
83
  category = 'B-composer';
83
84
  recommendation = `Profile role "${base}" — may load via profile overlay; verify before merge`;
85
+ } else if (base && bindingForSpecialist(base)?.specialistId) {
86
+ // Base name doesn't match a specialist's own name, but
87
+ // lib/roles/flavor-bindings.mjs still routes it onto a live anchor —
88
+ // reachable via prompt-composer under that anchor's identity.
89
+ category = 'B-composer';
90
+ recommendation = `Role flavor bound to ${bindingForSpecialist(base).specialistId} via lib/roles/flavor-bindings.mjs — reachable via prompt-composer; document in registry or bind explicitly`;
84
91
  } else if (skill.startsWith('roles/')) {
85
92
  category = 'C-merge';
86
93
  recommendation = 'Consider merging into parent roles/<base>.md or binding to specialist';
87
94
  } else if (skill.startsWith('docs/')) {
88
95
  category = 'A-bind';
89
- recommendation = 'Bind to cx-docs-keeper or domain owner specialist';
96
+ recommendation = 'Bind to cx-operations or domain owner specialist';
90
97
  }
91
98
 
92
99
  items.push({ skill, category, recommendation });
@@ -0,0 +1,200 @@
1
+ /**
2
+ * lib/registry/custom-scaffold.mjs — CLI-facing scaffolding for user-authored
3
+ * custom specialists and teams (construct-rf26.13).
4
+ *
5
+ * Writes into one of two extension tiers, both read back by
6
+ * lib/registry/assemble.mjs at the same precedence ADR-0052 establishes for
7
+ * provider manifests (builtin -> user -> project):
8
+ * - "user" ~/.construct/org/** — usable across every project on the machine
9
+ * - "project" <rootDir>/.cx/org/** — git-tracked, highest precedence, scoped
10
+ * to one project (the existing overlay
11
+ * introduced by ADR-0046)
12
+ *
13
+ * Refuses to overwrite an existing file unless { force: true }, mirroring
14
+ * lib/specialists/scaffold.mjs's createSpecialistDraft convention. Validates
15
+ * with lib/registry/custom-schema.mjs before writing anything, so a scaffolded
16
+ * record is guaranteed registry-loadable the moment it lands on disk.
17
+ */
18
+
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { homeDir } from '../paths.mjs';
22
+ import { assembleRegistry } from './assemble.mjs';
23
+ import { validateCustomSpecialist, validateCustomTeam } from './custom-schema.mjs';
24
+
25
+ const ID_RE = /^[a-z][a-z0-9-]*$/;
26
+
27
+ export function customOrgDir(scope, { rootDir = process.cwd() } = {}) {
28
+ if (scope === 'user') return path.join(homeDir(), '.construct', 'org');
29
+ return path.join(rootDir, '.cx', 'org');
30
+ }
31
+
32
+ function existingTeamIds(rootDir) {
33
+ try {
34
+ return Object.keys(assembleRegistry(rootDir).teams);
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function renderPromptStub({ id, role, description }) {
41
+ return [
42
+ `# cx-${id}`,
43
+ '',
44
+ `Role: ${role}`,
45
+ '',
46
+ description || 'TODO: describe this specialist\'s perspective and output format.',
47
+ '',
48
+ '## Anti-fabrication contract',
49
+ '',
50
+ 'Every load-bearing claim cites a source the reader can re-verify. When a fact is not in the source, write `unknown` or `[unverified]`. See `rules/common/no-fabrication.md`.',
51
+ '',
52
+ ].join('\n');
53
+ }
54
+
55
+ /**
56
+ * Scaffold a custom specialist JSON record (+ a minimal prompt stub) into the
57
+ * requested tier. Throws with actionable, field-named errors on any failure.
58
+ * @returns {{ path: string, relPath: string, promptPath: string, record: object, scope: string }}
59
+ */
60
+ export function createCustomSpecialist({
61
+ rootDir = process.cwd(),
62
+ scope = 'project',
63
+ id,
64
+ role,
65
+ description,
66
+ modelTier = 'standard',
67
+ reasoningEffort,
68
+ skills = [],
69
+ fence,
70
+ team,
71
+ teamId,
72
+ handoffCandidates = [],
73
+ claudeTools = 'Read,Grep,Glob,LS',
74
+ displayName,
75
+ force = false,
76
+ } = {}) {
77
+ if (!id || !ID_RE.test(id)) {
78
+ throw new Error(`invalid specialist id "${id}" — use lowercase kebab-case (e.g. "widget-specialist")`);
79
+ }
80
+ if (scope !== 'user' && scope !== 'project') {
81
+ throw new Error(`invalid scope "${scope}" — use "user" or "project"`);
82
+ }
83
+
84
+ const orgDir = customOrgDir(scope, { rootDir });
85
+ const specialistsDir = path.join(orgDir, 'specialists');
86
+ const filePath = path.join(specialistsDir, `cx-${id}.json`);
87
+ if (fs.existsSync(filePath) && !force) {
88
+ throw new Error(`${filePath} already exists — pass force to overwrite`);
89
+ }
90
+
91
+ const resolvedTeam = team || teamId;
92
+ const knownTeams = existingTeamIds(rootDir);
93
+ if (resolvedTeam && knownTeams && !knownTeams.includes(resolvedTeam)) {
94
+ throw new Error(
95
+ `team "${resolvedTeam}" does not exist — create it first with \`construct team create ${resolvedTeam} --owner=${role || id}\`, ` +
96
+ `or reference one of: ${knownTeams.slice(0, 10).join(', ')}${knownTeams.length > 10 ? ', …' : ''}`,
97
+ );
98
+ }
99
+
100
+ const promptRelPath = path.posix.join(
101
+ path.relative(rootDir, orgDir).split(path.sep).join('/'),
102
+ 'prompts',
103
+ `cx-${id}.md`,
104
+ );
105
+
106
+ const record = {
107
+ name: id,
108
+ displayName: displayName || description,
109
+ description,
110
+ role: role || id,
111
+ modelTier,
112
+ ...(reasoningEffort ? { reasoningEffort } : {}),
113
+ skills,
114
+ fence: fence || { allowedPaths: [] },
115
+ claudeTools,
116
+ promptFile: promptRelPath,
117
+ handoffCandidates,
118
+ team: resolvedTeam,
119
+ teamId: resolvedTeam,
120
+ internal: true,
121
+ custom: true,
122
+ };
123
+
124
+ const errors = validateCustomSpecialist(record, { rootDir, checkPromptFileExists: false });
125
+ if (errors.length) {
126
+ throw new Error(`custom specialist "${id}" failed validation:\n ${errors.join('\n ')}`);
127
+ }
128
+
129
+ const promptDir = path.join(orgDir, 'prompts');
130
+ const promptPath = path.join(rootDir, promptRelPath);
131
+ fs.mkdirSync(promptDir, { recursive: true });
132
+ if (!fs.existsSync(promptPath) || force) {
133
+ fs.writeFileSync(promptPath, renderPromptStub({ id, role: record.role, description }));
134
+ }
135
+
136
+ fs.mkdirSync(specialistsDir, { recursive: true });
137
+ fs.writeFileSync(filePath, JSON.stringify(record, null, 2) + '\n');
138
+
139
+ return { path: filePath, relPath: path.relative(rootDir, filePath), promptPath, record, scope };
140
+ }
141
+
142
+ /**
143
+ * Scaffold a custom team JSON record into the requested tier. Throws with
144
+ * actionable, field-named errors on any failure.
145
+ * @returns {{ path: string, relPath: string, record: object, scope: string }}
146
+ */
147
+ export function createCustomTeam({
148
+ rootDir = process.cwd(),
149
+ scope = 'project',
150
+ id,
151
+ name,
152
+ owner,
153
+ roles,
154
+ specialists = [],
155
+ charter,
156
+ decisionRights = [],
157
+ forbiddenDecisions = [],
158
+ escalationPath,
159
+ contact = {},
160
+ groupId,
161
+ force = false,
162
+ } = {}) {
163
+ if (!id || !ID_RE.test(id)) {
164
+ throw new Error(`invalid team id "${id}" — use lowercase kebab-case (e.g. "widget-team")`);
165
+ }
166
+ if (scope !== 'user' && scope !== 'project') {
167
+ throw new Error(`invalid scope "${scope}" — use "user" or "project"`);
168
+ }
169
+
170
+ const orgDir = customOrgDir(scope, { rootDir });
171
+ const teamsDir = path.join(orgDir, 'teams');
172
+ const filePath = path.join(teamsDir, `${id}.json`);
173
+ if (fs.existsSync(filePath) && !force) {
174
+ throw new Error(`${filePath} already exists — pass force to overwrite`);
175
+ }
176
+
177
+ const resolvedRoles = roles && roles.length ? roles : (owner ? [owner] : []);
178
+ const record = {
179
+ name: name || id,
180
+ owner,
181
+ roles: resolvedRoles,
182
+ specialists,
183
+ decisionRights,
184
+ forbiddenDecisions,
185
+ escalationPath: escalationPath && escalationPath.length ? escalationPath : [owner, 'orchestrator'].filter(Boolean),
186
+ charter,
187
+ contact,
188
+ ...(groupId ? { groupId } : {}),
189
+ };
190
+
191
+ const errors = validateCustomTeam({ id, ...record });
192
+ if (errors.length) {
193
+ throw new Error(`custom team "${id}" failed validation:\n ${errors.join('\n ')}`);
194
+ }
195
+
196
+ fs.mkdirSync(teamsDir, { recursive: true });
197
+ fs.writeFileSync(filePath, JSON.stringify(record, null, 2) + '\n');
198
+
199
+ return { path: filePath, relPath: path.relative(rootDir, filePath), record, scope };
200
+ }