@smartmemory/compose 0.3.6-beta → 0.3.7

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 (134) hide show
  1. package/.claude/skills/compose/SKILL.md +42 -88
  2. package/bin/compose.js +288 -0
  3. package/bin/git-hooks/pre-push.template +29 -0
  4. package/bin/judgment-import.js +7 -0
  5. package/contracts/feature-json.schema.json +5 -0
  6. package/contracts/judgment-record.schema.json +425 -4
  7. package/dist/assets/App-PkZzHeMj.js +894 -0
  8. package/dist/assets/_baseUniq-Bo837sRJ.js +1 -0
  9. package/dist/assets/arc-BafGpyqE.js +1 -0
  10. package/dist/assets/architectureDiagram-Q4EWVU46-BOBfUsqL.js +36 -0
  11. package/dist/assets/blockDiagram-DXYQGD6D-Dwodev1a.js +132 -0
  12. package/dist/assets/{browser-BSM23If2.js → browser-1ntj1-x_.js} +6 -6
  13. package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-CU_bhYag.js} +1 -1
  14. package/dist/assets/channel-qVK_qn4E.js +1 -0
  15. package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-p8WsDwnO.js} +1 -1
  16. package/dist/assets/chunk-4TB4RGXK-B8h7-eR0.js +206 -0
  17. package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-DxeEr98s.js} +1 -1
  18. package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-BYt8F151.js} +1 -1
  19. package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-DGSOVeie.js} +1 -1
  20. package/dist/assets/chunk-OYMX7WX6-B-QdgYR2.js +231 -0
  21. package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-Du5UAZLs.js} +1 -1
  22. package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-C8JbNBSk.js} +1 -1
  23. package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +1 -0
  24. package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +1 -0
  25. package/dist/assets/clone-Pu3RyLUh.js +1 -0
  26. package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-O1ESaqge.js} +1 -1
  27. package/dist/assets/dagre-KV5264BT-CPTmFPHw.js +4 -0
  28. package/dist/assets/diagram-5BDNPKRD-B3PNrWs5.js +10 -0
  29. package/dist/assets/diagram-G4DWMVQ6-Cscfr6vc.js +24 -0
  30. package/dist/assets/diagram-MMDJMWI5-CSfqZ-TM.js +43 -0
  31. package/dist/assets/diagram-TYMM5635-Cg4aYS7W.js +24 -0
  32. package/dist/assets/erDiagram-SMLLAGMA-_ZqwG5pl.js +85 -0
  33. package/dist/assets/flowDiagram-DWJPFMVM-C83boxFT.js +162 -0
  34. package/dist/assets/ganttDiagram-T4ZO3ILL-CWnIjuEi.js +292 -0
  35. package/dist/assets/gitGraphDiagram-UUTBAWPF-DrMdxZfH.js +106 -0
  36. package/dist/assets/graph-Bi99_6Yf.js +331 -0
  37. package/dist/assets/graph-RE4I7Ty7.js +1 -0
  38. package/dist/assets/index-Rm2RE-c0.js +123 -0
  39. package/dist/assets/infoDiagram-42DDH7IO-BLmP4Epr.js +2 -0
  40. package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-yuWWshKN.js} +5 -5
  41. package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-BOfhaJov.js} +1 -1
  42. package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-Bbolde15.js} +7 -7
  43. package/dist/assets/katex-DkKDou_j.js +257 -0
  44. package/dist/assets/layout-BSf33zm8.js +1 -0
  45. package/dist/assets/{linear-Ck7gpa5N.js → linear-AvSTWMqx.js} +1 -1
  46. package/dist/assets/min-QBM8H4xN.js +1 -0
  47. package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-BuvgtqIc.js} +7 -7
  48. package/dist/assets/{mobile-CaoXUwAr.js → mobile-BnXEOE3U.js} +2 -2
  49. package/dist/assets/pieDiagram-DEJITSTG-DIzF16vh.js +30 -0
  50. package/dist/assets/quadrantDiagram-34T5L4WZ-D-mbUIjS.js +7 -0
  51. package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-CEs4kCLd.js} +3 -3
  52. package/dist/assets/sankeyDiagram-XADWPNL6-DFsnCr9n.js +10 -0
  53. package/dist/assets/sequenceDiagram-FGHM5R23-BEJYdTjQ.js +157 -0
  54. package/dist/assets/stateDiagram-FHFEXIEX-BBXs57uY.js +1 -0
  55. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +1 -0
  56. package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BGvLoVAY.js} +3 -3
  57. package/dist/assets/vennDiagram-DHZGUBPP-9LaBTMe0.js +34 -0
  58. package/dist/assets/wardley-RL74JXVD-P4MEqMTP.js +162 -0
  59. package/dist/assets/wardleyDiagram-NUSXRM2D-o-tmxnlC.js +20 -0
  60. package/dist/assets/xychartDiagram-5P7HB3ND-Dpn7V6qk.js +7 -0
  61. package/dist/index.html +2 -2
  62. package/lib/bug-escalation.js +30 -4
  63. package/lib/build.js +777 -52
  64. package/lib/canon-guard.js +223 -0
  65. package/lib/canon-registry.js +187 -0
  66. package/lib/codex-preflight.js +26 -4
  67. package/lib/dispatch-ledger.js +301 -0
  68. package/lib/dispatch-metrics.js +236 -0
  69. package/lib/experiment-judge.js +6 -1
  70. package/lib/feature-writer.js +9 -0
  71. package/lib/gsd.js +11 -2
  72. package/lib/hooks-status.js +32 -3
  73. package/lib/judgment/store/index.js +158 -0
  74. package/lib/judgment/store/records.js +183 -24
  75. package/lib/judgment-attest.js +259 -0
  76. package/lib/judgment-gen.js +370 -21
  77. package/lib/judgment-verify.js +153 -0
  78. package/lib/judgment-writer.js +2803 -277
  79. package/lib/lane-gate.js +2 -0
  80. package/lib/local-claude-connector.js +199 -54
  81. package/lib/mcp-enforcement.js +21 -35
  82. package/lib/result-normalizer.js +93 -15
  83. package/lib/review-normalize.js +4 -0
  84. package/lib/stratum-mcp-client.js +131 -6
  85. package/package.json +2 -2
  86. package/server/compose-mcp-tools.js +15 -1
  87. package/server/compose-mcp.js +96 -1
  88. package/server/mcp-tool-policy.js +1 -1
  89. package/dist/assets/App-BG3ngu8H.js +0 -896
  90. package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
  91. package/dist/assets/arc-_v4hR_uD.js +0 -1
  92. package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
  93. package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
  94. package/dist/assets/channel-B3U5wFAT.js +0 -1
  95. package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
  96. package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
  97. package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
  98. package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
  99. package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
  100. package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
  101. package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
  102. package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
  103. package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
  104. package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
  105. package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
  106. package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
  107. package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
  108. package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
  109. package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
  110. package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
  111. package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
  112. package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
  113. package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
  114. package/dist/assets/graph-BXPQrYYB.js +0 -331
  115. package/dist/assets/graph-C9eacEi8.js +0 -1
  116. package/dist/assets/index-3ZH5eMcZ.js +0 -119
  117. package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
  118. package/dist/assets/katex-C5jXJg4s.js +0 -257
  119. package/dist/assets/layout-DEXfKzaS.js +0 -1
  120. package/dist/assets/map-Czzmt4hB.js +0 -1
  121. package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
  122. package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
  123. package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
  124. package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
  125. package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
  126. package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
  127. package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
  128. package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
  129. package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
  130. package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
  131. package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
  132. package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
  133. package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
  134. package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
@@ -391,111 +391,65 @@ If Stratum unavailable, continue with flat prompt chain.
391
391
 
392
392
  Triage is implicit on entry (severity, scope, path) — it gates which steps to run, not a Stratum step itself. Phases 6 (plan), 9 (docs) are folded into `fix` and `ship` respectively.
393
393
 
394
- ### Spec Template
394
+ ### Spec Template (TS engine grammar, post-cutover)
395
+
396
+ The TS engine's spec IR (validated by `stratum/ts/src/ir/validate.js`) differs
397
+ from the retired python grammar: `version` is the number `1`, contracts use
398
+ string shorthand, `flows.entry` names the entry flow, and steps use
399
+ `do`/`out`/`after`/`ensure`/`attempts` (NOT `function`/`inputs`/`depends_on`).
400
+ MCP `stratum_plan` currently reports grammar violations as a bare
401
+ "spec validation failed" (smartmemory/stratum#26) — get the template right.
395
402
 
396
403
  ```yaml
397
- version: "0.1"
404
+ version: 1
398
405
  contracts:
399
- ResearchResult:
400
- findings: {type: array}
401
- relevant_files: {type: array}
402
406
  DesignResult:
403
- path: {type: string}
404
- word_count: {type: integer}
407
+ path: string
408
+ words: number
405
409
  BlueprintResult:
406
- path: {type: string}
410
+ path: string
407
411
  ImplementResult:
408
- files_changed: {type: array}
409
- tests_pass: {type: boolean}
410
-
411
- functions:
412
- research:
413
- mode: compute
414
- intent: "Explore the codebase with compose-explorer agents and surface patterns relevant to the feature."
415
- input: {description: {type: string}}
416
- output: ResearchResult
417
- ensure:
418
- - "len(result.findings) > 0"
419
- retries: 2
420
-
421
- write_design:
422
- mode: compute
423
- intent: "Run Phase 1 (and optional Phases 2-3) — explore, gate, write design.md."
424
- input: {description: {type: string}}
425
- output: DesignResult
426
- ensure:
427
- - "file_exists(result.path)"
428
- - "result.word_count > 200"
429
- retries: 2
430
-
431
- write_blueprint:
432
- mode: compute
433
- intent: "Run Phases 4-5 — blueprint, verification. Gate before returning."
434
- input: {description: {type: string}}
435
- output: BlueprintResult
436
- ensure:
437
- - "file_exists(result.path)"
438
- retries: 2
439
-
440
- implement:
441
- mode: compute
442
- intent: "Run Phase 7 — TDD, E2E, review loop, coverage sweep."
443
- input: {description: {type: string}}
444
- output: ImplementResult
445
- ensure:
446
- - "result.tests_pass == True"
447
- - "len(result.files_changed) > 0"
448
- retries: 2
412
+ files_changed: string
413
+ tests_pass: boolean
449
414
 
450
415
  flows:
416
+ entry: compose_feature
451
417
  compose_feature:
452
- input: {description: {type: string}}
453
- output: ImplementResult
418
+ input:
419
+ goal: string
420
+ output:
421
+ from: "${implement.output}"
422
+ contract: ImplementResult
454
423
  steps:
455
- - id: research
456
- function: research
457
- inputs: {description: "$.input.description"}
458
- output_schema:
459
- type: object
460
- required: [findings]
461
- properties:
462
- findings: {type: array, items: {type: string}}
463
- relevant_files: {type: array, items: {type: string}}
464
-
465
424
  - id: write_design
466
- function: write_design
467
- inputs: {description: "$.input.description"}
468
- depends_on: [research]
469
- output_schema:
470
- type: object
471
- required: [path, word_count]
472
- properties:
473
- path: {type: string}
474
- word_count: {type: integer}
425
+ do: "Run Phase 1 (and optional 2-3) — explore, gate, write design.md. Codex design gate to REVIEW CLEAN."
426
+ out: DesignResult
427
+ ensure:
428
+ - file_contains: {path: "docs/features/<code>/design.md", text: "Design"}
429
+ attempts: 3
475
430
 
476
431
  - id: write_blueprint
477
- function: write_blueprint
478
- inputs: {description: "$.input.description"}
479
- depends_on: [write_design]
480
- output_schema:
481
- type: object
482
- required: [path]
483
- properties:
484
- path: {type: string}
432
+ after: [write_design]
433
+ do: "Run Phases 4-5 — blueprint grounded in real code, corrections table, verify every file:line ref."
434
+ out: BlueprintResult
435
+ ensure:
436
+ - file_contains: {path: "docs/features/<code>/blueprint.md", text: "Corrections"}
437
+ attempts: 3
485
438
 
486
439
  - id: implement
487
- function: implement
488
- inputs: {description: "$.input.description"}
489
- depends_on: [write_blueprint]
490
- output_schema:
491
- type: object
492
- required: [files_changed, tests_pass]
493
- properties:
494
- files_changed: {type: array, items: {type: string}}
495
- tests_pass: {type: boolean}
496
-
440
+ after: [write_blueprint]
441
+ do: "Run Phase 7 — TDD, E2E, review loop, coverage sweep."
442
+ out: ImplementResult
443
+ ensure:
444
+ - file_exists: "docs/features/<code>/design.md"
445
+ attempts: 3
497
446
  ```
498
447
 
448
+ Step results are reported via `stratum_step_done` with the step's `dispatchToken`;
449
+ `result.output` must match the step's `out` contract exactly (string shorthand
450
+ means scalar fields — e.g. `files_changed` is one string, not an array; extra
451
+ keys are rejected).
452
+
499
453
  The bugfix flow lives in `compose/pipelines/bug-fix.stratum.yaml` (8 steps + bisect, ships with compose). The CLI entry `compose fix <bug-code>` (in `bin/compose.js`) reads `docs/bugs/<code>/description.md` (scaffolds and exits if missing) and dispatches `runBuild(code, { mode: 'bug', template: 'bug-fix', description })`.
500
454
 
501
455
  ### Hard-bug machinery (COMP-FIX-HARD, 2026-05-01)
package/bin/compose.js CHANGED
@@ -137,6 +137,7 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
137
137
  console.log(' items List vision items from local state (no server)')
138
138
  console.log(' items show <id> Show detail for a specific vision item')
139
139
  console.log(' triage Analyze a feature and recommend build profile')
140
+ console.log(' metrics [--since <duration|ISO>] [--feature <code>] [--json] Report dispatch, settlement, and triage metrics')
140
141
  console.log(' qa-scope Show affected routes from a feature\'s changed files')
141
142
  console.log(' context decisions Show the build decision log (--feature <FC>, --format text|json)')
142
143
  console.log(' gate list List pending gates (--item <id>, --status pending|all|resolved)')
@@ -146,6 +147,7 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
146
147
  console.log(' sync Re-sync global skills from this install (alias of setup)')
147
148
  console.log(' update Pull latest compose, reinstall deps, refresh global skill')
148
149
  console.log(' doctor Check external skill dependencies')
150
+ console.log(' guard Manage the canon guard and drift detection (install|uninstall|status|init|verify [--fix])')
149
151
  console.log(' --version Print compose version, git SHA, and install root')
150
152
  process.exit(0)
151
153
  }
@@ -738,6 +740,17 @@ async function runUpdate(flags) {
738
740
  } else {
739
741
  console.log('Stratum MCP wiring already current')
740
742
  }
743
+
744
+ // Healing .mcp.json only fixes what the NEXT client launch reads. An MCP
745
+ // server the agent already spawned keeps serving the old Stratum build for
746
+ // the rest of the session, so an update can look applied while every
747
+ // stratum_* tool still runs pre-update code. Say so explicitly — this has
748
+ // bitten us before (stale tool contracts surviving a "successful" upgrade).
749
+ if (!wiring.skipped) {
750
+ console.log('')
751
+ console.log('⚠ Restart your MCP client (e.g. /mcp reconnect, or restart Claude Code)')
752
+ console.log(' to pick up the new Stratum server — a running one keeps serving the old build.')
753
+ }
741
754
  }
742
755
 
743
756
  console.log('')
@@ -1898,6 +1911,245 @@ if (cmd === 'hooks') {
1898
1911
  process.exit(1)
1899
1912
  }
1900
1913
 
1914
+ if (cmd === 'guard') {
1915
+ // compose guard {install,uninstall,status,verify} — COMP-CANON-GUARD S4/S5.
1916
+ // Manages the write-time PreToolUse hook registration in .claude/settings.json.
1917
+ // Scoped to compose's own checkout (dogfooding): the hook script uses a
1918
+ // relative import into lib/, so it only works where .claude/ and lib/ are
1919
+ // siblings — this repo. Cross-project install is design Open Question 2.
1920
+ const sub = args[0] || 'status'
1921
+ const { readFileSync: rfSync, writeFileSync: wfSync, existsSync: exSync, mkdirSync: mkSync } = await import('fs')
1922
+ const { join: pjoin } = await import('path')
1923
+ const { installGuardHook, uninstallGuardHook, guardHookStatus, HOOK_COMMAND, HOOK_MATCHER } =
1924
+ await import('../lib/canon-guard.js')
1925
+
1926
+ const projectRoot = PACKAGE_ROOT
1927
+ const claudeDir = pjoin(projectRoot, '.claude')
1928
+ const settingsPath = pjoin(claudeDir, 'settings.json')
1929
+ const hookScript = pjoin(claudeDir, 'hooks', 'canon-guard.mjs')
1930
+
1931
+ function readSettings() {
1932
+ if (!exSync(settingsPath)) return {}
1933
+ try {
1934
+ return JSON.parse(rfSync(settingsPath, 'utf-8'))
1935
+ } catch (e) {
1936
+ console.error(`Error: ${settingsPath} is not valid JSON: ${e.message}`)
1937
+ process.exit(1)
1938
+ }
1939
+ }
1940
+ function writeSettings(obj) {
1941
+ mkSync(claudeDir, { recursive: true })
1942
+ wfSync(settingsPath, JSON.stringify(obj, null, 2) + '\n')
1943
+ }
1944
+
1945
+ if (sub === 'init') {
1946
+ // Establish the first record baseline (trust-on-first-use). This exists
1947
+ // BECAUSE `verify --fix` correctly refuses to stamp records: without a
1948
+ // separate, deliberate bootstrap there would be no way to create the very
1949
+ // first manifest, and drift detection would report every record as `added`
1950
+ // forever. Kept distinct so the one-time act of trusting the current records
1951
+ // is explicit and auditable rather than a side effect of a repair flag.
1952
+ // Reject unrecognised args rather than silently ignoring them: `init` WRITES
1953
+ // a baseline, so a mistyped or unsupported scope flag must never quietly
1954
+ // baseline the wrong workspace.
1955
+ const initExtra = args.slice(1).filter((a) => a !== '--cwd' && !a.startsWith('--cwd='))
1956
+ if (args.slice(1).length > 0) {
1957
+ console.error('Usage: compose guard init')
1958
+ console.error('`guard init` baselines the workspace it is run IN — cd there instead of passing a scope flag.')
1959
+ if (initExtra.length !== args.slice(1).length) {
1960
+ console.error('(--cwd is not supported here: it would write the baseline to a different repo than the one you named.)')
1961
+ }
1962
+ process.exit(1)
1963
+ }
1964
+
1965
+ const { root: cwd } = resolveCwdWithWorkspace(args)
1966
+ const { computeRecordHashes, initManifestExclusive, manifestPathFor, recordFileSet } =
1967
+ await import('../lib/judgment-attest.js')
1968
+
1969
+ // `init` WRITES trust, so be explicit about the destination. The shared
1970
+ // resolver honours COMPOSE_TARGET, which can point somewhere other than the
1971
+ // shell's cwd — never let that be silent for this command.
1972
+ if (resolve(cwd) !== resolve(process.cwd())) {
1973
+ console.log(`Baselining resolved workspace: ${cwd}`)
1974
+ console.log('(resolved via COMPOSE_TARGET / workspace config, not your current directory)')
1975
+ }
1976
+
1977
+ const records = recordFileSet(cwd)
1978
+ if (records.length === 0) {
1979
+ console.log('No judgment records found — nothing to baseline.')
1980
+ process.exit(0)
1981
+ }
1982
+
1983
+ // Existence is decided by the FILESYSTEM and the write is exclusive (O_EXCL).
1984
+ // A parsed-value check was wrong twice over: a manifest whose contents are
1985
+ // literally `null` parses to null and would read as "absent" (overwriting a
1986
+ // real baseline), and a check-then-write race let two initializers both pass
1987
+ // the check. Re-baselining over an existing manifest is the laundering step,
1988
+ // so it must fail on the write itself, not on an advisory look.
1989
+ let hashes
1990
+ try {
1991
+ hashes = computeRecordHashes(cwd)
1992
+ } catch (e) {
1993
+ if (e?.code === 'JUDGMENT_RECORD_MALFORMED') {
1994
+ console.error(`Refusing to baseline: ${e.message}`)
1995
+ console.error('A malformed record cannot be trusted as-is — fix or remove it, then re-run.')
1996
+ process.exit(1)
1997
+ }
1998
+ throw e
1999
+ }
2000
+
2001
+ try {
2002
+ initManifestExclusive(cwd, hashes)
2003
+ } catch (e) {
2004
+ if (e?.code === 'EEXIST') {
2005
+ console.error(`A judgment record baseline already exists: ${manifestPathFor(cwd)}`)
2006
+ console.error('`guard init` will not overwrite it — that would launder any raw record edit.')
2007
+ console.error('Use `compose guard verify --fix` for projection drift, or the judgment_* tools to change records.')
2008
+ process.exit(1)
2009
+ }
2010
+ throw e
2011
+ }
2012
+
2013
+ console.log(`Baselined ${records.length} judgment record${records.length === 1 ? '' : 's'}.`)
2014
+ console.log('These records are trusted AS-IS: there is no prior attestation to verify them against.')
2015
+ console.log('From here, drift detection reports any careless change that does not go through the judgment tools.')
2016
+ console.log(`Commit ${manifestPathFor(cwd)} so the baseline travels with the repo and re-baselining shows up as a reviewable diff.`)
2017
+ process.exit(0)
2018
+ }
2019
+
2020
+ if (sub === 'verify') {
2021
+ const { root: cwd } = resolveCwdWithWorkspace(args)
2022
+ const verifyArgs = args.slice(1)
2023
+ const fix = verifyArgs.includes('--fix')
2024
+ if (verifyArgs.some((arg) => arg !== '--fix') || verifyArgs.filter((arg) => arg === '--fix').length > 1) {
2025
+ console.error('Usage: compose guard verify [--fix]')
2026
+ process.exit(1)
2027
+ }
2028
+
2029
+ const { verifyJudgmentCanon } = await import('../lib/judgment-verify.js')
2030
+ const { regenerateProjections } = await import('../lib/judgment-gen.js')
2031
+ const { computeRecordHashes, writeManifest } = await import('../lib/judgment-attest.js')
2032
+
2033
+ function formatProjectionFinding(finding) {
2034
+ const match = /^(.*) \(([^)]+)\)$/.exec(finding)
2035
+ return match ? `${match[1]} [${match[2]}]` : finding
2036
+ }
2037
+
2038
+ function printDrift(result) {
2039
+ console.error('Judgment canon drift detected:')
2040
+ if (result.treeDrift.length > 0) {
2041
+ console.error(' Tree drift (records-anchored file set):')
2042
+ for (const finding of result.treeDrift) {
2043
+ console.error(` - ${finding.path} [${finding.kind}]`)
2044
+ }
2045
+ }
2046
+ if (result.projectionDrift.length > 0) {
2047
+ console.error(' Projection drift (records-anchored):')
2048
+ for (const finding of result.projectionDrift) {
2049
+ console.error(` - ${formatProjectionFinding(finding)}`)
2050
+ }
2051
+ }
2052
+ if (result.recordDrift.length > 0) {
2053
+ console.error(' Record drift detection (careless changes only):')
2054
+ for (const finding of result.recordDrift) {
2055
+ console.error(` - ${finding.path} [${finding.kind}]`)
2056
+ }
2057
+ }
2058
+ }
2059
+
2060
+ let result = await verifyJudgmentCanon(cwd)
2061
+
2062
+ if (fix) {
2063
+ const projectionDriftBefore = result.projectionDrift
2064
+ if (projectionDriftBefore.length > 0) {
2065
+ regenerateProjections(cwd)
2066
+
2067
+ // `--fix` NEVER writes the record manifest. The earlier "refresh it when
2068
+ // records already passed" branch was both unnecessary and unsafe:
2069
+ // unnecessary because a passing record set already matches the manifest,
2070
+ // and unsafe because verifyJudgmentCanon releases the judgment lock when
2071
+ // it returns, so a raw edit landing between that verdict and the rewrite
2072
+ // would be stamped — laundering a hand-edit through the repair flag,
2073
+ // exactly what R1 forbids. Projections are derived and safe to
2074
+ // regenerate; records are not, so `--fix` simply does not touch them.
2075
+ if (result.recordDrift.length > 0) {
2076
+ console.log('Record drift was deliberately not fixed: --fix cannot bless record edits; use the judgment tools to make record changes.')
2077
+ }
2078
+
2079
+ result = await verifyJudgmentCanon(cwd)
2080
+ const remainingProjectionDrift = new Set(result.projectionDrift)
2081
+ const repaired = projectionDriftBefore.filter((finding) => !remainingProjectionDrift.has(finding))
2082
+ if (repaired.length > 0) {
2083
+ console.log('Fixed projection drift:')
2084
+ for (const finding of repaired) {
2085
+ console.log(` - ${formatProjectionFinding(finding)}`)
2086
+ }
2087
+ }
2088
+ } else {
2089
+ console.log('No projection drift to fix.')
2090
+ if (result.recordDrift.length > 0) {
2091
+ console.log('Record drift was deliberately not fixed: --fix cannot bless record edits; use the judgment tools to make record changes.')
2092
+ }
2093
+ }
2094
+
2095
+ if (result.treeDrift.length > 0) {
2096
+ console.log('Tree drift was not fixed: --fix only regenerates derived projections.')
2097
+ }
2098
+ }
2099
+
2100
+ if (result.ok) {
2101
+ console.log('Judgment canon drift detection passed.')
2102
+ process.exit(0)
2103
+ }
2104
+
2105
+ printDrift(result)
2106
+ process.exit(1)
2107
+ }
2108
+
2109
+ if (sub === 'install') {
2110
+ if (!exSync(hookScript)) {
2111
+ console.error(`Error: hook script missing at ${hookScript}`)
2112
+ console.error('It is a tracked source file — ensure your checkout includes .claude/hooks/canon-guard.mjs')
2113
+ process.exit(1)
2114
+ }
2115
+ const { settings, changed } = installGuardHook(readSettings())
2116
+ if (changed) {
2117
+ writeSettings(settings)
2118
+ console.log('Installed canon-guard PreToolUse hook in .claude/settings.json')
2119
+ } else {
2120
+ console.log('canon-guard hook already installed (current).')
2121
+ }
2122
+ console.log(` matcher: ${HOOK_MATCHER}`)
2123
+ console.log(` command: ${HOOK_COMMAND}`)
2124
+ process.exit(0)
2125
+ }
2126
+
2127
+ if (sub === 'uninstall') {
2128
+ const { settings, changed } = uninstallGuardHook(readSettings())
2129
+ if (changed) {
2130
+ writeSettings(settings)
2131
+ console.log('Removed canon-guard hook from .claude/settings.json')
2132
+ } else {
2133
+ console.log('No canon-guard hook installed.')
2134
+ }
2135
+ process.exit(0)
2136
+ }
2137
+
2138
+ if (!sub || sub === 'status') {
2139
+ const st = guardHookStatus(readSettings())
2140
+ const scriptState = exSync(hookScript) ? 'present' : 'MISSING'
2141
+ if (st.state === 'installed') console.log('canon-guard: installed (current)')
2142
+ else if (st.state === 'stale') console.log('canon-guard: installed (stale — re-run `compose guard install`)')
2143
+ else console.log('canon-guard: absent — run `compose guard install`')
2144
+ console.log(` hook script: ${scriptState} (${hookScript})`)
2145
+ console.log(` guards: docs/judgment/** (Write|Edit|NotebookEdit) — Claude-runtime only`)
2146
+ process.exit(0)
2147
+ }
2148
+
2149
+ console.error(`Unknown guard subcommand: "${sub}". Use: install | uninstall | status | init | verify [--fix]`)
2150
+ process.exit(1)
2151
+ }
2152
+
1901
2153
  if (cmd === 'validate') {
1902
2154
  // compose validate [--scope=feature|project] [--code=CODE] [--block-on=error|warning|info] [--json]
1903
2155
  let scope = 'project'
@@ -3822,6 +4074,42 @@ if (cmd === 'build') {
3822
4074
  console.error(' compose smartmemory sync [--dry-run] [--feature <CODE>]')
3823
4075
  process.exit(1)
3824
4076
 
4077
+ } else if (cmd === 'metrics') {
4078
+ // ---------------------------------------------------------------------------
4079
+ // compose metrics [--since <duration|ISO>] [--feature <code>] [--json]
4080
+ // ---------------------------------------------------------------------------
4081
+ const { root: cwd } = resolveCwdWithWorkspace(args)
4082
+ let since = null
4083
+ let feature = null
4084
+ let json = false
4085
+
4086
+ for (let index = 0; index < args.length; index++) {
4087
+ const arg = args[index]
4088
+ if (arg === '--json') {
4089
+ json = true
4090
+ continue
4091
+ }
4092
+ if (arg === '--since' || arg === '--feature') {
4093
+ const value = args[index + 1]
4094
+ if (!value || value.startsWith('--')) {
4095
+ console.error(`compose metrics: ${arg} requires a value`)
4096
+ process.exit(1)
4097
+ }
4098
+ if (arg === '--since') since = value
4099
+ else feature = value
4100
+ index++
4101
+ continue
4102
+ }
4103
+ console.error(`compose metrics: unknown flag ${arg}`)
4104
+ process.exit(1)
4105
+ }
4106
+
4107
+ const { collectDispatchMetrics, renderDispatchMetrics } = await import('../lib/dispatch-metrics.js')
4108
+ const report = collectDispatchMetrics(cwd, { since, feature })
4109
+ if (json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
4110
+ else process.stdout.write(renderDispatchMetrics(report))
4111
+ process.exit(0)
4112
+
3825
4113
  } else {
3826
4114
  console.error(`Unknown command: ${cmd}`)
3827
4115
  process.exit(1)
@@ -14,12 +14,25 @@
14
14
  # (no flag change needed below). XREF_TARGET_MISSING stays error and blocks.
15
15
 
16
16
  set -u
17
+ HOOK_VERSION="1"
17
18
  COMPOSE_NODE="__COMPOSE_NODE__"
18
19
  COMPOSE_BIN="__COMPOSE_BIN__"
19
20
  COMPOSE_WORKSPACE_ID="__COMPOSE_WORKSPACE_ID__"
20
21
  LOG="${COMPOSE_HOOK_LOG:-.compose/data/pre-push.log}"
21
22
  mkdir -p "$(dirname "$LOG")" 2>/dev/null || true
22
23
 
24
+ # EAGAIN guard: git can hand hooks nonblocking stdout/stderr fds; a large echo
25
+ # (the validate output below) then dies mid-write with "echo: write error:
26
+ # Resource temporarily unavailable" and makes a successful push look failed.
27
+ # O_NONBLOCK lives on the open file description (shared across processes), so a
28
+ # child clearing it fixes the shell's own fds too.
29
+ command -v perl >/dev/null 2>&1 && perl -MFcntl -e '
30
+ for my $fd (1, 2) {
31
+ open(my $fh, ">&=", $fd) or next;
32
+ my $flags = fcntl($fh, F_GETFL, 0) // next;
33
+ fcntl($fh, F_SETFL, $flags & ~O_NONBLOCK);
34
+ }' 2>/dev/null || true
35
+
23
36
  # ── Docs-only detection ──────────────────────────────────────────────────────
24
37
  # Parse the ref updates git feeds on stdin (this MUST run before anything else
25
38
  # touches stdin). If every pushed commit touches only docs/** or *.md, the test
@@ -61,6 +74,22 @@ if [ "$VALIDATE_EXIT" -ne 0 ]; then
61
74
  echo "pre-push: compose validate reported drift (ADVISORY — not blocking). Review with \`compose validate\`." >&2
62
75
  fi
63
76
 
77
+ # ── Judgment drift-detection gate (COMP-CANON-GUARD S5) ─────────────────────
78
+ # This runs for every push, including docs-only pushes: judgment canon is under
79
+ # docs/judgment/**, so placing it inside the test-gate skip would miss the most
80
+ # relevant push type. Accepted residual: git push --no-verify bypasses this hook.
81
+ echo "pre-push: checking judgment canon for drift…" >&2
82
+ "$COMPOSE_NODE" "$COMPOSE_BIN" guard verify >> "$LOG" 2>&1
83
+ GUARD_EXIT=$?
84
+ if [ "$GUARD_EXIT" -ne 0 ]; then
85
+ tail -n 40 "$LOG" >&2
86
+ echo "" >&2
87
+ echo "pre-push: judgment drift detected — push aborted (full log: $LOG)." >&2
88
+ echo "Run \`compose guard verify\` for details." >&2
89
+ exit "$GUARD_EXIT"
90
+ fi
91
+ echo "pre-push: judgment canon drift check green." >&2
92
+
64
93
  # ── Test gate (COMP-RESUME follow-up) ───────────────────────────────────────
65
94
  # Block the push if the suite is red. This is the gate whose absence let a broken
66
95
  # integration test (and a stale-schema consumer bug) reach main — nothing ran the
@@ -29,6 +29,10 @@ import {
29
29
  judgmentJointAdd,
30
30
  judgmentLedgerAppend,
31
31
  } from '../lib/judgment-writer.js';
32
+ import {
33
+ computeRecordHashes,
34
+ writeManifest,
35
+ } from '../lib/judgment-attest.js';
32
36
  import { getJudgmentValidator } from '../lib/judgment/schema.js';
33
37
 
34
38
  const METHODS = ['EXT', 'INT', 'CONSTRUCT', 'ASSERT', 'STRADDLE'];
@@ -538,6 +542,9 @@ export async function runJudgmentImport(cwd, { dryRun = false } = {}) {
538
542
  cpSync(join(staging, 'docs', 'judgment', 'records'), tmpDst, { recursive: true });
539
543
  rename(tmpDst, dst);
540
544
  regenerateProjections(cwd);
545
+ // Trust-on-first-use baseline for the promoted TARGET records. Never use
546
+ // the staging manifest here: its workspace paths are disposable.
547
+ writeManifest(cwd, computeRecordHashes(cwd));
541
548
  }
542
549
 
543
550
  return {
@@ -40,6 +40,11 @@
40
40
  "maximum": 4,
41
41
  "description": "COMP-TRIAGE-5: raw machine tier (0-4) from the front estimator/refinement. Maps to `complexity` via tierToComplexity."
42
42
  },
43
+ "triageConfidence": {
44
+ "type": "string",
45
+ "enum": ["high", "medium", "low"],
46
+ "description": "COMP-TRIAGE-6: confidence from the front scope estimate."
47
+ },
43
48
  "estimateSource": {
44
49
  "type": "string",
45
50
  "enum": ["front", "refined", "escalated"],