@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.
- package/.claude/skills/compose/SKILL.md +42 -88
- package/bin/compose.js +288 -0
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +7 -0
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/judgment-record.schema.json +425 -4
- package/dist/assets/App-PkZzHeMj.js +894 -0
- package/dist/assets/_baseUniq-Bo837sRJ.js +1 -0
- package/dist/assets/arc-BafGpyqE.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-BOBfUsqL.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-Dwodev1a.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-1ntj1-x_.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-CU_bhYag.js} +1 -1
- package/dist/assets/channel-qVK_qn4E.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-p8WsDwnO.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-B8h7-eR0.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-DxeEr98s.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-BYt8F151.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-DGSOVeie.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-B-QdgYR2.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-Du5UAZLs.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-C8JbNBSk.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +1 -0
- package/dist/assets/clone-Pu3RyLUh.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-O1ESaqge.js} +1 -1
- package/dist/assets/dagre-KV5264BT-CPTmFPHw.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-B3PNrWs5.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-Cscfr6vc.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-CSfqZ-TM.js +43 -0
- package/dist/assets/diagram-TYMM5635-Cg4aYS7W.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-_ZqwG5pl.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-C83boxFT.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-CWnIjuEi.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-DrMdxZfH.js +106 -0
- package/dist/assets/graph-Bi99_6Yf.js +331 -0
- package/dist/assets/graph-RE4I7Ty7.js +1 -0
- package/dist/assets/index-Rm2RE-c0.js +123 -0
- package/dist/assets/infoDiagram-42DDH7IO-BLmP4Epr.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-yuWWshKN.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-BOfhaJov.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-Bbolde15.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BSf33zm8.js +1 -0
- package/dist/assets/{linear-Ck7gpa5N.js → linear-AvSTWMqx.js} +1 -1
- package/dist/assets/min-QBM8H4xN.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-BuvgtqIc.js} +7 -7
- package/dist/assets/{mobile-CaoXUwAr.js → mobile-BnXEOE3U.js} +2 -2
- package/dist/assets/pieDiagram-DEJITSTG-DIzF16vh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-D-mbUIjS.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-CEs4kCLd.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-DFsnCr9n.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-BEJYdTjQ.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-BBXs57uY.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BGvLoVAY.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-9LaBTMe0.js +34 -0
- package/dist/assets/wardley-RL74JXVD-P4MEqMTP.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-o-tmxnlC.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-Dpn7V6qk.js +7 -0
- package/dist/index.html +2 -2
- package/lib/bug-escalation.js +30 -4
- package/lib/build.js +777 -52
- package/lib/canon-guard.js +223 -0
- package/lib/canon-registry.js +187 -0
- package/lib/codex-preflight.js +26 -4
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- package/lib/feature-writer.js +9 -0
- package/lib/gsd.js +11 -2
- package/lib/hooks-status.js +32 -3
- package/lib/judgment/store/index.js +158 -0
- package/lib/judgment/store/records.js +183 -24
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-gen.js +370 -21
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-writer.js +2803 -277
- package/lib/lane-gate.js +2 -0
- package/lib/local-claude-connector.js +199 -54
- package/lib/mcp-enforcement.js +21 -35
- package/lib/result-normalizer.js +93 -15
- package/lib/review-normalize.js +4 -0
- package/lib/stratum-mcp-client.js +131 -6
- package/package.json +2 -2
- package/server/compose-mcp-tools.js +15 -1
- package/server/compose-mcp.js +96 -1
- package/server/mcp-tool-policy.js +1 -1
- package/dist/assets/App-BG3ngu8H.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
- package/dist/assets/arc-_v4hR_uD.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
- package/dist/assets/channel-B3U5wFAT.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
- package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
- package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-3ZH5eMcZ.js +0 -119
- package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DEXfKzaS.js +0 -1
- package/dist/assets/map-Czzmt4hB.js +0 -1
- package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
- 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:
|
|
404
|
+
version: 1
|
|
398
405
|
contracts:
|
|
399
|
-
ResearchResult:
|
|
400
|
-
findings: {type: array}
|
|
401
|
-
relevant_files: {type: array}
|
|
402
406
|
DesignResult:
|
|
403
|
-
path:
|
|
404
|
-
|
|
407
|
+
path: string
|
|
408
|
+
words: number
|
|
405
409
|
BlueprintResult:
|
|
406
|
-
path:
|
|
410
|
+
path: string
|
|
407
411
|
ImplementResult:
|
|
408
|
-
files_changed:
|
|
409
|
-
tests_pass:
|
|
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:
|
|
453
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
package/bin/judgment-import.js
CHANGED
|
@@ -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"],
|