@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.
- package/.claude/skills/compose/SKILL.md +42 -88
- package/.compose-deps.json +1 -13
- package/README.md +72 -5
- package/bin/compose.js +754 -347
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +7 -0
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/judgment-record.schema.json +425 -4
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
- package/dist/assets/arc-DY26UIVo.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
- package/dist/assets/clone-DgklGjHm.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
- package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
- package/dist/assets/graph-BNzKGvoy.js +1 -0
- package/dist/assets/graph-CI_1htl0.js +331 -0
- package/dist/assets/index-BEfrNBp8.js +123 -0
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BI8cXFPI.js +1 -0
- package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/min-vPHfnXcC.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
- package/dist/index.html +3 -3
- package/lib/agent-string.js +7 -5
- package/lib/append-integrity.js +81 -0
- package/lib/backfill-evidence.js +109 -0
- package/lib/bug-escalation.js +39 -4
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +1624 -195
- package/lib/canon-guard.js +245 -0
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +291 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +50 -15
- package/lib/colleague/context.js +215 -0
- package/lib/colleague/writeback.js +95 -0
- package/lib/completion-gate.js +1421 -0
- package/lib/completion-writer.js +47 -47
- package/lib/consumer-fanout.js +105 -11
- package/lib/coverage-gate.js +200 -0
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- package/lib/feature-json.js +1 -1
- package/lib/feature-reconciler.js +8 -0
- package/lib/feature-validator.js +64 -1
- package/lib/feature-writer.js +66 -2
- package/lib/fluid/factory.js +167 -0
- package/lib/fluid/ideabox-dates.js +73 -0
- package/lib/fluid/ideabox-migrate.js +154 -0
- package/lib/fluid/ideabox-ops.js +585 -0
- package/lib/fluid/ideabox-view.js +146 -0
- package/lib/fluid/import-ideabox.js +186 -0
- package/lib/fluid/local-provider.js +606 -0
- package/lib/fluid/provider.js +684 -0
- package/lib/fluid/record-shape.js +214 -0
- package/lib/fluid/record-store.js +328 -0
- package/lib/fluid/render-ideabox.js +261 -0
- package/lib/fluid/schema.js +40 -0
- package/lib/fluid/smartmemory-provider.js +1695 -0
- package/lib/gsd.js +63 -14
- package/lib/guard-cli.js +175 -0
- package/lib/guard-custody.js +141 -0
- package/lib/guard-descriptors.js +530 -0
- package/lib/guard-enrol.js +254 -0
- package/lib/health-score.js +1 -1
- package/lib/hooks-status.js +32 -3
- package/lib/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +166 -0
- package/lib/judgment/store/records.js +184 -25
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +375 -22
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-writer.js +2842 -262
- package/lib/lane-gate.js +2 -0
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +250 -54
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- package/lib/mcp-enforcement.js +21 -35
- package/lib/migrate-anon.js +5 -0
- package/lib/migrate-roadmap.js +15 -0
- package/lib/new.js +13 -1
- package/lib/pipeline-compat.js +104 -0
- package/lib/policy-catalog.js +295 -0
- package/lib/policy-check.js +0 -0
- package/lib/process-termination.js +98 -0
- package/lib/resolve-workspace.js +5 -1
- package/lib/result-normalizer.js +428 -153
- package/lib/review-normalize.js +4 -0
- package/lib/roadmap-errors.js +65 -0
- package/lib/roadmap-preservers.js +24 -4
- package/lib/roadmap-residue.js +299 -0
- package/lib/smartmemory-client.js +614 -78
- package/lib/smartmemory-config.js +54 -0
- package/lib/smartmemory-ingest.js +19 -2
- package/lib/step-prompt.js +7 -6
- package/lib/stratum-engine.js +53 -4
- package/lib/stratum-mcp-client.js +391 -31
- package/lib/test-bootstrap.js +31 -0
- package/lib/tool-inventory.js +122 -0
- package/lib/version-check.js +91 -19
- package/lib/vision-writer.js +88 -1
- package/package.json +7 -6
- package/pipelines/bug-fix.stratum.yaml +205 -211
- package/pipelines/build-quick.profiles.json +12 -0
- package/pipelines/build-quick.stratum.yaml +263 -350
- package/pipelines/content.stratum.yaml +81 -77
- package/pipelines/coverage-sweep.stratum.yaml +49 -30
- package/pipelines/plan.stratum.yaml +76 -86
- package/pipelines/refactor.stratum.yaml +125 -125
- package/pipelines/research.stratum.yaml +56 -58
- package/pipelines/review-fix.profiles.json +6 -0
- package/pipelines/review-fix.stratum.yaml +110 -83
- package/presets/team-feature.profiles.json +6 -0
- package/presets/team-feature.stratum.yaml +93 -66
- package/presets/team-research.profiles.json +6 -0
- package/presets/team-research.stratum.yaml +89 -80
- package/presets/team-review.profiles.json +8 -0
- package/presets/team-review.stratum.yaml +98 -80
- package/scripts/cost-census.mjs +70 -0
- package/scripts/guard-sign/compose-guard-sign.sh +62 -0
- package/server/agent-health.js +22 -0
- package/server/agent-hooks.js +14 -1
- package/server/agent-server.js +5 -248
- package/server/agent-spawn.js +3 -4
- package/server/agent-workspace.js +294 -0
- package/server/build-routes.js +6 -5
- package/server/build-stream-bridge.js +53 -0
- package/server/cc-session-watcher.js +4 -1
- package/server/coalescing-buffer.js +7 -1
- package/server/completion-projection.js +228 -0
- package/server/compose-mcp-tools.js +124 -24
- package/server/compose-mcp.js +91 -790
- package/server/decision-event-emit.js +41 -2
- package/server/decision-event-id.js +17 -0
- package/server/decision-events-snapshot.js +3 -0
- package/server/design-routes.js +14 -8
- package/server/feature-scan.js +76 -2
- package/server/file-watcher.js +170 -21
- package/server/ideabox-routes.js +166 -224
- package/server/index.js +70 -100
- package/server/lifecycle-guard.js +240 -10
- package/server/lifecycle-phase-history.js +276 -0
- package/server/maya-routes.js +507 -0
- package/server/mcp-tool-defs.js +940 -0
- package/server/mcp-tool-policy.js +35 -3
- package/server/model-tiers.js +22 -5
- package/server/pipeline-routes.js +21 -11
- package/server/project-root.js +58 -19
- package/server/remote-utils.js +3 -1
- package/server/schema-validator.js +7 -1
- package/server/session-manager.js +5 -6
- package/server/session-routes.js +3 -1
- package/server/stratum-client.js +57 -10
- package/server/stratum-sync.js +6 -3
- package/server/summarizer.js +3 -4
- package/server/supervisor.js +0 -1
- package/server/vision-routes.js +208 -98
- package/server/vision-server.js +86 -23
- package/server/vision-store.js +60 -6
- package/server/vision-utils.js +3 -4
- package/server/workspace-activity.js +18 -0
- package/server/workspace-middleware.js +2 -2
- package/server/workspace-runtime.js +243 -0
- package/server/worktree-gc.js +1 -0
- 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/index-LIwREYgH.css +0 -1
- 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/mobile-CaoXUwAr.js +0 -17
- 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
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
package/bin/compose.js
CHANGED
|
@@ -91,7 +91,12 @@ function resolveCwdWithWorkspace(args) {
|
|
|
91
91
|
// ---------------------------------------------------------------------------
|
|
92
92
|
import { parseTeamFlag } from '../lib/team-flag.js';
|
|
93
93
|
import { loadDeps, checkExternalSkills, printDepReport, buildDepReport, checkExternalBinaries, printBinaryReport, buildBinaryReport } from '../lib/deps.js';
|
|
94
|
-
import {
|
|
94
|
+
import {
|
|
95
|
+
checkLatestVersion,
|
|
96
|
+
checkPackageVersion,
|
|
97
|
+
resolveStratumVersion,
|
|
98
|
+
formatDriftNudge,
|
|
99
|
+
} from '../lib/version-check.js';
|
|
95
100
|
import { computeHooksStatus, formatHookStatusLines, HOOK_MARKERS } from '../lib/hooks-status.js';
|
|
96
101
|
|
|
97
102
|
const [,, cmd, ...args] = process.argv
|
|
@@ -115,38 +120,10 @@ if (cmd === '--version' || cmd === '-V' || cmd === 'version') {
|
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
console.log(
|
|
122
|
-
console.log(' remote Manage remote access: pair, list, revoke, status')
|
|
123
|
-
console.log(' new Kickoff a product (research, brainstorm, roadmap, scaffold)')
|
|
124
|
-
console.log(' import Scan existing project and generate structured analysis')
|
|
125
|
-
console.log(' feature Add a single feature (folder, design seed, ROADMAP entry)')
|
|
126
|
-
console.log(' build Run a feature through the headless lifecycle')
|
|
127
|
-
console.log(' fix Run a bug through the headless bug-fix lifecycle')
|
|
128
|
-
console.log(' gsd Per-task fresh-context dispatch from existing blueprint+Boundary Map')
|
|
129
|
-
console.log(' pipeline View and edit the build pipeline')
|
|
130
|
-
console.log(' roadmap Show roadmap status and next buildable features')
|
|
131
|
-
console.log(' roadmap generate Regenerate ROADMAP.md from feature.json files')
|
|
132
|
-
console.log(' roadmap migrate Extract ROADMAP.md entries into feature.json files')
|
|
133
|
-
console.log(' roadmap check Verify feature.json and ROADMAP.md are in sync')
|
|
134
|
-
console.log(' roadmap xref-sync Pull-reconcile feature.json external links to live state')
|
|
135
|
-
console.log(' roadmap xref-push Push-write GitHub trackers to match expect= (dry-run; --apply to write)')
|
|
136
|
-
console.log(' migrate-anon Promote historical anonymous ROADMAP rows to typed features (interactive)')
|
|
137
|
-
console.log(' items List vision items from local state (no server)')
|
|
138
|
-
console.log(' items show <id> Show detail for a specific vision item')
|
|
139
|
-
console.log(' triage Analyze a feature and recommend build profile')
|
|
140
|
-
console.log(' qa-scope Show affected routes from a feature\'s changed files')
|
|
141
|
-
console.log(' context decisions Show the build decision log (--feature <FC>, --format text|json)')
|
|
142
|
-
console.log(' gate list List pending gates (--item <id>, --status pending|all|resolved)')
|
|
143
|
-
console.log(' gate resolve <id> Resolve a gate (--approve|--revise|--kill, --comment <text>)')
|
|
144
|
-
console.log(' init Initialize Compose in the current project')
|
|
145
|
-
console.log(' setup Install/sync global Compose skills (alias: sync)')
|
|
146
|
-
console.log(' sync Re-sync global skills from this install (alias of setup)')
|
|
147
|
-
console.log(' update Pull latest compose, reinstall deps, refresh global skill')
|
|
148
|
-
console.log(' doctor Check external skill dependencies')
|
|
149
|
-
console.log(' --version Print compose version, git SHA, and install root')
|
|
123
|
+
// Rendered from the single command table (lib/cli-commands.js) so --help can
|
|
124
|
+
// never silently omit a shipped command (COMP-AUDIT-13).
|
|
125
|
+
const { renderHelp } = await import('../lib/cli-commands.js')
|
|
126
|
+
console.log(renderHelp())
|
|
150
127
|
process.exit(0)
|
|
151
128
|
}
|
|
152
129
|
|
|
@@ -566,10 +543,24 @@ async function runInit(flags, cwdOverride) {
|
|
|
566
543
|
}
|
|
567
544
|
}
|
|
568
545
|
|
|
569
|
-
// 8. Copy default pipeline specs if absent
|
|
546
|
+
// 8. Copy default pipeline specs if absent.
|
|
547
|
+
// COMP-PIPELINE-QUARANTINE follow-up: `bug-fix` belongs in this set. `compose
|
|
548
|
+
// fix` checks for pipelines/bug-fix.stratum.yaml, runs init when it is absent,
|
|
549
|
+
// and then fails with "Lifecycle spec not found" because init never copied it —
|
|
550
|
+
// so the command was unavailable in every fresh workspace independently of the
|
|
551
|
+
// spec's dialect. Each spec's `<name>.profiles.json` sidecar travels WITH it:
|
|
552
|
+
// loadPipelineProfiles fails open, so a spec copied without its sidecar runs on
|
|
553
|
+
// bare defaults and silently drops the tool restrictions it declares.
|
|
570
554
|
const pipelinesDir = join(cwd, 'pipelines')
|
|
571
555
|
mkdirSync(pipelinesDir, { recursive: true })
|
|
572
|
-
|
|
556
|
+
const DEFAULT_PIPELINES = [
|
|
557
|
+
'build.stratum.yaml',
|
|
558
|
+
'build-quick.stratum.yaml',
|
|
559
|
+
'bug-fix.stratum.yaml',
|
|
560
|
+
'new.stratum.yaml',
|
|
561
|
+
'plan.stratum.yaml',
|
|
562
|
+
]
|
|
563
|
+
for (const specName of DEFAULT_PIPELINES) {
|
|
573
564
|
const dest = join(pipelinesDir, specName)
|
|
574
565
|
if (!existsSync(dest)) {
|
|
575
566
|
const src = join(PACKAGE_ROOT, 'pipelines', specName)
|
|
@@ -578,6 +569,12 @@ async function runInit(flags, cwdOverride) {
|
|
|
578
569
|
console.log(`Copied default pipeline to ${dest}`)
|
|
579
570
|
}
|
|
580
571
|
}
|
|
572
|
+
const sidecarName = specName.replace(/\.stratum\.ya?ml$/, '.profiles.json')
|
|
573
|
+
const sidecarDest = join(pipelinesDir, sidecarName)
|
|
574
|
+
if (!existsSync(sidecarDest)) {
|
|
575
|
+
const sidecarSrc = join(PACKAGE_ROOT, 'pipelines', sidecarName)
|
|
576
|
+
if (existsSync(sidecarSrc)) copyFileSync(sidecarSrc, sidecarDest)
|
|
577
|
+
}
|
|
581
578
|
}
|
|
582
579
|
|
|
583
580
|
// 9. Sync all compose-owned skills to detected agents
|
|
@@ -738,17 +735,90 @@ async function runUpdate(flags) {
|
|
|
738
735
|
} else {
|
|
739
736
|
console.log('Stratum MCP wiring already current')
|
|
740
737
|
}
|
|
738
|
+
|
|
739
|
+
// Healing .mcp.json only fixes what the NEXT client launch reads. An MCP
|
|
740
|
+
// server the agent already spawned keeps serving the old Stratum build for
|
|
741
|
+
// the rest of the session, so an update can look applied while every
|
|
742
|
+
// stratum_* tool still runs pre-update code. Say so explicitly — this has
|
|
743
|
+
// bitten us before (stale tool contracts surviving a "successful" upgrade).
|
|
744
|
+
if (!wiring.skipped) {
|
|
745
|
+
console.log('')
|
|
746
|
+
console.log('⚠ Restart your MCP client (e.g. /mcp reconnect, or restart Claude Code)')
|
|
747
|
+
console.log(' to pick up the new Stratum server — a running one keeps serving the old build.')
|
|
748
|
+
}
|
|
741
749
|
}
|
|
742
750
|
|
|
743
751
|
console.log('')
|
|
744
752
|
console.log(`compose updated to v${getPkgVersion()}${style === 'git' ? ` @ ${getGitSha(root) || '?'}` : ''}`)
|
|
745
753
|
}
|
|
746
754
|
|
|
755
|
+
/** Hard ceiling on how long the update nudge may delay a command. */
|
|
756
|
+
const NUDGE_BUDGET_MS = 4000
|
|
757
|
+
|
|
758
|
+
/** Stratum is enabled unless this cwd's workspace manifest explicitly opts out.
|
|
759
|
+
* This deliberately does not use resolveWorkspace(): the nudge runs before
|
|
760
|
+
* argument parsing, and workspace resolution may print errors and exit. */
|
|
761
|
+
function isStratumCapabilityEnabled() {
|
|
762
|
+
try {
|
|
763
|
+
const manifest = join(process.cwd(), '.compose', 'compose.json')
|
|
764
|
+
const config = JSON.parse(readFileSync(manifest, 'utf-8'))
|
|
765
|
+
return config?.capabilities?.stratum !== false
|
|
766
|
+
} catch {
|
|
767
|
+
// Absent, unreadable, or malformed means "enabled" for compatibility.
|
|
768
|
+
return true
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** Print a one-line update nudge at session entry points. NOTIFY ONLY —
|
|
773
|
+
* this must never mutate an installation. Compose has ~109 lazy `await import()`
|
|
774
|
+
* sites, so replacing files under a running process would have it load new
|
|
775
|
+
* modules into an old module graph; safe self-update needs a versioned install
|
|
776
|
+
* (COMP-UPDATE-VERSIONED-INSTALL), not a check here.
|
|
777
|
+
*
|
|
778
|
+
* Swallows everything: a courtesy line may never fail the command it precedes. */
|
|
779
|
+
async function emitDriftNudge() {
|
|
780
|
+
try {
|
|
781
|
+
// Only an interactive terminal pays for a registry lookup. Everything else
|
|
782
|
+
// — a spawned CLI, CI, a script — reads cache and stays silent on a miss.
|
|
783
|
+
//
|
|
784
|
+
// This is not an optimization, it is a correctness fix. The nudge sits on
|
|
785
|
+
// the startup path of init/build/plan, and the test suite alone spawns
|
|
786
|
+
// `compose init` dozens of times in parallel with a cold HOME. Every one of
|
|
787
|
+
// those fired a live fetch, and under that concurrency one failed to settle:
|
|
788
|
+
// node reported "Detected unsettled top-level await" at this call and exited
|
|
789
|
+
// non-zero, breaking `compose init` itself. A courtesy line must never be
|
|
790
|
+
// able to do that, and non-interactive callers were never its audience.
|
|
791
|
+
const cacheOnly = !process.stdout.isTTY
|
|
792
|
+
const stratumCurrent = isStratumCapabilityEnabled()
|
|
793
|
+
? resolveStratumVersion(PACKAGE_ROOT)
|
|
794
|
+
: null
|
|
795
|
+
const work = Promise.all([
|
|
796
|
+
checkPackageVersion('@smartmemory/compose', getPkgVersion(), { cacheOnly }),
|
|
797
|
+
stratumCurrent
|
|
798
|
+
? checkPackageVersion('@smartmemory/stratum', stratumCurrent, { cacheOnly })
|
|
799
|
+
: Promise.resolve(null),
|
|
800
|
+
])
|
|
801
|
+
// Belt and braces: whatever happens below, this await settles. The timer is
|
|
802
|
+
// ref'd deliberately — an unref'd one would let the loop drain and reproduce
|
|
803
|
+
// the very unsettled-await exit this guards against.
|
|
804
|
+
const [composeInfo, stratumInfo] = await Promise.race([
|
|
805
|
+
work,
|
|
806
|
+
new Promise((resolve) => setTimeout(() => resolve([null, null]), NUDGE_BUDGET_MS)),
|
|
807
|
+
])
|
|
808
|
+
for (const line of formatDriftNudge({ compose: composeInfo, stratum: stratumInfo })) {
|
|
809
|
+
console.log(line)
|
|
810
|
+
}
|
|
811
|
+
} catch {
|
|
812
|
+
// never break the command this is attached to
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
747
816
|
// ---------------------------------------------------------------------------
|
|
748
817
|
// Command dispatch
|
|
749
818
|
// ---------------------------------------------------------------------------
|
|
750
819
|
|
|
751
820
|
if (cmd === 'init') {
|
|
821
|
+
await emitDriftNudge()
|
|
752
822
|
await runInit(args)
|
|
753
823
|
process.exit(0)
|
|
754
824
|
}
|
|
@@ -1148,38 +1218,122 @@ _This is a seed design doc created by \`compose feature\`. The \`compose build\`
|
|
|
1148
1218
|
process.exit(0)
|
|
1149
1219
|
}
|
|
1150
1220
|
|
|
1221
|
+
if (cmd === 'guard' && ['descriptors', 'sign', 'enrol'].includes(args[0])) {
|
|
1222
|
+
// New signing verbs stay above the canon-guard dispatcher below. They resolve
|
|
1223
|
+
// the target workspace; canon-guard intentionally continues to dogfood this
|
|
1224
|
+
// package root for its own hook state.
|
|
1225
|
+
const sub = args[0]
|
|
1226
|
+
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
1227
|
+
const { runGuardDescriptors, runGuardSign, runGuardEnrol } = await import('../lib/guard-cli.js')
|
|
1228
|
+
const principalAt = args.indexOf('--principal')
|
|
1229
|
+
const principal = principalAt === -1 ? undefined : (args[principalAt + 1] ?? '')
|
|
1230
|
+
const result = sub === 'descriptors' ? await runGuardDescriptors(cwd)
|
|
1231
|
+
: sub === 'sign' ? await runGuardSign(cwd)
|
|
1232
|
+
: await runGuardEnrol(principal === undefined ? {} : { principal }, { emit: (line) => console.log(line) })
|
|
1233
|
+
for (const line of result.lines) console.log(line)
|
|
1234
|
+
process.exit(result.status === 'refused' ? 1 : 0)
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1151
1237
|
if (cmd === 'roadmap') {
|
|
1152
1238
|
const subcmd = args[0]
|
|
1153
1239
|
|
|
1154
1240
|
// compose roadmap generate — regenerate ROADMAP.md from feature.json files,
|
|
1155
1241
|
// converging to a fixed point before finishing.
|
|
1156
1242
|
if (subcmd === 'generate' || subcmd === 'gen') {
|
|
1157
|
-
const { writeRoadmap } = await import('../lib/roadmap-gen.js')
|
|
1158
1243
|
const { checkRoundtrip } = await import('../lib/roadmap-roundtrip.js')
|
|
1244
|
+
const { generateRoadmapFromBase } = await import('../lib/roadmap-gen.js')
|
|
1159
1245
|
const { listFeatures } = await import('../lib/feature-json.js')
|
|
1160
|
-
const { loadExternalPrefixes } = await import('../lib/project-paths.js')
|
|
1246
|
+
const { loadExternalPrefixes, resolveRoadmapPath, loadFeaturesDir } = await import('../lib/project-paths.js')
|
|
1161
1247
|
const { isNarrativeOwned } = await import('../lib/roadmap-config.js')
|
|
1248
|
+
const { readPreservedSections } = await import('../lib/roadmap-preservers.js')
|
|
1249
|
+
const { computeResidue, protectResidue, RoadmapProseLossError } = await import('../lib/roadmap-residue.js')
|
|
1162
1250
|
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
1163
1251
|
// Narrative-owned workspaces (#39): ROADMAP.md is hand-authored, not a render
|
|
1164
|
-
// of feature.json
|
|
1165
|
-
// below would still overwrite the file (or crash if it's absent) — skip the
|
|
1166
|
-
// whole generate path here so the hand-authored file is never touched.
|
|
1252
|
+
// of feature.json — never regenerate or overwrite it.
|
|
1167
1253
|
if (isNarrativeOwned(cwd)) {
|
|
1168
1254
|
console.log('narrative-owned workspace (roadmap.narrative=true) — ROADMAP.md is hand-authored; generate skipped.')
|
|
1169
1255
|
process.exit(0)
|
|
1170
1256
|
}
|
|
1171
|
-
const path =
|
|
1257
|
+
const path = resolveRoadmapPath(cwd)
|
|
1258
|
+
const base = existsSync(path) ? readFileSync(path, 'utf-8') : ''
|
|
1259
|
+
const acceptLoss = args.includes('--accept-loss')
|
|
1260
|
+
const protect = args.includes('--protect')
|
|
1172
1261
|
const externalPrefixes = loadExternalPrefixes(cwd)
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
//
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1262
|
+
const now = new Date().toISOString().slice(0, 10)
|
|
1263
|
+
|
|
1264
|
+
// Guard 1 (COMP-CONFLICT-MERGE): structural preserved-section marker defects
|
|
1265
|
+
// silently drop the content they were meant to protect — an unbalanced open
|
|
1266
|
+
// (typo'd close) or a duplicate id (Map collision). Fail loud, write nothing.
|
|
1267
|
+
try {
|
|
1268
|
+
readPreservedSections(base, { strict: true })
|
|
1269
|
+
} catch (err) {
|
|
1270
|
+
if (err?.code === 'ROADMAP_UNBALANCED_MARKER') {
|
|
1271
|
+
console.error(`Refusing to write ${path}: unbalanced preserved-section marker(s):`)
|
|
1272
|
+
for (const m of err.markers) console.error(` line ${m.lineNo}: open "${m.id}" has no matching close`)
|
|
1273
|
+
console.error(err.remediation)
|
|
1274
|
+
process.exit(1)
|
|
1275
|
+
}
|
|
1276
|
+
if (err?.code === 'ROADMAP_DUPLICATE_MARKER') {
|
|
1277
|
+
console.error(`Refusing to write ${path}: duplicate preserved-section id(s):`)
|
|
1278
|
+
for (const m of err.markers) console.error(` line ${m.lineNo}: id "${m.id}" is used more than once`)
|
|
1279
|
+
console.error(err.remediation)
|
|
1280
|
+
process.exit(1)
|
|
1281
|
+
}
|
|
1282
|
+
throw err
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Honor a configured non-default features directory (paths.features), else a
|
|
1286
|
+
// project using one regenerates from the wrong/empty feature set.
|
|
1287
|
+
const featuresDir = loadFeaturesDir(cwd)
|
|
1288
|
+
const features = listFeatures(cwd, featuresDir)
|
|
1289
|
+
|
|
1290
|
+
// Emit the phase-override drift warning + roadmap_drift audit event, which the
|
|
1291
|
+
// old writeRoadmap-first path produced and the pure roundtrip path suppresses.
|
|
1292
|
+
generateRoadmapFromBase(base, features, { cwd, featuresDir, now })
|
|
1293
|
+
|
|
1294
|
+
// Guard 2: compute the FINAL canonical bytes (fixed point over all passes)
|
|
1295
|
+
// BEFORE writing, then diff the base against exactly what will be written.
|
|
1296
|
+
// The duplicate-heading loss only surfaces on a later pass, so checking the
|
|
1297
|
+
// first candidate would pass cleanly and then write the lossy result.
|
|
1298
|
+
const featureCodes = new Set(features.map(f => f.code))
|
|
1299
|
+
const rt = checkRoundtrip(base, features, { now, externalPrefixes, featuresDir })
|
|
1300
|
+
let finalText = rt.canonical
|
|
1301
|
+
let residue = computeResidue(base, finalText, { featureCodes })
|
|
1302
|
+
|
|
1303
|
+
if (residue.length > 0 && protect) {
|
|
1304
|
+
const protectedBase = protectResidue(base, residue)
|
|
1305
|
+
const rt2 = checkRoundtrip(protectedBase, features, { now, externalPrefixes, featuresDir })
|
|
1306
|
+
const stillLost = computeResidue(protectedBase, rt2.canonical, { featureCodes })
|
|
1307
|
+
if (stillLost.length === 0) {
|
|
1308
|
+
writeFileSync(path, rt2.canonical)
|
|
1309
|
+
console.log(`Generated ${path} — wrapped ${residue.length} line(s) in preserved-section markers`)
|
|
1310
|
+
process.exit(0)
|
|
1311
|
+
}
|
|
1312
|
+
// --protect could not fully account for the loss; fall through to the error
|
|
1313
|
+
// with the residual set so the operator sees what remains.
|
|
1314
|
+
finalText = rt2.canonical
|
|
1315
|
+
residue = stillLost
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
if (residue.length > 0 && !acceptLoss) {
|
|
1319
|
+
const err = new RoadmapProseLossError(residue)
|
|
1320
|
+
console.error(`Refusing to write ${path}: ${err.message}:`)
|
|
1321
|
+
for (const r of err.lines) {
|
|
1322
|
+
console.error(` line ${r.lineNo} (under "${r.nearestHeading ?? '(preamble)'}"): ${r.text}`)
|
|
1323
|
+
}
|
|
1324
|
+
console.error('')
|
|
1325
|
+
console.error(err.remediation)
|
|
1326
|
+
process.exit(1)
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (residue.length > 0 && acceptLoss) {
|
|
1330
|
+
console.log(`--accept-loss: dropping ${residue.length} hand-authored line(s):`)
|
|
1331
|
+
for (const r of residue) console.log(` line ${r.lineNo}: ${r.text}`)
|
|
1182
1332
|
}
|
|
1333
|
+
|
|
1334
|
+
writeFileSync(path, finalText)
|
|
1335
|
+
if (!rt.fixedPoint) console.log(`Generated ${path} (canonicalized over ${rt.passes} passes)`)
|
|
1336
|
+
else console.log(`Generated ${path} from feature.json files`)
|
|
1183
1337
|
process.exit(0)
|
|
1184
1338
|
}
|
|
1185
1339
|
|
|
@@ -1579,7 +1733,7 @@ if (cmd === 'record-completion') {
|
|
|
1579
1733
|
//
|
|
1580
1734
|
// Flags:
|
|
1581
1735
|
// --commit-sha=<sha> required; full 40-char hex SHA (Decision 9)
|
|
1582
|
-
// --tests-pass=<bool> default
|
|
1736
|
+
// --tests-pass=<bool> REQUIRED (true|false) — no default (COMP-COMPLETION-GATE)
|
|
1583
1737
|
// --notes=<string> optional
|
|
1584
1738
|
// --files-changed-from-stdin read newline-separated paths from stdin
|
|
1585
1739
|
// --no-status set_status: false (don't flip status to COMPLETE)
|
|
@@ -1624,7 +1778,8 @@ if (cmd === 'record-completion') {
|
|
|
1624
1778
|
console.log('')
|
|
1625
1779
|
console.log('Options:')
|
|
1626
1780
|
console.log(' --commit-sha=<sha> Full 40-char hex SHA (required)')
|
|
1627
|
-
console.log(' --tests-pass=<bool> Whether tests passed
|
|
1781
|
+
console.log(' --tests-pass=<bool> Whether tests passed — REQUIRED, no default')
|
|
1782
|
+
console.log(' (configure guard.testCommand to attest by running instead)')
|
|
1628
1783
|
console.log(' --notes=<string> Optional provenance notes')
|
|
1629
1784
|
console.log(' --files-changed-from-stdin Read newline-separated repo-relative paths from stdin')
|
|
1630
1785
|
console.log(' --no-status Do not flip feature status to COMPLETE')
|
|
@@ -1647,8 +1802,13 @@ if (cmd === 'record-completion') {
|
|
|
1647
1802
|
process.exit(1)
|
|
1648
1803
|
}
|
|
1649
1804
|
|
|
1650
|
-
// Parse tests-pass: default
|
|
1651
|
-
|
|
1805
|
+
// Parse tests-pass. COMP-COMPLETION-GATE (BREAKING): there is NO default.
|
|
1806
|
+
// This used to default to `true`, which meant every completion recorded from
|
|
1807
|
+
// the CLI attested a test run the operator may never have made — the record
|
|
1808
|
+
// said "tests passed" on the strength of nothing. An attestation nobody made
|
|
1809
|
+
// is worse than an absent one, so the flag is now required unless a
|
|
1810
|
+
// `guard.testCommand` is configured to attest by running.
|
|
1811
|
+
let testsPass
|
|
1652
1812
|
if (flags['tests-pass'] !== undefined) {
|
|
1653
1813
|
const tp = flags['tests-pass']
|
|
1654
1814
|
if (tp === 'false' || tp === false) testsPass = false
|
|
@@ -1685,7 +1845,48 @@ if (cmd === 'record-completion') {
|
|
|
1685
1845
|
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
1686
1846
|
const { recordCompletion } = await import('../lib/completion-writer.js')
|
|
1687
1847
|
try {
|
|
1688
|
-
|
|
1848
|
+
let result
|
|
1849
|
+
if (completionArgs.set_status === false) {
|
|
1850
|
+
// --no-status records evidence without completing, so it does not drive
|
|
1851
|
+
// the guard. tests_pass still has no default; the writer requires a boolean.
|
|
1852
|
+
if (testsPass === undefined) {
|
|
1853
|
+
console.error(
|
|
1854
|
+
'Error: --tests-pass is required (true|false). It no longer defaults to true — ' +
|
|
1855
|
+
'a completion record must not attest a test run that was never made.'
|
|
1856
|
+
)
|
|
1857
|
+
process.exit(1)
|
|
1858
|
+
}
|
|
1859
|
+
result = await recordCompletion(cwd, completionArgs)
|
|
1860
|
+
} else {
|
|
1861
|
+
const { completionGate } = await import('../lib/completion-gate.js')
|
|
1862
|
+
const gated = await completionGate({
|
|
1863
|
+
featureCode: featureCode,
|
|
1864
|
+
commitSha: commitSha,
|
|
1865
|
+
testsPass: testsPass,
|
|
1866
|
+
filesChanged: filesChanged,
|
|
1867
|
+
notes: flags['notes'] || undefined,
|
|
1868
|
+
force: flags['force'] === true || undefined,
|
|
1869
|
+
idempotencyKey: flags['idempotency-key'] || undefined,
|
|
1870
|
+
workspaceRoot: cwd,
|
|
1871
|
+
})
|
|
1872
|
+
if (!gated.ok) {
|
|
1873
|
+
console.error(`Refused at ${gated.refusedAt}:\n - ${gated.reasons.join('\n - ')}`)
|
|
1874
|
+
if (gated.refusedAt === 'evidence' && testsPass === undefined) {
|
|
1875
|
+
console.error(
|
|
1876
|
+
'\nHint: --tests-pass no longer defaults to true. Pass --tests-pass true|false, ' +
|
|
1877
|
+
'or configure guard.testCommand in .compose/compose.json so the test run itself attests.'
|
|
1878
|
+
)
|
|
1879
|
+
}
|
|
1880
|
+
process.exit(1)
|
|
1881
|
+
}
|
|
1882
|
+
result = gated.result
|
|
1883
|
+
if (gated.partial) {
|
|
1884
|
+
console.warn(
|
|
1885
|
+
`Completed, but a projection failed — ` +
|
|
1886
|
+
gated.failures.map(f => `${f.step}: ${f.message} (recover: ${f.recover})`).join('; ')
|
|
1887
|
+
)
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1689
1890
|
console.log(JSON.stringify({
|
|
1690
1891
|
completion_id: result.completion_id,
|
|
1691
1892
|
idempotent: result.idempotent,
|
|
@@ -1898,6 +2099,276 @@ if (cmd === 'hooks') {
|
|
|
1898
2099
|
process.exit(1)
|
|
1899
2100
|
}
|
|
1900
2101
|
|
|
2102
|
+
if (cmd === 'guard') {
|
|
2103
|
+
// compose guard {install,uninstall,status,verify} — COMP-CANON-GUARD S4/S5.
|
|
2104
|
+
// Manages the write-time PreToolUse hook registration in .claude/settings.json.
|
|
2105
|
+
// Scoped to compose's own checkout (dogfooding): the hook script uses a
|
|
2106
|
+
// relative import into lib/, so it only works where .claude/ and lib/ are
|
|
2107
|
+
// siblings — this repo. Cross-project install is design Open Question 2.
|
|
2108
|
+
const sub = args[0] || 'status'
|
|
2109
|
+
const { readFileSync: rfSync, writeFileSync: wfSync, existsSync: exSync, mkdirSync: mkSync } = await import('fs')
|
|
2110
|
+
const { join: pjoin } = await import('path')
|
|
2111
|
+
const { installGuardHook, uninstallGuardHook, guardHookStatus, HOOK_COMMAND, HOOK_MATCHER } =
|
|
2112
|
+
await import('../lib/canon-guard.js')
|
|
2113
|
+
|
|
2114
|
+
const projectRoot = PACKAGE_ROOT
|
|
2115
|
+
const claudeDir = pjoin(projectRoot, '.claude')
|
|
2116
|
+
const settingsPath = pjoin(claudeDir, 'settings.json')
|
|
2117
|
+
const hookScript = pjoin(claudeDir, 'hooks', 'canon-guard.mjs')
|
|
2118
|
+
|
|
2119
|
+
function readSettings() {
|
|
2120
|
+
if (!exSync(settingsPath)) return {}
|
|
2121
|
+
try {
|
|
2122
|
+
return JSON.parse(rfSync(settingsPath, 'utf-8'))
|
|
2123
|
+
} catch (e) {
|
|
2124
|
+
console.error(`Error: ${settingsPath} is not valid JSON: ${e.message}`)
|
|
2125
|
+
process.exit(1)
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
function writeSettings(obj) {
|
|
2129
|
+
mkSync(claudeDir, { recursive: true })
|
|
2130
|
+
wfSync(settingsPath, JSON.stringify(obj, null, 2) + '\n')
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
if (sub === 'init') {
|
|
2134
|
+
// Establish the first record baseline (trust-on-first-use). This exists
|
|
2135
|
+
// BECAUSE `verify --fix` correctly refuses to stamp records: without a
|
|
2136
|
+
// separate, deliberate bootstrap there would be no way to create the very
|
|
2137
|
+
// first manifest, and drift detection would report every record as `added`
|
|
2138
|
+
// forever. Kept distinct so the one-time act of trusting the current records
|
|
2139
|
+
// is explicit and auditable rather than a side effect of a repair flag.
|
|
2140
|
+
// Reject unrecognised args rather than silently ignoring them: `init` WRITES
|
|
2141
|
+
// a baseline, so a mistyped or unsupported scope flag must never quietly
|
|
2142
|
+
// baseline the wrong workspace.
|
|
2143
|
+
const initExtra = args.slice(1).filter((a) => a !== '--cwd' && !a.startsWith('--cwd='))
|
|
2144
|
+
if (args.slice(1).length > 0) {
|
|
2145
|
+
console.error('Usage: compose guard init')
|
|
2146
|
+
console.error('`guard init` baselines the workspace it is run IN — cd there instead of passing a scope flag.')
|
|
2147
|
+
if (initExtra.length !== args.slice(1).length) {
|
|
2148
|
+
console.error('(--cwd is not supported here: it would write the baseline to a different repo than the one you named.)')
|
|
2149
|
+
}
|
|
2150
|
+
process.exit(1)
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
2154
|
+
const { computeRecordHashes, initManifestExclusive, manifestPathFor, recordFileSet } =
|
|
2155
|
+
await import('../lib/judgment-attest.js')
|
|
2156
|
+
|
|
2157
|
+
// `init` WRITES trust, so be explicit about the destination. The shared
|
|
2158
|
+
// resolver honours COMPOSE_TARGET, which can point somewhere other than the
|
|
2159
|
+
// shell's cwd — never let that be silent for this command.
|
|
2160
|
+
if (resolve(cwd) !== resolve(process.cwd())) {
|
|
2161
|
+
console.log(`Baselining resolved workspace: ${cwd}`)
|
|
2162
|
+
console.log('(resolved via COMPOSE_TARGET / workspace config, not your current directory)')
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
const records = recordFileSet(cwd)
|
|
2166
|
+
if (records.length === 0) {
|
|
2167
|
+
console.log('No judgment records found — nothing to baseline.')
|
|
2168
|
+
process.exit(0)
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// Existence is decided by the FILESYSTEM and the write is exclusive (O_EXCL).
|
|
2172
|
+
// A parsed-value check was wrong twice over: a manifest whose contents are
|
|
2173
|
+
// literally `null` parses to null and would read as "absent" (overwriting a
|
|
2174
|
+
// real baseline), and a check-then-write race let two initializers both pass
|
|
2175
|
+
// the check. Re-baselining over an existing manifest is the laundering step,
|
|
2176
|
+
// so it must fail on the write itself, not on an advisory look.
|
|
2177
|
+
let hashes
|
|
2178
|
+
try {
|
|
2179
|
+
hashes = computeRecordHashes(cwd)
|
|
2180
|
+
} catch (e) {
|
|
2181
|
+
if (e?.code === 'JUDGMENT_RECORD_MALFORMED') {
|
|
2182
|
+
console.error(`Refusing to baseline: ${e.message}`)
|
|
2183
|
+
console.error('A malformed record cannot be trusted as-is — fix or remove it, then re-run.')
|
|
2184
|
+
process.exit(1)
|
|
2185
|
+
}
|
|
2186
|
+
throw e
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
try {
|
|
2190
|
+
initManifestExclusive(cwd, hashes)
|
|
2191
|
+
} catch (e) {
|
|
2192
|
+
if (e?.code === 'EEXIST') {
|
|
2193
|
+
console.error(`A judgment record baseline already exists: ${manifestPathFor(cwd)}`)
|
|
2194
|
+
console.error('`guard init` will not overwrite it — that would launder any raw record edit.')
|
|
2195
|
+
console.error('Use `compose guard verify --fix` for projection drift, or the judgment_* tools to change records.')
|
|
2196
|
+
process.exit(1)
|
|
2197
|
+
}
|
|
2198
|
+
throw e
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
console.log(`Baselined ${records.length} judgment record${records.length === 1 ? '' : 's'}.`)
|
|
2202
|
+
console.log('These records are trusted AS-IS: there is no prior attestation to verify them against.')
|
|
2203
|
+
console.log('From here, drift detection reports any careless change that does not go through the judgment tools.')
|
|
2204
|
+
console.log(`Commit ${manifestPathFor(cwd)} so the baseline travels with the repo and re-baselining shows up as a reviewable diff.`)
|
|
2205
|
+
process.exit(0)
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
if (sub === 'verify') {
|
|
2209
|
+
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
2210
|
+
const verifyArgs = args.slice(1)
|
|
2211
|
+
const fix = verifyArgs.includes('--fix')
|
|
2212
|
+
if (verifyArgs.some((arg) => arg !== '--fix') || verifyArgs.filter((arg) => arg === '--fix').length > 1) {
|
|
2213
|
+
console.error('Usage: compose guard verify [--fix]')
|
|
2214
|
+
process.exit(1)
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
const { verifyJudgmentCanon } = await import('../lib/judgment-verify.js')
|
|
2218
|
+
const { regenerateProjections } = await import('../lib/judgment-gen.js')
|
|
2219
|
+
const { computeRecordHashes, writeManifest } = await import('../lib/judgment-attest.js')
|
|
2220
|
+
|
|
2221
|
+
function formatProjectionFinding(finding) {
|
|
2222
|
+
const match = /^(.*) \(([^)]+)\)$/.exec(finding)
|
|
2223
|
+
return match ? `${match[1]} [${match[2]}]` : finding
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
function printDrift(result) {
|
|
2227
|
+
console.error('Judgment canon drift detected:')
|
|
2228
|
+
if (result.treeDrift.length > 0) {
|
|
2229
|
+
console.error(' Tree drift (records-anchored file set):')
|
|
2230
|
+
for (const finding of result.treeDrift) {
|
|
2231
|
+
console.error(` - ${finding.path} [${finding.kind}]`)
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
if (result.projectionDrift.length > 0) {
|
|
2235
|
+
console.error(' Projection drift (records-anchored):')
|
|
2236
|
+
for (const finding of result.projectionDrift) {
|
|
2237
|
+
console.error(` - ${formatProjectionFinding(finding)}`)
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
if (result.recordDrift.length > 0) {
|
|
2241
|
+
console.error(' Record drift detection (careless changes only):')
|
|
2242
|
+
for (const finding of result.recordDrift) {
|
|
2243
|
+
console.error(` - ${finding.path} [${finding.kind}]`)
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
let result = await verifyJudgmentCanon(cwd)
|
|
2249
|
+
|
|
2250
|
+
if (fix) {
|
|
2251
|
+
const projectionDriftBefore = result.projectionDrift
|
|
2252
|
+
if (projectionDriftBefore.length > 0) {
|
|
2253
|
+
regenerateProjections(cwd)
|
|
2254
|
+
|
|
2255
|
+
// `--fix` NEVER writes the record manifest. The earlier "refresh it when
|
|
2256
|
+
// records already passed" branch was both unnecessary and unsafe:
|
|
2257
|
+
// unnecessary because a passing record set already matches the manifest,
|
|
2258
|
+
// and unsafe because verifyJudgmentCanon releases the judgment lock when
|
|
2259
|
+
// it returns, so a raw edit landing between that verdict and the rewrite
|
|
2260
|
+
// would be stamped — laundering a hand-edit through the repair flag,
|
|
2261
|
+
// exactly what R1 forbids. Projections are derived and safe to
|
|
2262
|
+
// regenerate; records are not, so `--fix` simply does not touch them.
|
|
2263
|
+
if (result.recordDrift.length > 0) {
|
|
2264
|
+
console.log('Record drift was deliberately not fixed: --fix cannot bless record edits; use the judgment tools to make record changes.')
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
result = await verifyJudgmentCanon(cwd)
|
|
2268
|
+
const remainingProjectionDrift = new Set(result.projectionDrift)
|
|
2269
|
+
const repaired = projectionDriftBefore.filter((finding) => !remainingProjectionDrift.has(finding))
|
|
2270
|
+
if (repaired.length > 0) {
|
|
2271
|
+
console.log('Fixed projection drift:')
|
|
2272
|
+
for (const finding of repaired) {
|
|
2273
|
+
console.log(` - ${formatProjectionFinding(finding)}`)
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
} else {
|
|
2277
|
+
console.log('No projection drift to fix.')
|
|
2278
|
+
if (result.recordDrift.length > 0) {
|
|
2279
|
+
console.log('Record drift was deliberately not fixed: --fix cannot bless record edits; use the judgment tools to make record changes.')
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
if (result.treeDrift.length > 0) {
|
|
2284
|
+
console.log('Tree drift was not fixed: --fix only regenerates derived projections.')
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
if (result.ok) {
|
|
2289
|
+
console.log('Judgment canon drift detection passed.')
|
|
2290
|
+
process.exit(0)
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
printDrift(result)
|
|
2294
|
+
process.exit(1)
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
if (sub === 'install') {
|
|
2298
|
+
if (!exSync(hookScript)) {
|
|
2299
|
+
console.error(`Error: hook script missing at ${hookScript}`)
|
|
2300
|
+
console.error('It is a tracked source file — ensure your checkout includes .claude/hooks/canon-guard.mjs')
|
|
2301
|
+
process.exit(1)
|
|
2302
|
+
}
|
|
2303
|
+
const { settings, changed } = installGuardHook(readSettings())
|
|
2304
|
+
if (changed) {
|
|
2305
|
+
writeSettings(settings)
|
|
2306
|
+
console.log('Installed canon-guard PreToolUse hook in .claude/settings.json')
|
|
2307
|
+
} else {
|
|
2308
|
+
console.log('canon-guard hook already installed (current).')
|
|
2309
|
+
}
|
|
2310
|
+
console.log(` matcher: ${HOOK_MATCHER}`)
|
|
2311
|
+
console.log(` command: ${HOOK_COMMAND}`)
|
|
2312
|
+
process.exit(0)
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
if (sub === 'uninstall') {
|
|
2316
|
+
const { settings, changed } = uninstallGuardHook(readSettings())
|
|
2317
|
+
if (changed) {
|
|
2318
|
+
writeSettings(settings)
|
|
2319
|
+
console.log('Removed canon-guard hook from .claude/settings.json')
|
|
2320
|
+
} else {
|
|
2321
|
+
console.log('No canon-guard hook installed.')
|
|
2322
|
+
}
|
|
2323
|
+
process.exit(0)
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
if (!sub || sub === 'status') {
|
|
2327
|
+
const st = guardHookStatus(readSettings())
|
|
2328
|
+
const scriptState = exSync(hookScript) ? 'present' : 'MISSING'
|
|
2329
|
+
if (st.state === 'installed') console.log('canon-guard: installed (current)')
|
|
2330
|
+
else if (st.state === 'stale') console.log('canon-guard: installed (stale — re-run `compose guard install`)')
|
|
2331
|
+
else console.log('canon-guard: absent — run `compose guard install`')
|
|
2332
|
+
console.log(` hook script: ${scriptState} (${hookScript})`)
|
|
2333
|
+
// Derived from the registry, never hardcoded: a literal string here
|
|
2334
|
+
// silently under-reported the guarded set as soon as COMP-CANON-OVERRIDE
|
|
2335
|
+
// widened it. "Claude-runtime only" is not modesty — Bash and Codex writes
|
|
2336
|
+
// never reach this hook (drift detection, not enforcement).
|
|
2337
|
+
const { guardedDisplaysFor } = await import('../lib/canon-registry.js')
|
|
2338
|
+
for (const pattern of guardedDisplaysFor('hook')) {
|
|
2339
|
+
console.log(` guards: ${pattern} (Write|Edit|NotebookEdit) — Claude-runtime only`)
|
|
2340
|
+
}
|
|
2341
|
+
// `compose guard status` must keep working outside a workspace (it always
|
|
2342
|
+
// has, for the canon-guard hook state above); only the newer `signing:`
|
|
2343
|
+
// block needs a resolved workspace, so resolve it tolerantly here instead
|
|
2344
|
+
// of going through resolveCwdWithWorkspace -> dieOnWorkspaceError.
|
|
2345
|
+
let signingWorkspaceRoot = null
|
|
2346
|
+
try {
|
|
2347
|
+
const wsId = getWorkspaceFlag(args)
|
|
2348
|
+
signingWorkspaceRoot = resolveWorkspace({ workspaceId: wsId === '__COMPOSE_WORKSPACE_ID__' ? null : wsId }).root
|
|
2349
|
+
} catch { /* no workspace resolved — signing block is skipped below */ }
|
|
2350
|
+
|
|
2351
|
+
if (!signingWorkspaceRoot) {
|
|
2352
|
+
console.log('signing: (no workspace resolved — run inside a compose workspace or pass --workspace)')
|
|
2353
|
+
if (args.includes('--prune')) process.exit(1)
|
|
2354
|
+
process.exit(0)
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
const { pruneGuardGenerations, signingStatusLines } = await import('../lib/guard-cli.js')
|
|
2358
|
+
if (args.includes('--prune')) {
|
|
2359
|
+
const pruned = await pruneGuardGenerations(signingWorkspaceRoot)
|
|
2360
|
+
for (const line of pruned.lines) console.log(line)
|
|
2361
|
+
if (pruned.status === 'refused') process.exit(1)
|
|
2362
|
+
}
|
|
2363
|
+
const signing = await signingStatusLines(signingWorkspaceRoot, { coverage: args.includes('--coverage') })
|
|
2364
|
+
for (const line of signing.lines) console.log(line)
|
|
2365
|
+
process.exit(0)
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
console.error(`Unknown guard subcommand: "${sub}". Use: install | uninstall | status [--prune] [--coverage] | init | verify [--fix] | enrol | sign | descriptors`)
|
|
2369
|
+
process.exit(1)
|
|
2370
|
+
}
|
|
2371
|
+
|
|
1901
2372
|
if (cmd === 'validate') {
|
|
1902
2373
|
// compose validate [--scope=feature|project] [--code=CODE] [--block-on=error|warning|info] [--json]
|
|
1903
2374
|
let scope = 'project'
|
|
@@ -2105,6 +2576,7 @@ if (cmd === 'experiment') {
|
|
|
2105
2576
|
}
|
|
2106
2577
|
|
|
2107
2578
|
if (cmd === 'build') {
|
|
2579
|
+
await emitDriftNudge()
|
|
2108
2580
|
// Parse --cwd <path> for cross-repo builds
|
|
2109
2581
|
let agentWorkDir = null
|
|
2110
2582
|
const cwdIdx = args.indexOf('--cwd')
|
|
@@ -2138,6 +2610,29 @@ if (cmd === 'build') {
|
|
|
2138
2610
|
console.error('Error: --template requires a name argument')
|
|
2139
2611
|
process.exit(1)
|
|
2140
2612
|
}
|
|
2613
|
+
// COMP-PIPELINE-QUARANTINE round 3: refuse templates that belong to another
|
|
2614
|
+
// command's runner. `compose build` is feature mode, which sends
|
|
2615
|
+
// {featureCode, description, implementer_agent, reviewer_agent}; these four
|
|
2616
|
+
// specs declare a different runner's envelope (task / projectName+intent /
|
|
2617
|
+
// gateCommands), so selecting one here resolves the spec and then fails at
|
|
2618
|
+
// plan time with an unhelpful error. Point at the command that can drive it.
|
|
2619
|
+
const MODE_BOUND_TEMPLATES = {
|
|
2620
|
+
'bug-fix': 'compose fix <bug-code>',
|
|
2621
|
+
plan: 'compose plan "<intent>"',
|
|
2622
|
+
new: 'compose new',
|
|
2623
|
+
gsd: 'compose gsd <feature-code>',
|
|
2624
|
+
}
|
|
2625
|
+
// Compare the BASENAME: resolveTemplatePath builds `<cwd>/pipelines/<name>.stratum.yaml`
|
|
2626
|
+
// via join(), which normalizes away a leading `./`, so `--template ./bug-fix`
|
|
2627
|
+
// resolved the same spec while slipping past an exact-string check.
|
|
2628
|
+
const templateKey = basename(String(templateValue)).replace(/\.stratum\.ya?ml$/, '')
|
|
2629
|
+
if (MODE_BOUND_TEMPLATES[templateKey]) {
|
|
2630
|
+
console.error(
|
|
2631
|
+
`Error: --template ${templateValue} is not a build template — it belongs to ` +
|
|
2632
|
+
`\`${MODE_BOUND_TEMPLATES[templateKey]}\`, which supplies the inputs it declares.`
|
|
2633
|
+
)
|
|
2634
|
+
process.exit(1)
|
|
2635
|
+
}
|
|
2141
2636
|
templateName = templateValue
|
|
2142
2637
|
}
|
|
2143
2638
|
if (teamTemplate && !templateName) {
|
|
@@ -2185,10 +2680,16 @@ if (cmd === 'build') {
|
|
|
2185
2680
|
}
|
|
2186
2681
|
}
|
|
2187
2682
|
|
|
2683
|
+
const { root: buildCwd } = resolveCwdWithWorkspace(args)
|
|
2684
|
+
|
|
2188
2685
|
// Multiple codes: compose build FEAT-1 FEAT-2 FEAT-3
|
|
2189
2686
|
const isMulti = featureCodes.length > 1
|
|
2190
|
-
// Single prefix: compose build STRAT-COMP (no trailing digit)
|
|
2191
|
-
|
|
2687
|
+
// Single prefix: compose build STRAT-COMP (no trailing digit). An exact
|
|
2688
|
+
// feature on disk always wins over the prefix heuristic — codes like
|
|
2689
|
+
// COMP-SEMVER-STRICT have no trailing digit but are single features.
|
|
2690
|
+
const isExactFeature = featureCodes.length === 1 && featureCode &&
|
|
2691
|
+
existsSync(join(buildCwd, 'docs', 'features', featureCode, 'feature.json'))
|
|
2692
|
+
const isPrefix = featureCodes.length === 1 && featureCode && !isExactFeature && !/\d$/.test(featureCode)
|
|
2192
2693
|
const isBatch = all || isPrefix || isMulti
|
|
2193
2694
|
|
|
2194
2695
|
if (abort && isBatch) {
|
|
@@ -2261,7 +2762,6 @@ if (cmd === 'build') {
|
|
|
2261
2762
|
// and there's no bundled preset fallback — so when --quick is requested and
|
|
2262
2763
|
// that file is absent, treat it as init-needed (runInit re-seeds it) rather
|
|
2263
2764
|
// than letting runBuild fail later with "Lifecycle spec not found".
|
|
2264
|
-
const { root: buildCwd } = resolveCwdWithWorkspace(args)
|
|
2265
2765
|
const needsInit =
|
|
2266
2766
|
!existsSync(join(buildCwd, '.compose', 'compose.json')) ||
|
|
2267
2767
|
!existsSync(join(buildCwd, 'pipelines', 'build.stratum.yaml')) ||
|
|
@@ -2374,7 +2874,11 @@ if (cmd === 'build') {
|
|
|
2374
2874
|
const { root: fixCwd } = resolveCwdWithWorkspace(args)
|
|
2375
2875
|
if (!existsSync(join(fixCwd, '.compose', 'compose.json')) || !existsSync(join(fixCwd, 'pipelines', 'bug-fix.stratum.yaml'))) {
|
|
2376
2876
|
console.log('Running compose init...\n')
|
|
2377
|
-
|
|
2877
|
+
// Thread the RESOLVED workspace root, exactly as the build path does. Without
|
|
2878
|
+
// it runInit seeds process.cwd(), which differs from fixCwd when `compose fix`
|
|
2879
|
+
// is invoked from a subdirectory — the guard checks fixCwd but the seed lands
|
|
2880
|
+
// elsewhere, so the run still fails with the same missing-spec error.
|
|
2881
|
+
await runInit(args.filter(a => a.startsWith('--')), fixCwd)
|
|
2378
2882
|
console.log('')
|
|
2379
2883
|
}
|
|
2380
2884
|
|
|
@@ -2451,6 +2955,7 @@ if (cmd === 'build') {
|
|
|
2451
2955
|
})
|
|
2452
2956
|
})
|
|
2453
2957
|
} else if (cmd === 'plan') {
|
|
2958
|
+
await emitDriftNudge()
|
|
2454
2959
|
// compose plan "<intent>" — runs the plan.stratum.yaml product-planning
|
|
2455
2960
|
// lifecycle (explore_design → plan → ship). Thin delegation to runBuild() with
|
|
2456
2961
|
// template='plan', mode='plan'. Derives a PLAN-<slug> code from the intent
|
|
@@ -2801,306 +3306,82 @@ if (cmd === 'build') {
|
|
|
2801
3306
|
}
|
|
2802
3307
|
process.exit(1)
|
|
2803
3308
|
})
|
|
2804
|
-
} else if (cmd === '
|
|
3309
|
+
} else if (cmd === 'judgment') {
|
|
2805
3310
|
// ---------------------------------------------------------------------------
|
|
2806
|
-
// compose
|
|
3311
|
+
// compose judgment trace <slug> — causal ancestry of a position
|
|
3312
|
+
// (COMP-JUDGMENT-PRECEDENT slice A). Read-only. The store has always persisted
|
|
3313
|
+
// revision chains and the `supersedes` ref; nothing surfaced them.
|
|
2807
3314
|
// ---------------------------------------------------------------------------
|
|
2808
|
-
const
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
if (existsSync(cfgPath)) {
|
|
2815
|
-
try { return JSON.parse(readFileSync(cfgPath, 'utf-8')) } catch {}
|
|
2816
|
-
}
|
|
2817
|
-
return {}
|
|
2818
|
-
}
|
|
2819
|
-
function getIdeaboxRelPath(cwd) {
|
|
2820
|
-
return loadComposeConfig(cwd)?.paths?.ideabox || 'docs/product/ideabox.md'
|
|
2821
|
-
}
|
|
2822
|
-
const ibConfig = loadComposeConfig(ibCwd)
|
|
2823
|
-
|
|
2824
|
-
const {
|
|
2825
|
-
parseIdeabox: _parseIdeabox,
|
|
2826
|
-
serializeIdeabox: _serializeIdeabox,
|
|
2827
|
-
addIdea: _addIdea,
|
|
2828
|
-
promoteIdea: _promoteIdea,
|
|
2829
|
-
killIdea: _killIdea,
|
|
2830
|
-
setPriority: _setPriority,
|
|
2831
|
-
loadLens: _loadLens,
|
|
2832
|
-
readIdeabox: _readIdeabox,
|
|
2833
|
-
writeIdeabox: _writeIdeabox,
|
|
2834
|
-
addDiscussion: _addDiscussion,
|
|
2835
|
-
} = await import('../lib/ideabox.js')
|
|
2836
|
-
|
|
2837
|
-
const ibRelPath = getIdeaboxRelPath(ibCwd)
|
|
2838
|
-
// ibFullPath is used for direct file ops (existsSync/mkdirSync/writeFileSync/
|
|
2839
|
-
// display); resolve it absolute-safe. ibRelPath stays relative because the
|
|
2840
|
-
// lib readIdeabox/writeIdeabox readers re-join it under ibCwd.
|
|
2841
|
-
const { resolveIdeaboxPath: _resolveIdeaboxPathCli } = await import('../lib/project-paths.js')
|
|
2842
|
-
const ibFullPath = _resolveIdeaboxPathCli(ibCwd)
|
|
2843
|
-
|
|
2844
|
-
if (!ibSubcmd || ibSubcmd === '--help' || ibSubcmd === '-h') {
|
|
2845
|
-
console.log('Usage: compose ideabox <subcommand>')
|
|
2846
|
-
console.log('')
|
|
2847
|
-
console.log('Subcommands:')
|
|
2848
|
-
console.log(' add "<title>" Add a new idea')
|
|
2849
|
-
console.log(' list List all ideas')
|
|
2850
|
-
console.log(' promote <ID> Mark idea as PROMOTED (creates feature folder)')
|
|
2851
|
-
console.log(' kill <ID> "<reason>" Move idea to Killed Ideas')
|
|
2852
|
-
console.log(' pri <ID> <P0|P1|P2> Set priority')
|
|
2853
|
-
console.log(' discuss <ID> "<comment>" Add a discussion comment')
|
|
2854
|
-
console.log(' triage [--lens <name>] Walk untriaged ideas and assign priorities')
|
|
2855
|
-
process.exit(0)
|
|
3315
|
+
// `args` excludes the command itself (const [,, cmd, ...args]), so the
|
|
3316
|
+
// subcommand is args[0] — same shape as `subcmd = args[0]` elsewhere.
|
|
3317
|
+
const sub = args[0]
|
|
3318
|
+
if (sub !== 'trace') {
|
|
3319
|
+
console.log('Usage: compose judgment trace <slug> [--json]')
|
|
3320
|
+
process.exit(sub ? 1 : 0)
|
|
2856
3321
|
}
|
|
2857
|
-
|
|
2858
|
-
if (
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
console.error('Usage: compose ideabox add "<title>" [--source "..."] [--desc "..."] [--cluster "..."]')
|
|
2862
|
-
process.exit(1)
|
|
2863
|
-
}
|
|
2864
|
-
// Parse optional flags
|
|
2865
|
-
const sourceIdx = args.indexOf('--source')
|
|
2866
|
-
const descIdx = args.indexOf('--desc')
|
|
2867
|
-
const clusterIdx = args.indexOf('--cluster')
|
|
2868
|
-
const tagsIdx = args.indexOf('--tags')
|
|
2869
|
-
const source = sourceIdx !== -1 ? args[sourceIdx + 1] : ''
|
|
2870
|
-
const description = descIdx !== -1 ? args[descIdx + 1] : ''
|
|
2871
|
-
const cluster = clusterIdx !== -1 ? args[clusterIdx + 1] : null
|
|
2872
|
-
const tagsRaw = tagsIdx !== -1 ? args[tagsIdx + 1] : ''
|
|
2873
|
-
const tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim().startsWith('#') ? t.trim() : `#${t.trim()}`) : []
|
|
2874
|
-
|
|
2875
|
-
if (!existsSync(ibFullPath)) {
|
|
2876
|
-
const { IDEABOX_TEMPLATE } = await import('../lib/ideabox.js')
|
|
2877
|
-
mkdirSync(dirname(ibFullPath), { recursive: true })
|
|
2878
|
-
writeFileSync(ibFullPath, IDEABOX_TEMPLATE)
|
|
2879
|
-
}
|
|
2880
|
-
|
|
2881
|
-
const parsed = _readIdeabox(ibCwd, ibRelPath)
|
|
2882
|
-
_addIdea(parsed, { title, description, source, tags, cluster })
|
|
2883
|
-
_writeIdeabox(ibCwd, ibRelPath, parsed)
|
|
2884
|
-
const newIdea = parsed.ideas[parsed.ideas.length - 1]
|
|
2885
|
-
console.log(`Added ${newIdea.id}: ${newIdea.title}`)
|
|
2886
|
-
process.exit(0)
|
|
3322
|
+
const slug = args[1]
|
|
3323
|
+
if (!slug || slug.startsWith('--')) {
|
|
3324
|
+
console.error('Usage: compose judgment trace <slug> [--json]')
|
|
3325
|
+
process.exit(1)
|
|
2887
3326
|
}
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
const parsed = _readIdeabox(ibCwd, ibRelPath)
|
|
2895
|
-
if (parsed.ideas.length === 0 && parsed.killed.length === 0) {
|
|
2896
|
-
console.log('No ideas yet.')
|
|
3327
|
+
const { root: jCwd } = resolveCwdWithWorkspace(args)
|
|
3328
|
+
const { getJudgmentTrace } = await import('../lib/judgment-writer.js')
|
|
3329
|
+
try {
|
|
3330
|
+
const t = await getJudgmentTrace(jCwd, slug)
|
|
3331
|
+
if (args.includes('--json')) {
|
|
3332
|
+
console.log(JSON.stringify(t, null, 2))
|
|
2897
3333
|
process.exit(0)
|
|
2898
3334
|
}
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
const
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
for (const idea of group) {
|
|
2917
|
-
const pri = idea.priority !== '—' ? ` [${idea.priority}]` : ''
|
|
2918
|
-
const tags = idea.tags.length ? ` ${idea.tags.join(' ')}` : ''
|
|
2919
|
-
console.log(` ${idea.id}${pri} ${idea.title}${tags}`)
|
|
3335
|
+
console.log(`${t.slug} [${t.status}]`)
|
|
3336
|
+
for (const r of t.revisions) {
|
|
3337
|
+
const marks = [r.retracted ? 'retracted' : null, r.conviction ? `conviction=${r.conviction}` : null]
|
|
3338
|
+
.filter(Boolean).join(' ')
|
|
3339
|
+
const d = Array.isArray(r.delta) && r.delta.length ? ` (${r.delta.join('; ')})` : ''
|
|
3340
|
+
console.log(` r${r.rev} ${r.written_at ?? '—'} ${r.claim_count} claim(s)${marks ? ' ' + marks : ''}${d}`)
|
|
3341
|
+
}
|
|
3342
|
+
if (Array.isArray(t.supersededBy) && t.supersededBy.length) {
|
|
3343
|
+
console.log(` superseded by: ${t.supersededBy.map(s => `${s.slug}#r${s.rev}`).join(', ')}`)
|
|
3344
|
+
}
|
|
3345
|
+
let anc = t.supersedes, depth = 1
|
|
3346
|
+
while (anc) {
|
|
3347
|
+
const pin = anc.pinned ? `#r${anc.rev}` : ''
|
|
3348
|
+
console.log(`${' '.repeat(depth)}↳ supersedes ${anc.slug}${pin} [${anc.status}]`)
|
|
3349
|
+
for (const r of anc.revisions) {
|
|
3350
|
+
const d = Array.isArray(r.delta) && r.delta.length ? ` (${r.delta.join('; ')})` : ''
|
|
3351
|
+
console.log(`${' '.repeat(depth + 1)}r${r.rev} ${r.written_at ?? '—'} conviction=${r.conviction ?? '—'}${d}`)
|
|
2920
3352
|
}
|
|
3353
|
+
anc = anc.supersedes; depth++
|
|
2921
3354
|
}
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
console.log(`\n[KILLED] (${parsed.killed.length})`)
|
|
2925
|
-
for (const idea of parsed.killed) {
|
|
2926
|
-
console.log(` ${idea.id} ${idea.title} — ${idea.killedReason}`)
|
|
2927
|
-
}
|
|
2928
|
-
}
|
|
2929
|
-
process.exit(0)
|
|
2930
|
-
}
|
|
2931
|
-
|
|
2932
|
-
if (ibSubcmd === 'promote') {
|
|
2933
|
-
const ideaId = args[1]
|
|
2934
|
-
if (!ideaId) {
|
|
2935
|
-
console.error('Usage: compose ideabox promote <ID> [<FEATURE-CODE>]')
|
|
2936
|
-
process.exit(1)
|
|
2937
|
-
}
|
|
2938
|
-
const featureCode = args[2] || ''
|
|
2939
|
-
|
|
2940
|
-
if (!existsSync(ibFullPath)) {
|
|
2941
|
-
console.error(`Ideabox not found at ${ibFullPath}`)
|
|
2942
|
-
process.exit(1)
|
|
2943
|
-
}
|
|
2944
|
-
|
|
2945
|
-
const parsed = _readIdeabox(ibCwd, ibRelPath)
|
|
2946
|
-
const idea = parsed.ideas.find(i => i.id.toUpperCase() === ideaId.toUpperCase())
|
|
2947
|
-
if (!idea) {
|
|
2948
|
-
console.error(`Idea not found: ${ideaId}`)
|
|
2949
|
-
process.exit(1)
|
|
2950
|
-
}
|
|
2951
|
-
|
|
2952
|
-
// Generate feature code if not provided
|
|
2953
|
-
let resolvedCode = featureCode
|
|
2954
|
-
if (!resolvedCode) {
|
|
2955
|
-
// Derive a slug from the title
|
|
2956
|
-
const slug = idea.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 20).replace(/-+$/, '')
|
|
2957
|
-
resolvedCode = `IDEA-${idea.num}-${slug}`.toUpperCase()
|
|
2958
|
-
}
|
|
2959
|
-
|
|
2960
|
-
// Create feature folder if missing — respect paths.features (may be
|
|
2961
|
-
// relocated outside ibCwd — COMP-PATHS-EXTERNAL; resolve absolute, no re-root)
|
|
2962
|
-
const featuresBase = resolveFeaturesPathFromConfig(ibCwd, ibConfig)
|
|
2963
|
-
const featuresDir = join(featuresBase, resolvedCode)
|
|
2964
|
-
if (!existsSync(featuresDir)) {
|
|
2965
|
-
// COMP-MCP-VALIDATE-1: route through the validated writer (schema-guarded)
|
|
2966
|
-
// instead of a raw writeFileSync.
|
|
2967
|
-
const { writeFeature } = await import('../lib/feature-json.js')
|
|
2968
|
-
writeFeature(ibCwd, {
|
|
2969
|
-
code: resolvedCode,
|
|
2970
|
-
description: idea.title,
|
|
2971
|
-
status: 'PLANNED',
|
|
2972
|
-
promotedFrom: ideaId,
|
|
2973
|
-
createdAt: new Date().toISOString(),
|
|
2974
|
-
}, featuresBase)
|
|
2975
|
-
console.log(`Created feature folder: ${join(featuresBase, resolvedCode)}/`)
|
|
2976
|
-
}
|
|
2977
|
-
|
|
2978
|
-
_promoteIdea(parsed, ideaId, resolvedCode)
|
|
2979
|
-
_writeIdeabox(ibCwd, ibRelPath, parsed)
|
|
2980
|
-
console.log(`Promoted ${ideaId} → ${resolvedCode}`)
|
|
2981
|
-
process.exit(0)
|
|
2982
|
-
}
|
|
2983
|
-
|
|
2984
|
-
if (ibSubcmd === 'kill') {
|
|
2985
|
-
const ideaId = args[1]
|
|
2986
|
-
const reason = args[2] || ''
|
|
2987
|
-
if (!ideaId) {
|
|
2988
|
-
console.error('Usage: compose ideabox kill <ID> "<reason>"')
|
|
2989
|
-
process.exit(1)
|
|
2990
|
-
}
|
|
2991
|
-
if (!existsSync(ibFullPath)) {
|
|
2992
|
-
console.error(`Ideabox not found at ${ibFullPath}`)
|
|
2993
|
-
process.exit(1)
|
|
2994
|
-
}
|
|
2995
|
-
|
|
2996
|
-
const parsed = _readIdeabox(ibCwd, ibRelPath)
|
|
2997
|
-
_killIdea(parsed, ideaId, reason)
|
|
2998
|
-
_writeIdeabox(ibCwd, ibRelPath, parsed)
|
|
2999
|
-
console.log(`Killed ${ideaId}: ${reason}`)
|
|
3000
|
-
process.exit(0)
|
|
3001
|
-
}
|
|
3002
|
-
|
|
3003
|
-
if (ibSubcmd === 'pri') {
|
|
3004
|
-
const ideaId = args[1]
|
|
3005
|
-
const priority = args[2]
|
|
3006
|
-
if (!ideaId || !priority) {
|
|
3007
|
-
console.error('Usage: compose ideabox pri <ID> <P0|P1|P2>')
|
|
3008
|
-
process.exit(1)
|
|
3009
|
-
}
|
|
3010
|
-
if (!existsSync(ibFullPath)) {
|
|
3011
|
-
console.error(`Ideabox not found at ${ibFullPath}`)
|
|
3012
|
-
process.exit(1)
|
|
3013
|
-
}
|
|
3014
|
-
|
|
3015
|
-
const parsed = _readIdeabox(ibCwd, ibRelPath)
|
|
3016
|
-
_setPriority(parsed, ideaId, priority)
|
|
3017
|
-
_writeIdeabox(ibCwd, ibRelPath, parsed)
|
|
3018
|
-
console.log(`Set ${ideaId} priority → ${priority}`)
|
|
3355
|
+
if (t.cycle) console.error(` WARNING: supersession cycle — ${t.cycle}`)
|
|
3356
|
+
for (const w of t.warnings) console.error(` WARNING: ${w}`)
|
|
3019
3357
|
process.exit(0)
|
|
3358
|
+
} catch (err) {
|
|
3359
|
+
console.error(err?.message || String(err))
|
|
3360
|
+
process.exit(1)
|
|
3020
3361
|
}
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
_writeIdeabox(ibCwd, ibRelPath, parsed)
|
|
3037
|
-
const today = new Date().toISOString().slice(0, 10)
|
|
3038
|
-
console.log(`[${today}] human: ${comment}`)
|
|
3039
|
-
process.exit(0)
|
|
3362
|
+
} else if (cmd === 'ideabox') {
|
|
3363
|
+
// ---------------------------------------------------------------------------
|
|
3364
|
+
// compose ideabox — idea management CLI
|
|
3365
|
+
//
|
|
3366
|
+
// The implementation lives in lib/ideabox-cli.js (COMP-PLAN-IDEA-UNIFY S3b-1).
|
|
3367
|
+
// It was ~300 lines inline here, which made the cutover to the record store
|
|
3368
|
+
// testable only by spawning a subprocess. Behaviour is unchanged; the backing
|
|
3369
|
+
// store is not — every mutation now writes a record and re-renders
|
|
3370
|
+
// docs/product/ideabox.md as a projection of them.
|
|
3371
|
+
// ---------------------------------------------------------------------------
|
|
3372
|
+
const { root: ibCwd } = resolveCwdWithWorkspace(args)
|
|
3373
|
+
let ibConfig = {}
|
|
3374
|
+
const ibCfgPath = join(ibCwd, '.compose', 'compose.json')
|
|
3375
|
+
if (existsSync(ibCfgPath)) {
|
|
3376
|
+
try { ibConfig = JSON.parse(readFileSync(ibCfgPath, 'utf-8')) } catch {}
|
|
3040
3377
|
}
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
console.log('No ideabox found. Run: compose ideabox add "<title>" first.')
|
|
3048
|
-
process.exit(0)
|
|
3049
|
-
}
|
|
3050
|
-
|
|
3051
|
-
const parsed = _readIdeabox(ibCwd, ibRelPath)
|
|
3052
|
-
const untriaged = parsed.ideas.filter(i => i.priority === '—' && i.status === 'NEW')
|
|
3053
|
-
|
|
3054
|
-
if (untriaged.length === 0) {
|
|
3055
|
-
console.log('No untriaged ideas.')
|
|
3056
|
-
process.exit(0)
|
|
3057
|
-
}
|
|
3058
|
-
|
|
3059
|
-
let lens = null
|
|
3060
|
-
if (lensName) {
|
|
3061
|
-
lens = _loadLens(ibCwd, lensName)
|
|
3062
|
-
if (!lens) {
|
|
3063
|
-
console.warn(`Lens not found: docs/product/ideabox-priority-${lensName}.md`)
|
|
3064
|
-
} else {
|
|
3065
|
-
console.log(`Using lens: ${lensName}`)
|
|
3066
|
-
}
|
|
3067
|
-
}
|
|
3068
|
-
|
|
3069
|
-
// Interactive triage using readline
|
|
3070
|
-
const { createInterface } = await import('node:readline')
|
|
3071
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
3072
|
-
const question = (q) => new Promise(resolve => rl.question(q, resolve))
|
|
3073
|
-
|
|
3074
|
-
let changed = false
|
|
3075
|
-
for (const idea of untriaged) {
|
|
3076
|
-
console.log(`\n${idea.id}: ${idea.title}`)
|
|
3077
|
-
if (idea.description) console.log(` ${idea.description.slice(0, 120)}`)
|
|
3078
|
-
if (lens) console.log(` [lens: ${lensName}]`)
|
|
3079
|
-
|
|
3080
|
-
const ans = await question(' Priority [P0/P1/P2/skip]: ')
|
|
3081
|
-
const p = ans.trim().toUpperCase()
|
|
3082
|
-
if (['P0', 'P1', 'P2'].includes(p)) {
|
|
3083
|
-
_setPriority(parsed, idea.id, p)
|
|
3084
|
-
changed = true
|
|
3085
|
-
console.log(` Set ${idea.id} → ${p}`)
|
|
3086
|
-
} else {
|
|
3087
|
-
console.log(' Skipped')
|
|
3088
|
-
}
|
|
3089
|
-
}
|
|
3090
|
-
|
|
3091
|
-
rl.close()
|
|
3092
|
-
|
|
3093
|
-
if (changed) {
|
|
3094
|
-
_writeIdeabox(ibCwd, ibRelPath, parsed)
|
|
3095
|
-
console.log('\nSaved.')
|
|
3096
|
-
}
|
|
3097
|
-
process.exit(0)
|
|
3378
|
+
const { runIdeaboxCommand } = await import('../lib/ideabox-cli.js')
|
|
3379
|
+
try {
|
|
3380
|
+
process.exit(await runIdeaboxCommand(ibCwd, args, { config: ibConfig }))
|
|
3381
|
+
} catch (err) {
|
|
3382
|
+
console.error(err?.message || String(err))
|
|
3383
|
+
process.exit(1)
|
|
3098
3384
|
}
|
|
3099
|
-
|
|
3100
|
-
console.error(`Unknown ideabox subcommand: ${ibSubcmd}`)
|
|
3101
|
-
console.error('Run: compose ideabox --help')
|
|
3102
|
-
process.exit(1)
|
|
3103
|
-
|
|
3104
3385
|
} else if (cmd === 'qa-scope') {
|
|
3105
3386
|
// ---------------------------------------------------------------------------
|
|
3106
3387
|
// compose qa-scope <featureCode>
|
|
@@ -3225,6 +3506,96 @@ if (cmd === 'build') {
|
|
|
3225
3506
|
process.exit(1)
|
|
3226
3507
|
}
|
|
3227
3508
|
|
|
3509
|
+
} else if (cmd === 'lineage') {
|
|
3510
|
+
// ---------------------------------------------------------------------------
|
|
3511
|
+
// compose lineage <stamp|stale|show> --feature <FC> [--changed <file>] [--format text|json]
|
|
3512
|
+
// COMP-PROV-LINEAGE: W3C PROV-O artifact lineage (vocabulary only, no RDF).
|
|
3513
|
+
// stamp — materialise wasGeneratedBy/wasDerivedFrom markers into artifacts
|
|
3514
|
+
// stale — reachability query: given a --changed artifact, list stale descendants
|
|
3515
|
+
// show — print the derivation graph for the feature's canonical artifacts
|
|
3516
|
+
// See docs/features/COMP-PROV-LINEAGE/. Read-only except `stamp`; no server.
|
|
3517
|
+
// ---------------------------------------------------------------------------
|
|
3518
|
+
const lineageSub = args[0]
|
|
3519
|
+
const flagVal = (flag) => {
|
|
3520
|
+
const i = args.indexOf(flag)
|
|
3521
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : null
|
|
3522
|
+
}
|
|
3523
|
+
const feature = flagVal('--feature')
|
|
3524
|
+
const format = flagVal('--format') || 'text'
|
|
3525
|
+
if (!feature || !['stamp', 'stale', 'show'].includes(lineageSub)) {
|
|
3526
|
+
console.error('usage: compose lineage <stamp|stale|show> --feature <FC> [--changed <file>] [--format text|json]')
|
|
3527
|
+
process.exit(1)
|
|
3528
|
+
}
|
|
3529
|
+
const { isFeatureCode } = await import('../lib/feature-code.js')
|
|
3530
|
+
if (!isFeatureCode(feature)) {
|
|
3531
|
+
console.error(`Invalid feature code: ${JSON.stringify(feature)} (must match ${'^[A-Z][A-Z0-9-]*[A-Z0-9]$'})`)
|
|
3532
|
+
process.exit(1)
|
|
3533
|
+
}
|
|
3534
|
+
const { join } = await import('node:path')
|
|
3535
|
+
const { existsSync } = await import('node:fs')
|
|
3536
|
+
const { resolveFeaturesPath } = await import('../lib/project-paths.js')
|
|
3537
|
+
const {
|
|
3538
|
+
stampFeatureLineage, findStaleDescendants, buildDerivationGraph,
|
|
3539
|
+
} = await import('../lib/lineage.js')
|
|
3540
|
+
const featureDir = join(resolveFeaturesPath(process.cwd()), feature)
|
|
3541
|
+
if (!existsSync(featureDir)) {
|
|
3542
|
+
console.error(`No feature folder at ${featureDir}`)
|
|
3543
|
+
process.exit(1)
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
if (lineageSub === 'stamp') {
|
|
3547
|
+
const results = stampFeatureLineage(featureDir)
|
|
3548
|
+
if (format === 'json') {
|
|
3549
|
+
console.log(JSON.stringify(results, null, 2))
|
|
3550
|
+
} else if (results.length === 0) {
|
|
3551
|
+
console.log(`No canonical artifacts found in ${feature}.`)
|
|
3552
|
+
} else {
|
|
3553
|
+
const changed = results.filter(r => r.changed).length
|
|
3554
|
+
console.log(`Stamped lineage on ${feature} — ${changed} of ${results.length} artifact(s) updated:\n`)
|
|
3555
|
+
for (const r of results) {
|
|
3556
|
+
const from = r.wasDerivedFrom.length ? r.wasDerivedFrom.join(', ') : '(origin)'
|
|
3557
|
+
console.log(` ${r.changed ? '✓' : '·'} ${r.file} wasGeneratedBy=${r.wasGeneratedBy ?? '?'} wasDerivedFrom=${from}`)
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
} else if (lineageSub === 'stale') {
|
|
3561
|
+
const changedFile = flagVal('--changed')
|
|
3562
|
+
if (!changedFile) {
|
|
3563
|
+
console.error('compose lineage stale: --changed <file> is required')
|
|
3564
|
+
process.exit(1)
|
|
3565
|
+
}
|
|
3566
|
+
// Must be a bare filename inside the feature folder — no path traversal.
|
|
3567
|
+
if (changedFile.includes('/') || changedFile.includes('\\') || changedFile.includes('..')) {
|
|
3568
|
+
console.error(`compose lineage stale: --changed must be a bare filename, got ${JSON.stringify(changedFile)}`)
|
|
3569
|
+
process.exit(1)
|
|
3570
|
+
}
|
|
3571
|
+
const results = findStaleDescendants(featureDir, changedFile)
|
|
3572
|
+
if (format === 'json') {
|
|
3573
|
+
console.log(JSON.stringify(results, null, 2))
|
|
3574
|
+
} else {
|
|
3575
|
+
const stale = results.filter(r => r.stale)
|
|
3576
|
+
if (results.length === 0) {
|
|
3577
|
+
console.log(`${changedFile} has no downstream artifacts in ${feature}.`)
|
|
3578
|
+
} else if (stale.length === 0) {
|
|
3579
|
+
console.log(`No stale descendants of ${changedFile} — all ${results.length} downstream artifact(s) are newer.`)
|
|
3580
|
+
} else {
|
|
3581
|
+
console.log(`${stale.length} artifact(s) downstream of ${changedFile} are STALE (older than the change):\n`)
|
|
3582
|
+
for (const r of stale) console.log(` ⚠ ${r.file}`)
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
} else if (lineageSub === 'show') {
|
|
3586
|
+
const graph = buildDerivationGraph(featureDir)
|
|
3587
|
+
if (format === 'json') {
|
|
3588
|
+
console.log(JSON.stringify(Object.fromEntries(graph), null, 2))
|
|
3589
|
+
} else if (graph.size === 0) {
|
|
3590
|
+
console.log(`No canonical artifacts found in ${feature}.`)
|
|
3591
|
+
} else {
|
|
3592
|
+
console.log(`Derivation graph for ${feature}:\n`)
|
|
3593
|
+
for (const [file, parents] of graph) {
|
|
3594
|
+
console.log(parents.length ? ` ${file} ⟵ ${parents.join(', ')}` : ` ${file} (origin)`)
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3228
3599
|
} else if (cmd === 'gates' || cmd === 'gate') {
|
|
3229
3600
|
// ---------------------------------------------------------------------------
|
|
3230
3601
|
// compose gate list [--item <id>] [--status pending|all|resolved] [--format text|json]
|
|
@@ -3822,6 +4193,42 @@ if (cmd === 'build') {
|
|
|
3822
4193
|
console.error(' compose smartmemory sync [--dry-run] [--feature <CODE>]')
|
|
3823
4194
|
process.exit(1)
|
|
3824
4195
|
|
|
4196
|
+
} else if (cmd === 'metrics') {
|
|
4197
|
+
// ---------------------------------------------------------------------------
|
|
4198
|
+
// compose metrics [--since <duration|ISO>] [--feature <code>] [--json]
|
|
4199
|
+
// ---------------------------------------------------------------------------
|
|
4200
|
+
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
4201
|
+
let since = null
|
|
4202
|
+
let feature = null
|
|
4203
|
+
let json = false
|
|
4204
|
+
|
|
4205
|
+
for (let index = 0; index < args.length; index++) {
|
|
4206
|
+
const arg = args[index]
|
|
4207
|
+
if (arg === '--json') {
|
|
4208
|
+
json = true
|
|
4209
|
+
continue
|
|
4210
|
+
}
|
|
4211
|
+
if (arg === '--since' || arg === '--feature') {
|
|
4212
|
+
const value = args[index + 1]
|
|
4213
|
+
if (!value || value.startsWith('--')) {
|
|
4214
|
+
console.error(`compose metrics: ${arg} requires a value`)
|
|
4215
|
+
process.exit(1)
|
|
4216
|
+
}
|
|
4217
|
+
if (arg === '--since') since = value
|
|
4218
|
+
else feature = value
|
|
4219
|
+
index++
|
|
4220
|
+
continue
|
|
4221
|
+
}
|
|
4222
|
+
console.error(`compose metrics: unknown flag ${arg}`)
|
|
4223
|
+
process.exit(1)
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
const { collectDispatchMetrics, renderDispatchMetrics } = await import('../lib/dispatch-metrics.js')
|
|
4227
|
+
const report = collectDispatchMetrics(cwd, { since, feature })
|
|
4228
|
+
if (json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
|
|
4229
|
+
else process.stdout.write(renderDispatchMetrics(report))
|
|
4230
|
+
process.exit(0)
|
|
4231
|
+
|
|
3825
4232
|
} else {
|
|
3826
4233
|
console.error(`Unknown command: ${cmd}`)
|
|
3827
4234
|
process.exit(1)
|