@smartmemory/compose 0.3.6-beta → 0.3.8

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 (269) hide show
  1. package/.claude/skills/compose/SKILL.md +42 -88
  2. package/.compose-deps.json +1 -13
  3. package/README.md +72 -5
  4. package/bin/compose.js +754 -347
  5. package/bin/git-hooks/pre-push.template +29 -0
  6. package/bin/judgment-import.js +7 -0
  7. package/bin/judgment-migrate.js +387 -0
  8. package/contracts/comp-obs-contract.schema.json +9 -3
  9. package/contracts/feature-json.schema.json +5 -0
  10. package/contracts/fluid-record.schema.json +209 -0
  11. package/contracts/judgment-record.schema.json +425 -4
  12. package/contracts/lifecycle-backfill.schema.json +322 -0
  13. package/dist/assets/App-Z4MU-H_F.js +916 -0
  14. package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
  15. package/dist/assets/arc-DY26UIVo.js +1 -0
  16. package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
  17. package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
  18. package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
  19. package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
  20. package/dist/assets/channel-SnZzzh7k.js +1 -0
  21. package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
  22. package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
  23. package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
  24. package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
  25. package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
  26. package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
  27. package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
  28. package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
  29. package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
  30. package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
  31. package/dist/assets/clone-DgklGjHm.js +1 -0
  32. package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
  33. package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
  34. package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
  35. package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
  36. package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
  37. package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
  38. package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
  39. package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
  40. package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
  41. package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
  42. package/dist/assets/graph-BNzKGvoy.js +1 -0
  43. package/dist/assets/graph-CI_1htl0.js +331 -0
  44. package/dist/assets/index-BEfrNBp8.js +123 -0
  45. package/dist/assets/index-yyrA5OZd.css +1 -0
  46. package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
  47. package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
  48. package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
  49. package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
  50. package/dist/assets/katex-DkKDou_j.js +257 -0
  51. package/dist/assets/layout-BI8cXFPI.js +1 -0
  52. package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
  53. package/dist/assets/min-vPHfnXcC.js +1 -0
  54. package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
  55. package/dist/assets/mobile-B7m9EO9D.js +17 -0
  56. package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
  57. package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
  58. package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
  59. package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
  60. package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
  61. package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
  62. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
  63. package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
  64. package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
  65. package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
  66. package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
  67. package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
  68. package/dist/index.html +3 -3
  69. package/lib/agent-string.js +7 -5
  70. package/lib/append-integrity.js +81 -0
  71. package/lib/backfill-evidence.js +109 -0
  72. package/lib/bug-escalation.js +39 -4
  73. package/lib/build-stream-schema.js +3 -1
  74. package/lib/build-stream-writer.js +25 -0
  75. package/lib/build.js +1624 -195
  76. package/lib/canon-guard.js +245 -0
  77. package/lib/canon-override.js +196 -0
  78. package/lib/canon-registry.js +291 -0
  79. package/lib/cli-commands.js +144 -0
  80. package/lib/codex-preflight.js +50 -15
  81. package/lib/colleague/context.js +215 -0
  82. package/lib/colleague/writeback.js +95 -0
  83. package/lib/completion-gate.js +1421 -0
  84. package/lib/completion-writer.js +47 -47
  85. package/lib/consumer-fanout.js +105 -11
  86. package/lib/coverage-gate.js +200 -0
  87. package/lib/dir-lock.js +170 -0
  88. package/lib/dispatch-ledger.js +301 -0
  89. package/lib/dispatch-metrics.js +236 -0
  90. package/lib/experiment-judge.js +6 -1
  91. package/lib/feature-json.js +1 -1
  92. package/lib/feature-reconciler.js +8 -0
  93. package/lib/feature-validator.js +64 -1
  94. package/lib/feature-writer.js +66 -2
  95. package/lib/fluid/factory.js +167 -0
  96. package/lib/fluid/ideabox-dates.js +73 -0
  97. package/lib/fluid/ideabox-migrate.js +154 -0
  98. package/lib/fluid/ideabox-ops.js +585 -0
  99. package/lib/fluid/ideabox-view.js +146 -0
  100. package/lib/fluid/import-ideabox.js +186 -0
  101. package/lib/fluid/local-provider.js +606 -0
  102. package/lib/fluid/provider.js +684 -0
  103. package/lib/fluid/record-shape.js +214 -0
  104. package/lib/fluid/record-store.js +328 -0
  105. package/lib/fluid/render-ideabox.js +261 -0
  106. package/lib/fluid/schema.js +40 -0
  107. package/lib/fluid/smartmemory-provider.js +1695 -0
  108. package/lib/gsd.js +63 -14
  109. package/lib/guard-cli.js +175 -0
  110. package/lib/guard-custody.js +141 -0
  111. package/lib/guard-descriptors.js +530 -0
  112. package/lib/guard-enrol.js +254 -0
  113. package/lib/health-score.js +1 -1
  114. package/lib/hooks-status.js +32 -3
  115. package/lib/ideabox-cli.js +315 -0
  116. package/lib/ideabox.js +121 -21
  117. package/lib/judgment/store/index.js +166 -0
  118. package/lib/judgment/store/records.js +184 -25
  119. package/lib/judgment/trace.js +380 -0
  120. package/lib/judgment-attest.js +259 -0
  121. package/lib/judgment-decision-write.js +277 -0
  122. package/lib/judgment-decisions.js +466 -0
  123. package/lib/judgment-gen.js +375 -22
  124. package/lib/judgment-verify.js +153 -0
  125. package/lib/judgment-writer.js +2842 -262
  126. package/lib/lane-gate.js +2 -0
  127. package/lib/lifecycle-modes.js +4 -4
  128. package/lib/lineage.js +400 -0
  129. package/lib/local-claude-connector.js +250 -54
  130. package/lib/maya-client.js +302 -0
  131. package/lib/maya-config.js +53 -0
  132. package/lib/maya-identity.js +283 -0
  133. package/lib/mcp-enforcement.js +21 -35
  134. package/lib/migrate-anon.js +5 -0
  135. package/lib/migrate-roadmap.js +15 -0
  136. package/lib/new.js +13 -1
  137. package/lib/pipeline-compat.js +104 -0
  138. package/lib/policy-catalog.js +295 -0
  139. package/lib/policy-check.js +0 -0
  140. package/lib/process-termination.js +98 -0
  141. package/lib/resolve-workspace.js +5 -1
  142. package/lib/result-normalizer.js +428 -153
  143. package/lib/review-normalize.js +4 -0
  144. package/lib/roadmap-errors.js +65 -0
  145. package/lib/roadmap-preservers.js +24 -4
  146. package/lib/roadmap-residue.js +299 -0
  147. package/lib/smartmemory-client.js +614 -78
  148. package/lib/smartmemory-config.js +54 -0
  149. package/lib/smartmemory-ingest.js +19 -2
  150. package/lib/step-prompt.js +7 -6
  151. package/lib/stratum-engine.js +53 -4
  152. package/lib/stratum-mcp-client.js +391 -31
  153. package/lib/test-bootstrap.js +31 -0
  154. package/lib/tool-inventory.js +122 -0
  155. package/lib/version-check.js +91 -19
  156. package/lib/vision-writer.js +88 -1
  157. package/package.json +7 -6
  158. package/pipelines/bug-fix.stratum.yaml +205 -211
  159. package/pipelines/build-quick.profiles.json +12 -0
  160. package/pipelines/build-quick.stratum.yaml +263 -350
  161. package/pipelines/content.stratum.yaml +81 -77
  162. package/pipelines/coverage-sweep.stratum.yaml +49 -30
  163. package/pipelines/plan.stratum.yaml +76 -86
  164. package/pipelines/refactor.stratum.yaml +125 -125
  165. package/pipelines/research.stratum.yaml +56 -58
  166. package/pipelines/review-fix.profiles.json +6 -0
  167. package/pipelines/review-fix.stratum.yaml +110 -83
  168. package/presets/team-feature.profiles.json +6 -0
  169. package/presets/team-feature.stratum.yaml +93 -66
  170. package/presets/team-research.profiles.json +6 -0
  171. package/presets/team-research.stratum.yaml +89 -80
  172. package/presets/team-review.profiles.json +8 -0
  173. package/presets/team-review.stratum.yaml +98 -80
  174. package/scripts/cost-census.mjs +70 -0
  175. package/scripts/guard-sign/compose-guard-sign.sh +62 -0
  176. package/server/agent-health.js +22 -0
  177. package/server/agent-hooks.js +14 -1
  178. package/server/agent-server.js +5 -248
  179. package/server/agent-spawn.js +3 -4
  180. package/server/agent-workspace.js +294 -0
  181. package/server/build-routes.js +6 -5
  182. package/server/build-stream-bridge.js +53 -0
  183. package/server/cc-session-watcher.js +4 -1
  184. package/server/coalescing-buffer.js +7 -1
  185. package/server/completion-projection.js +228 -0
  186. package/server/compose-mcp-tools.js +124 -24
  187. package/server/compose-mcp.js +91 -790
  188. package/server/decision-event-emit.js +41 -2
  189. package/server/decision-event-id.js +17 -0
  190. package/server/decision-events-snapshot.js +3 -0
  191. package/server/design-routes.js +14 -8
  192. package/server/feature-scan.js +76 -2
  193. package/server/file-watcher.js +170 -21
  194. package/server/ideabox-routes.js +166 -224
  195. package/server/index.js +70 -100
  196. package/server/lifecycle-guard.js +240 -10
  197. package/server/lifecycle-phase-history.js +276 -0
  198. package/server/maya-routes.js +507 -0
  199. package/server/mcp-tool-defs.js +940 -0
  200. package/server/mcp-tool-policy.js +35 -3
  201. package/server/model-tiers.js +22 -5
  202. package/server/pipeline-routes.js +21 -11
  203. package/server/project-root.js +58 -19
  204. package/server/remote-utils.js +3 -1
  205. package/server/schema-validator.js +7 -1
  206. package/server/session-manager.js +5 -6
  207. package/server/session-routes.js +3 -1
  208. package/server/stratum-client.js +57 -10
  209. package/server/stratum-sync.js +6 -3
  210. package/server/summarizer.js +3 -4
  211. package/server/supervisor.js +0 -1
  212. package/server/vision-routes.js +208 -98
  213. package/server/vision-server.js +86 -23
  214. package/server/vision-store.js +60 -6
  215. package/server/vision-utils.js +3 -4
  216. package/server/workspace-activity.js +18 -0
  217. package/server/workspace-middleware.js +2 -2
  218. package/server/workspace-runtime.js +243 -0
  219. package/server/worktree-gc.js +1 -0
  220. package/dist/assets/App-BG3ngu8H.js +0 -896
  221. package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
  222. package/dist/assets/arc-_v4hR_uD.js +0 -1
  223. package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
  224. package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
  225. package/dist/assets/channel-B3U5wFAT.js +0 -1
  226. package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
  227. package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
  228. package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
  229. package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
  230. package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
  231. package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
  232. package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
  233. package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
  234. package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
  235. package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
  236. package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
  237. package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
  238. package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
  239. package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
  240. package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
  241. package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
  242. package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
  243. package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
  244. package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
  245. package/dist/assets/graph-BXPQrYYB.js +0 -331
  246. package/dist/assets/graph-C9eacEi8.js +0 -1
  247. package/dist/assets/index-3ZH5eMcZ.js +0 -119
  248. package/dist/assets/index-LIwREYgH.css +0 -1
  249. package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
  250. package/dist/assets/katex-C5jXJg4s.js +0 -257
  251. package/dist/assets/layout-DEXfKzaS.js +0 -1
  252. package/dist/assets/map-Czzmt4hB.js +0 -1
  253. package/dist/assets/mobile-CaoXUwAr.js +0 -17
  254. package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
  255. package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
  256. package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
  257. package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
  258. package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
  259. package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
  260. package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
  261. package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
  262. package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
  263. package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
  264. package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
  265. package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
  266. package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
  267. package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
  268. package/lib/staleness.js +0 -87
  269. package/server/ideabox-cache.js +0 -77
@@ -0,0 +1,228 @@
1
+ /**
2
+ * server/completion-projection.js — COMP-COMPLETION-GATE slice 3 (§2.3b).
3
+ *
4
+ * The self-verifying projection: ONE predicate that decides whether a vision
5
+ * item may be shown as `complete`, and FOUR transports that apply it —
6
+ *
7
+ * verifiedCompleteProjection({ item, featureCode, cwd, … })
8
+ * ├── POST /api/vision/items/:id/completion-projection (the gate over REST)
9
+ * ├── applyVerifiedProjection(store, …) (in-process: the
10
+ * │ /lifecycle/complete route, the startup scanner, the reconciler)
11
+ * └── VisionWriter.completeItem direct mode (no server running)
12
+ *
13
+ * The property that matters: the predicate carries NO authority of its own. It
14
+ * re-reads canonical state — feature.json, and the guard ledger when a guard
15
+ * resource exists — and refuses unless the completion is ALREADY recorded
16
+ * there. A caller who can satisfy it could simply have read the same truth, so
17
+ * it can be public without reopening the bypass AC-10/AC-16 close. There is no
18
+ * token, no secret, and nothing to forge.
19
+ *
20
+ * Three verification tiers, stamped on the projection so a reader can never
21
+ * mistake a weaker one for a stronger one (AC-16b):
22
+ *
23
+ * 'guarded' — a guard resource exists and its state is complete
24
+ * 'canonical-status-only' — feature.json is COMPLETE, no guard resource exists
25
+ * (the 230 legacy features; startup seeding)
26
+ * 'document-derived' — no feature.json at all; the scanner derived
27
+ * completion from documents (unmanaged folders).
28
+ * Display state, never a completion compose vouches for.
29
+ *
30
+ * Only `guard_not_found` means legacy. Any other guard outcome — timeout, spawn
31
+ * failure, corrupt registry — fails CLOSED (§2.3b, round 5).
32
+ */
33
+
34
+ import { existsSync } from 'node:fs';
35
+ import path from 'node:path';
36
+
37
+ import { readFeature, featuresBase } from '../lib/feature-json.js';
38
+ import { loadFeaturesDir } from '../lib/project-paths.js';
39
+ import { resolveMode } from '../lib/lifecycle-modes.js';
40
+ import { guardEnabled, currentGuardState } from '../lib/completion-gate.js';
41
+ import { resourceId } from './lifecycle-guard.js';
42
+
43
+ export const VERIFIED_BY = Object.freeze({
44
+ GUARDED: 'guarded',
45
+ CANONICAL: 'canonical-status-only',
46
+ DOCUMENT: 'document-derived',
47
+ });
48
+
49
+ /**
50
+ * The guard states that attest a completion (COMP-LIFECYCLE-BACKFILL S1-1).
51
+ * `complete` is a walked lifecycle; `complete_backfilled` is one reconstructed
52
+ * from evidence after the fact. Both are terminal and both verify — the state
53
+ * itself is what tells a reader which one it was.
54
+ */
55
+ export const GUARDED_TERMINAL_STATES = new Set(['complete', 'complete_backfilled']);
56
+
57
+ /** The mode a vision item's lifecycle runs under; build when unstated. */
58
+ export function itemMode(item) {
59
+ return resolveMode(item?.lifecycle?.mode ?? 'build');
60
+ }
61
+
62
+ /**
63
+ * A MANAGED build-mode feature item: it is bound to a feature code by its
64
+ * lifecycle, that lifecycle is build mode, and the workspace has a feature.json
65
+ * for the code. This is the exact set the general status writers refuse
66
+ * `complete` for (AC-10, AC-16). Everything else — fix/plan items, UI-created
67
+ * items with no lifecycle, the `compose new` kickoff item (a build-mode item
68
+ * with no feature.json) — is not a feature completion the gate governs, and
69
+ * keeps its existing path (§2.3d; design §1.3 "documented, not changed").
70
+ */
71
+ export function isManagedBuildItem(item, cwd) {
72
+ const code = item?.lifecycle?.featureCode;
73
+ if (!code || itemMode(item) !== 'build') return false;
74
+ // EXISTENCE, not parseability: a present-but-malformed feature.json is a
75
+ // managed feature whose canon is broken, and the refusals must still apply
76
+ // (Codex r1 #1 — a corrupt file must not read as "unmanaged, anything goes").
77
+ return canonicalFile(cwd, code).exists;
78
+ }
79
+
80
+ /** feature.json for `code`: whether it exists, and its parsed content (null when unreadable). */
81
+ export function canonicalFile(cwd, code) {
82
+ const dir = loadFeaturesDir(cwd);
83
+ const file = path.join(featuresBase(cwd, dir), code, 'feature.json');
84
+ const exists = existsSync(file);
85
+ return { exists, file, feature: exists ? readFeature(cwd, code, dir) : null };
86
+ }
87
+
88
+ /**
89
+ * Decide whether `item` may be projected as complete for `featureCode`.
90
+ *
91
+ * @param {object} a
92
+ * @param {object} a.item the vision item (any transport's copy)
93
+ * @param {string} a.featureCode
94
+ * @param {string} a.cwd workspace root (feature.json, capabilities, resourceId)
95
+ * @param {boolean} [a.consultGuard=true] read the guard ledger (async subprocess). The
96
+ * synchronous startup path passes false (§2.3b).
97
+ * @param {boolean} [a.allowDocumentDerived=false] scanner only: accept a folder with no
98
+ * feature.json as the weakest tier
99
+ * @returns {Promise<{ok:boolean, verified_by?:string, reasons:string[], guardState?:string|null}>}
100
+ */
101
+ export async function verifiedCompleteProjection({
102
+ item, featureCode, cwd, consultGuard = true, allowDocumentDerived = false,
103
+ guardEnabledOverride = undefined,
104
+ }) {
105
+ const reasons = [];
106
+ if (!item) return { ok: false, reasons: ['vision item not found'] };
107
+ if (!featureCode) return { ok: false, reasons: ['featureCode is required'] };
108
+ if (!cwd) return { ok: false, reasons: ['cwd is required'] };
109
+
110
+ // 3. the item is bound to this feature. An unbound item (no lifecycle) may be
111
+ // projected only when its id or featureCode field names the code — the
112
+ // same match `findFeatureItem` uses — so the scanner's freshly created
113
+ // items and legacy UI items are reachable, but a lifecycle bound to a
114
+ // DIFFERENT code is never overwritten.
115
+ const bound = item.lifecycle?.featureCode;
116
+ if (bound && bound !== featureCode) {
117
+ return { ok: false, reasons: [`item ${item.id} is bound to ${bound}, not ${featureCode}`] };
118
+ }
119
+ if (!bound && item.id !== featureCode && item.featureCode !== featureCode && item.title !== featureCode) {
120
+ return { ok: false, reasons: [`item ${item.id} is not bound to ${featureCode}`] };
121
+ }
122
+
123
+ // 1. canonical status
124
+ const canon = canonicalFile(cwd, featureCode);
125
+ if (canon.exists && !canon.feature) {
126
+ // Present but unreadable: canon is BROKEN, not absent. Never downgrade to
127
+ // the document tier; nothing can be verified against a corrupt file.
128
+ return { ok: false, reasons: [`feature.json for ${featureCode} exists but could not be parsed — refusing to project a completion against broken canon`] };
129
+ }
130
+ const feature = canon.feature;
131
+ if (!feature) {
132
+ if (allowDocumentDerived) {
133
+ return { ok: true, verified_by: VERIFIED_BY.DOCUMENT, reasons: [] };
134
+ }
135
+ return { ok: false, reasons: [`no feature.json for ${featureCode} — nothing canonical records a completion`] };
136
+ }
137
+ if (feature.status !== 'COMPLETE') {
138
+ reasons.push(`feature.json for ${featureCode} reads ${feature.status ?? '(no status)'}, not COMPLETE`);
139
+ return { ok: false, reasons };
140
+ }
141
+
142
+ // 2. the guard, only if a resource exists — and only when the workspace has
143
+ // the guard enabled; with it off no resource can have been created by
144
+ // compose and there is no stratum to ask.
145
+ // COMP-LIFECYCLE-BACKFILL §5.10a/R3-7: `guardEnabledOverride` is an
146
+ // OPERATION-level override of a WORKSPACE-level config read. The config
147
+ // answers "is the guard on in this workspace now"; a resumed completion needs
148
+ // "was the guard on for THIS operation" — the flag it persisted. Read with
149
+ // `??`, never `||`: a persisted `false` must not fall through to live config.
150
+ const guardOn = guardEnabledOverride ?? guardEnabled(cwd);
151
+ if (!consultGuard || !guardOn) {
152
+ return { ok: true, verified_by: VERIFIED_BY.CANONICAL, reasons: [], guardState: null };
153
+ }
154
+ const rid = resourceId(featureCode, cwd, itemMode(item));
155
+ const g = await currentGuardState(rid);
156
+ if (g.error) {
157
+ // Fail closed. An unreachable guard is not a legacy feature.
158
+ return {
159
+ ok: false,
160
+ reasons: [`guard state for ${featureCode} could not be read (${g.error.code || g.error.error_type || 'error'}: ${g.error.message || ''}) — refusing rather than downgrading the verification tier`],
161
+ guardState: null,
162
+ };
163
+ }
164
+ if (g.state === null) {
165
+ return { ok: true, verified_by: VERIFIED_BY.CANONICAL, reasons: [], guardState: null };
166
+ }
167
+ // COMP-LIFECYCLE-BACKFILL: both terminals verify a completion. `complete` is a
168
+ // walked lifecycle; `complete_backfilled` is one reconstructed from evidence.
169
+ // The state itself is the provenance signal (Decision 11), so the stamp keeps
170
+ // the ACTUAL state rather than flattening the two.
171
+ if (!GUARDED_TERMINAL_STATES.has(g.state)) {
172
+ return { ok: false, reasons: [`guard for ${featureCode} is in state "${g.state}", not a completion terminal`], guardState: g.state };
173
+ }
174
+ return { ok: true, verified_by: VERIFIED_BY.GUARDED, reasons: [], guardState: g.state };
175
+ }
176
+
177
+ /**
178
+ * The projection record written next to `status: 'complete'`.
179
+ * @param {{ok:true, verified_by:string}} verdict
180
+ * @param {{commitSha?:string, ledgerRef?:string, source?:string}} [evidence]
181
+ */
182
+ export function projectionStamp(verdict, evidence = {}) {
183
+ return {
184
+ verified_by: verdict.verified_by,
185
+ at: new Date().toISOString(),
186
+ ...(evidence.commitSha ? { commit_sha: evidence.commitSha } : {}),
187
+ ...(evidence.ledgerRef ? { ledger_ref: evidence.ledgerRef } : {}),
188
+ ...(evidence.source ? { source: evidence.source } : {}),
189
+ };
190
+ }
191
+
192
+ /**
193
+ * In-process transport: verify, then write to the live VisionStore.
194
+ * Used by the /lifecycle/complete route, the reconciler, and the startup
195
+ * scanner (the latter with `consultGuard:false, allowDocumentDerived:true`).
196
+ *
197
+ * @returns {Promise<{ok:boolean, verified_by?:string, reasons:string[], item?:object}>}
198
+ */
199
+ export async function applyVerifiedProjection(store, {
200
+ itemId, featureCode, cwd, consultGuard = true, allowDocumentDerived = false, evidence = {},
201
+ guardEnabledOverride = undefined,
202
+ }) {
203
+ const item = store.items.get(itemId);
204
+ const v = await verifiedCompleteProjection({
205
+ item, featureCode, cwd, consultGuard, allowDocumentDerived, guardEnabledOverride,
206
+ });
207
+ if (!v.ok) return v;
208
+ const prior = { status: item.status, completion_projection: item.completion_projection ?? null };
209
+ const updated = store.updateItem(itemId, {
210
+ status: 'complete',
211
+ completion_projection: projectionStamp(v, evidence),
212
+ });
213
+ // `_save` swallows disk failures into a boolean the store records. A
214
+ // projection that only reached memory is not a projection: it vanishes on
215
+ // restart with no partial result and no recovery identity (Codex r1 #2).
216
+ // Roll the live item back so memory and disk agree, and report the failure.
217
+ if (store.lastSaveOk === false) {
218
+ try { store.updateItem(itemId, prior); } catch { /* best-effort rollback */ }
219
+ return { ok: false, reasons: [`vision-state could not be persisted for ${itemId} — projection rolled back`] };
220
+ }
221
+ return { ok: true, verified_by: v.verified_by, reasons: [], item: updated };
222
+ }
223
+
224
+ /** True when `p` is inside the workspace's features dir (used by the allowlist test's docs). */
225
+ export function isFeatureJsonPath(cwd, p) {
226
+ const base = path.resolve(cwd, loadFeaturesDir(cwd));
227
+ return path.resolve(p).startsWith(base) && existsSync(p);
228
+ }
@@ -9,7 +9,7 @@ import fs from 'node:fs';
9
9
  import http from 'node:http';
10
10
  import path from 'node:path';
11
11
  import { ArtifactManager, artifactKeysForMode } from './artifact-manager.js';
12
- import { getTargetRoot, getDataDir, resolveProjectPath, switchProject, setCurrentWorkspaceId, loadProjectConfig, isLifecycleEnabled } from './project-root.js';
12
+ import { getTargetRoot, getDataDir, resolveProjectPath, switchProject, setCurrentWorkspaceId, getCurrentWorkspaceId, loadProjectConfig, isLifecycleEnabled } from './project-root.js';
13
13
  import { resolveProfile, isToolAllowed } from './mcp-tool-policy.js';
14
14
  import { getRoadmap } from '../lib/get-roadmap.js';
15
15
 
@@ -19,7 +19,7 @@ import { getRoadmap } from '../lib/get-roadmap.js';
19
19
  * status/roadmap mutation is the bypass STRAT-GUARD exists to close, so it is
20
20
  * rejected unless it carries a valid out-of-band override token (the agent
21
21
  * cannot mint it). Guard off → legacy behavior (no-op). Internal callers
22
- * (recordCompletionsetFeatureStatus directly) never pass through here.
22
+ * (the completion gate persistFeatureRaw) never pass through here.
23
23
  *
24
24
  * @param {object} args tool args (may carry force / override_token)
25
25
  * @param {string} toolName for the error message
@@ -474,6 +474,29 @@ export async function toolAddChangelogEntry(args) {
474
474
  return addChangelogEntry(getTargetRoot(), args);
475
475
  }
476
476
 
477
+ /**
478
+ * COMP-CANON-OVERRIDE — mint a single-use, path-scoped canon override.
479
+ *
480
+ * `actor` is deliberately not forwarded from args: it is stamped by the writer
481
+ * per Decision 3 and must never be caller-supplied.
482
+ */
483
+ export async function toolCanonOverrideGrant(args) {
484
+ const { mintGrant } = await import('../lib/canon-override.js');
485
+ const { loadFeaturesDir } = await import('../lib/project-paths.js');
486
+ const root = getTargetRoot();
487
+ const grant = mintGrant(root, {
488
+ path: args?.path,
489
+ reason: args?.reason,
490
+ operation: args?.operation,
491
+ featuresDir: loadFeaturesDir(root),
492
+ });
493
+ return {
494
+ ...grant,
495
+ recorded_in: '.compose/canon-overrides.jsonl',
496
+ note: 'Single-use and path-scoped. Audit tooling for the Claude Write/Edit path, not enforcement.',
497
+ };
498
+ }
499
+
477
500
  export async function toolGetChangelogEntries(args) {
478
501
  const { getChangelogEntries } = await import('../lib/changelog-writer.js');
479
502
  return getChangelogEntries(getTargetRoot(), args);
@@ -498,9 +521,45 @@ export async function toolGetJournalEntries(args) {
498
521
  // ---------------------------------------------------------------------------
499
522
 
500
523
  export async function toolRecordCompletion(args) {
501
- await assertCompletionEvidence(args);
524
+ // COMP-COMPLETION-GATE slice 1: route the deliberate completion path through
525
+ // the gate, which verifies the same evidence AND takes a guarded transition —
526
+ // the step that has never happened for any real feature (design.md §1).
527
+ //
528
+ // `set_status:false` is not a completion: it records evidence without moving
529
+ // the feature, so it must not drive the guard to a terminal state. It keeps
530
+ // the evidence check and the original write.
531
+ const cwd = getTargetRoot();
502
532
  const { recordCompletion } = await import('../lib/completion-writer.js');
503
- return recordCompletion(getTargetRoot(), args);
533
+ const { completionGate } = await import('../lib/completion-gate.js');
534
+
535
+ if (args?.set_status === false) {
536
+ await assertCompletionEvidence(args);
537
+ return recordCompletion(cwd, args);
538
+ }
539
+
540
+ const gated = await completionGate({
541
+ featureCode: args?.feature_code,
542
+ commitSha: args?.commit_sha,
543
+ testsPass: args?.tests_pass,
544
+ filesChanged: args?.files_changed || [],
545
+ notes: args?.notes,
546
+ force: args?.force,
547
+ builtVia: args?.built_via,
548
+ idempotencyKey: args?.idempotency_key,
549
+ workspaceRoot: cwd,
550
+ });
551
+ if (!gated.ok) {
552
+ const e = new Error(
553
+ `record_completion: refused at ${gated.refusedAt} — ${gated.reasons.join('; ')}`,
554
+ );
555
+ // Preserve the established code for the evidence case so existing callers
556
+ // and tests keep matching on it; the guard cases get their own.
557
+ e.code = gated.refusedAt === 'evidence'
558
+ ? 'COMPLETION_EVIDENCE_REQUIRED'
559
+ : 'COMPLETION_GATE_REFUSED';
560
+ throw e;
561
+ }
562
+ return gated.result;
504
563
  }
505
564
 
506
565
  export async function toolGetCompletions(args) {
@@ -627,8 +686,8 @@ export async function toolBindSession({ featureCode, profile } = {}) {
627
686
  // differ from the request arg) so the anchor never drifts. The trusted env
628
687
  // profile is the floor; the bind `profile` arg may only NARROW it.
629
688
  const boundCode = (body && typeof body === 'object' && body.featureCode) || featureCode;
630
- if (boundCode) _boundFeatureCode = boundCode;
631
- _sessionProfile = resolveProfile(process.env.COMPOSE_SESSION_PROFILE, profile);
689
+ if (boundCode) sessionContext().featureCode = boundCode;
690
+ sessionContext().profile = resolveProfile(process.env.COMPOSE_SESSION_PROFILE, profile);
632
691
  return body;
633
692
  }
634
693
 
@@ -649,10 +708,14 @@ async function _postLifecycle(itemId, action, body) {
649
708
  }
650
709
  const { status, body: respBody } = result;
651
710
  if (status >= 400) {
652
- const errMsg = (respBody && typeof respBody === 'object' && respBody.error)
653
- ? respBody.error
654
- : `HTTP ${status}: ${typeof respBody === 'string' ? respBody : JSON.stringify(respBody)}`;
655
- throw new Error(errMsg);
711
+ const body = respBody && typeof respBody === 'object' ? respBody : null;
712
+ if (body) {
713
+ const parts = [body.error ?? `HTTP ${status}`];
714
+ if (Array.isArray(body.reasons) && body.reasons.length) parts.push(body.reasons.join('; '));
715
+ if (body.hint) parts.push(`hint: ${body.hint}`);
716
+ throw new Error(parts.join(' — '));
717
+ }
718
+ throw new Error(`HTTP ${status}: ${typeof respBody === 'string' ? respBody : JSON.stringify(respBody)}`);
656
719
  }
657
720
  return respBody;
658
721
  }
@@ -682,7 +745,8 @@ async function _postGate(gateId, action, body) {
682
745
  async function _httpRequest(method, urlPath, body = null) {
683
746
  const port = resolvePort();
684
747
  const headers = { 'Content-Type': 'application/json' };
685
- if (_binding?.id) headers['X-Compose-Workspace-Id'] = _binding.id;
748
+ const workspaceId = getCurrentWorkspaceId() || _binding?.id;
749
+ if (workspaceId) headers['X-Compose-Workspace-Id'] = workspaceId;
686
750
  let payload = null;
687
751
  if (body !== null && body !== undefined) {
688
752
  payload = JSON.stringify(body);
@@ -725,6 +789,17 @@ export async function toolCompleteFeature({ id, commit_sha, tests_pass, files_ch
725
789
  return _postLifecycle(id, 'complete', body);
726
790
  }
727
791
 
792
+ export async function toolBackfillCompletion({ id, commit_sha, tests_pass, files_changed, notes, reason, occurrences }) {
793
+ const body = {};
794
+ if (commit_sha !== undefined) body.commit_sha = commit_sha;
795
+ if (tests_pass !== undefined) body.tests_pass = tests_pass;
796
+ if (files_changed !== undefined) body.files_changed = files_changed;
797
+ if (notes !== undefined) body.notes = notes;
798
+ body.reason = reason;
799
+ body.occurrences = occurrences ?? [];
800
+ return _postLifecycle(id, 'backfill', body);
801
+ }
802
+
728
803
  export async function toolIterationStart({ id, loopType, maxIterations }) {
729
804
  return _postLifecycle(id, 'iteration/start', { loopType, maxIterations });
730
805
  }
@@ -835,11 +910,31 @@ export async function toolJudgmentLedgerAppend(args) {
835
910
  return judgmentLedgerAppend(getTargetRoot(), args);
836
911
  }
837
912
 
913
+ export async function toolJudgmentPersonWrite(args) {
914
+ const { judgmentPersonWrite } = await import('../lib/judgment-writer.js');
915
+ return judgmentPersonWrite(getTargetRoot(), args);
916
+ }
917
+
918
+ export async function toolJudgmentSituationWrite(args) {
919
+ const { judgmentSituationWrite } = await import('../lib/judgment-writer.js');
920
+ return judgmentSituationWrite(getTargetRoot(), args);
921
+ }
922
+
923
+ export async function toolJudgmentGoalWrite(args) {
924
+ const { judgmentGoalWrite } = await import('../lib/judgment-writer.js');
925
+ return judgmentGoalWrite(getTargetRoot(), args);
926
+ }
927
+
838
928
  export async function toolGetJudgmentState() {
839
929
  const { getJudgmentState } = await import('../lib/judgment-writer.js');
840
930
  return getJudgmentState(getTargetRoot());
841
931
  }
842
932
 
933
+ export async function toolGetJudgmentTrace(args = {}) {
934
+ const { getJudgmentTrace } = await import('../lib/judgment-writer.js');
935
+ return getJudgmentTrace(getTargetRoot(), args.slug);
936
+ }
937
+
843
938
  // ---------------------------------------------------------------------------
844
939
  // COMP-MCP-ENFORCE-1 — phase-scoped MCP tool gate (profile × phase)
845
940
  //
@@ -847,26 +942,32 @@ export async function toolGetJudgmentState() {
847
942
  // rewrite its own launch env); bind_session may only NARROW it. The bound
848
943
  // feature anchor (_boundFeatureCode) lets the gate resolve the current phase
849
944
  // on-disk and check that re-permitted mutations target the bound feature.
850
- // All process-global by intent (one MCP child per session).
945
+ // One MCP child may rebind while older calls await; keep policy state per root.
851
946
  // ---------------------------------------------------------------------------
852
947
 
853
- let _sessionProfile = resolveProfile(process.env.COMPOSE_SESSION_PROFILE, null);
854
- let _boundFeatureCode = null;
948
+ const sessionContexts = new Map();
949
+ function sessionContext() {
950
+ const root = getTargetRoot();
951
+ if (!sessionContexts.has(root)) {
952
+ sessionContexts.set(root, { profile: resolveProfile(process.env.COMPOSE_SESSION_PROFILE, null), featureCode: null });
953
+ }
954
+ return sessionContexts.get(root);
955
+ }
855
956
 
856
- export function _getSessionProfile() { return _sessionProfile; }
857
- export function _getBoundFeatureCode() { return _boundFeatureCode; }
957
+ export function _getSessionProfile() { return sessionContext().profile; }
958
+ export function _getBoundFeatureCode() { return sessionContext().featureCode; }
858
959
  /** @internal test seam */
859
960
  export function _testOnly_setSessionContext({ profile, boundFeatureCode } = {}) {
860
- if (profile !== undefined) _sessionProfile = profile;
861
- if (boundFeatureCode !== undefined) _boundFeatureCode = boundFeatureCode;
961
+ if (profile !== undefined) sessionContext().profile = profile;
962
+ if (boundFeatureCode !== undefined) sessionContext().featureCode = boundFeatureCode;
862
963
  }
863
964
 
864
965
  /** The bound feature's current lifecycle phase from vision-state.json, or null. */
865
966
  export function resolveBoundPhase() {
866
- if (!_boundFeatureCode) return null;
967
+ if (!sessionContext().featureCode) return null;
867
968
  try {
868
969
  const { items } = loadVisionState();
869
- const item = items.find((i) => i.lifecycle?.featureCode === _boundFeatureCode);
970
+ const item = items.find((i) => i.lifecycle?.featureCode === sessionContext().featureCode);
870
971
  return item?.lifecycle?.currentPhase ?? null;
871
972
  } catch {
872
973
  return null;
@@ -894,9 +995,9 @@ function _resolveTargetFeatureCode(tool, args = {}) {
894
995
  }
895
996
 
896
997
  function _targetMatchesBoundFeature(tool, args) {
897
- if (!_boundFeatureCode) return false;
998
+ if (!sessionContext().featureCode) return false;
898
999
  const code = _resolveTargetFeatureCode(tool, args);
899
- return code !== null && code === _boundFeatureCode;
1000
+ return code !== null && code === sessionContext().featureCode;
900
1001
  }
901
1002
 
902
1003
  /**
@@ -914,7 +1015,7 @@ export function assertToolPhaseAllowed(tool, args = {}, _testCtx) {
914
1015
  if (!guardOn) return;
915
1016
  if (_overrideOk(args)) return;
916
1017
 
917
- const profile = _testCtx?.profile ?? _sessionProfile;
1018
+ const profile = _testCtx?.profile ?? sessionContext().profile;
918
1019
  const phase = _testCtx?.phase ?? resolveBoundPhase();
919
1020
  const targetMatchesBoundFeature = _testCtx?.targetMatches ?? _targetMatchesBoundFeature(tool, args);
920
1021
 
@@ -931,4 +1032,3 @@ export function assertToolPhaseAllowed(tool, args = {}, _testCtx) {
931
1032
  throw e;
932
1033
  }
933
1034
  }
934
-