@smartmemory/compose 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/compose/SKILL.md +12 -3
- package/.compose-deps.json +51 -25
- package/README.md +79 -7
- package/bin/compose.js +495 -360
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/{_baseUniq-Bo837sRJ.js → _baseUniq-ClWoCPFl.js} +1 -1
- package/dist/assets/{arc-BafGpyqE.js → arc-DY26UIVo.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BOBfUsqL.js → architectureDiagram-Q4EWVU46-6Ggq4DqJ.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-Dwodev1a.js → blockDiagram-DXYQGD6D-CH3Ked0l.js} +1 -1
- package/dist/assets/{browser-1ntj1-x_.js → browser-BWkrenen.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-CU_bhYag.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-p8WsDwnO.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-B8h7-eR0.js → chunk-4TB4RGXK-JytR14a9.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DxeEr98s.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-BYt8F151.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DGSOVeie.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-B-QdgYR2.js → chunk-OYMX7WX6-BySQzVxc.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Du5UAZLs.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-C8JbNBSk.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-S5V4N54A-O1ESaqge.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-CPTmFPHw.js → dagre-KV5264BT-Cp3F5KTn.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-B3PNrWs5.js → diagram-5BDNPKRD-DiR6_2q_.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Cscfr6vc.js → diagram-G4DWMVQ6-w0i-p5HX.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-CSfqZ-TM.js → diagram-MMDJMWI5-tIHhwUv3.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Cg4aYS7W.js → diagram-TYMM5635-BAeY3B19.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-_ZqwG5pl.js → erDiagram-SMLLAGMA-Ckx_Knko.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-C83boxFT.js → flowDiagram-DWJPFMVM-DeoNka6J.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-CWnIjuEi.js → ganttDiagram-T4ZO3ILL-BmGnFbEg.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-DrMdxZfH.js → gitGraphDiagram-UUTBAWPF-Dk48IHsx.js} +1 -1
- package/dist/assets/{graph-RE4I7Ty7.js → graph-BNzKGvoy.js} +1 -1
- package/dist/assets/{graph-Bi99_6Yf.js → graph-CI_1htl0.js} +1 -1
- package/dist/assets/{index-Rm2RE-c0.js → index-BEfrNBp8.js} +3 -3
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/{infoDiagram-42DDH7IO-BLmP4Epr.js → infoDiagram-42DDH7IO-BRf827i0.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-yuWWshKN.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-BOfhaJov.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-Bbolde15.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +1 -1
- package/dist/assets/{layout-BSf33zm8.js → layout-BI8cXFPI.js} +1 -1
- package/dist/assets/{linear-AvSTWMqx.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/{min-QBM8H4xN.js → min-vPHfnXcC.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BuvgtqIc.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +1 -1
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/{pieDiagram-DEJITSTG-DIzF16vh.js → pieDiagram-DEJITSTG-Cno-gETh.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-D-mbUIjS.js → quadrantDiagram-34T5L4WZ-BUQM1Hfm.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-CEs4kCLd.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-DFsnCr9n.js → sankeyDiagram-XADWPNL6-Crynd3_b.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-BEJYdTjQ.js → sequenceDiagram-FGHM5R23-D9fZdCM8.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-BBXs57uY.js → stateDiagram-FHFEXIEX-CW9qVec8.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-BGvLoVAY.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-9LaBTMe0.js → vennDiagram-DHZGUBPP-BfytJcWk.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-P4MEqMTP.js → wardley-RL74JXVD-DLj-IjyB.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-o-tmxnlC.js → wardleyDiagram-NUSXRM2D-Ds0Ue68c.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-Dpn7V6qk.js → xychartDiagram-5P7HB3ND-vjWDXFL6.js} +1 -1
- 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 +9 -0
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +874 -170
- package/lib/canon-guard.js +28 -6
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +104 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +26 -13
- 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/deps.js +164 -7
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +3 -3
- 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 +57 -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 -23
- 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/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +9 -1
- package/lib/judgment/store/records.js +1 -1
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +5 -1
- package/lib/judgment-writer.js +56 -2
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +52 -1
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- 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 +396 -199
- 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 +271 -36
- 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 +109 -23
- package/server/compose-mcp.js +88 -882
- 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 +34 -2
- 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-PkZzHeMj.js +0 -894
- package/dist/assets/channel-qVK_qn4E.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +0 -1
- package/dist/assets/clone-Pu3RyLUh.js +0 -1
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/mobile-BnXEOE3U.js +0 -17
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +0 -1
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
package/bin/compose.js
CHANGED
|
@@ -90,8 +90,13 @@ function resolveCwdWithWorkspace(args) {
|
|
|
90
90
|
// --team flag (COMP-TEAMS)
|
|
91
91
|
// ---------------------------------------------------------------------------
|
|
92
92
|
import { parseTeamFlag } from '../lib/team-flag.js';
|
|
93
|
-
import { loadDeps, checkExternalSkills, printDepReport, buildDepReport, checkExternalBinaries, printBinaryReport, buildBinaryReport } from '../lib/deps.js';
|
|
94
|
-
import {
|
|
93
|
+
import { loadDeps, checkExternalSkills, printDepReport, buildDepReport, checkExternalBinaries, printBinaryReport, buildBinaryReport, installMissingPlugins, printInstallReport } from '../lib/deps.js';
|
|
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,40 +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(' metrics [--since <duration|ISO>] [--feature <code>] [--json] Report dispatch, settlement, and triage metrics')
|
|
141
|
-
console.log(' qa-scope Show affected routes from a feature\'s changed files')
|
|
142
|
-
console.log(' context decisions Show the build decision log (--feature <FC>, --format text|json)')
|
|
143
|
-
console.log(' gate list List pending gates (--item <id>, --status pending|all|resolved)')
|
|
144
|
-
console.log(' gate resolve <id> Resolve a gate (--approve|--revise|--kill, --comment <text>)')
|
|
145
|
-
console.log(' init Initialize Compose in the current project')
|
|
146
|
-
console.log(' setup Install/sync global Compose skills (alias: sync)')
|
|
147
|
-
console.log(' sync Re-sync global skills from this install (alias of setup)')
|
|
148
|
-
console.log(' update Pull latest compose, reinstall deps, refresh global skill')
|
|
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])')
|
|
151
|
-
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())
|
|
152
127
|
process.exit(0)
|
|
153
128
|
}
|
|
154
129
|
|
|
@@ -197,7 +172,7 @@ function detectAgents() {
|
|
|
197
172
|
* - PACKAGE_ROOT/.claude/skills/* (compose skill)
|
|
198
173
|
* - PACKAGE_ROOT/skills/* (stratum base skill)
|
|
199
174
|
*/
|
|
200
|
-
function syncSkills(agents) {
|
|
175
|
+
function syncSkills(agents, setupFlags = []) {
|
|
201
176
|
// Collect source skills: { name -> sourcePath }
|
|
202
177
|
const sourceSkills = new Map()
|
|
203
178
|
const skillSourceDirs = [
|
|
@@ -296,12 +271,28 @@ function syncSkills(agents) {
|
|
|
296
271
|
// actionable install hints. Soft check: warnings only, exit code unaffected.
|
|
297
272
|
const deps = loadDeps(PACKAGE_ROOT)
|
|
298
273
|
if (deps) {
|
|
299
|
-
|
|
274
|
+
let result = checkExternalSkills(deps)
|
|
275
|
+
// COMP-DEPS-AUTOINSTALL: close the loop rather than printing a hint and
|
|
276
|
+
// running degraded. Only required deps with a `plugin` spec are installed.
|
|
277
|
+
// The report printed below is the POST-install scan, so what the user reads
|
|
278
|
+
// is the state they actually end up in, never the state we started from.
|
|
279
|
+
const installReport = installMissingPlugins(result, { enabled: pluginInstallEnabled(setupFlags) })
|
|
280
|
+
printInstallReport(installReport)
|
|
281
|
+
if (installReport.installed.length > 0) result = checkExternalSkills(deps)
|
|
300
282
|
printDepReport(result)
|
|
301
283
|
printBinaryReport(checkExternalBinaries(deps))
|
|
302
284
|
}
|
|
303
285
|
}
|
|
304
286
|
|
|
287
|
+
/**
|
|
288
|
+
* COMP-DEPS-AUTOINSTALL opt-out. Off for `--no-install-deps` and for
|
|
289
|
+
* COMPOSE_NO_PLUGIN_INSTALL=1 (CI, sandboxes, anyone who manages plugins by hand).
|
|
290
|
+
*/
|
|
291
|
+
function pluginInstallEnabled(flags = []) {
|
|
292
|
+
if (process.env.COMPOSE_NO_PLUGIN_INSTALL === '1') return false
|
|
293
|
+
return !flags.includes('--no-install-deps')
|
|
294
|
+
}
|
|
295
|
+
|
|
305
296
|
// ---------------------------------------------------------------------------
|
|
306
297
|
// compose doctor — re-run the external dep check
|
|
307
298
|
// ---------------------------------------------------------------------------
|
|
@@ -568,10 +559,24 @@ async function runInit(flags, cwdOverride) {
|
|
|
568
559
|
}
|
|
569
560
|
}
|
|
570
561
|
|
|
571
|
-
// 8. Copy default pipeline specs if absent
|
|
562
|
+
// 8. Copy default pipeline specs if absent.
|
|
563
|
+
// COMP-PIPELINE-QUARANTINE follow-up: `bug-fix` belongs in this set. `compose
|
|
564
|
+
// fix` checks for pipelines/bug-fix.stratum.yaml, runs init when it is absent,
|
|
565
|
+
// and then fails with "Lifecycle spec not found" because init never copied it —
|
|
566
|
+
// so the command was unavailable in every fresh workspace independently of the
|
|
567
|
+
// spec's dialect. Each spec's `<name>.profiles.json` sidecar travels WITH it:
|
|
568
|
+
// loadPipelineProfiles fails open, so a spec copied without its sidecar runs on
|
|
569
|
+
// bare defaults and silently drops the tool restrictions it declares.
|
|
572
570
|
const pipelinesDir = join(cwd, 'pipelines')
|
|
573
571
|
mkdirSync(pipelinesDir, { recursive: true })
|
|
574
|
-
|
|
572
|
+
const DEFAULT_PIPELINES = [
|
|
573
|
+
'build.stratum.yaml',
|
|
574
|
+
'build-quick.stratum.yaml',
|
|
575
|
+
'bug-fix.stratum.yaml',
|
|
576
|
+
'new.stratum.yaml',
|
|
577
|
+
'plan.stratum.yaml',
|
|
578
|
+
]
|
|
579
|
+
for (const specName of DEFAULT_PIPELINES) {
|
|
575
580
|
const dest = join(pipelinesDir, specName)
|
|
576
581
|
if (!existsSync(dest)) {
|
|
577
582
|
const src = join(PACKAGE_ROOT, 'pipelines', specName)
|
|
@@ -580,10 +585,16 @@ async function runInit(flags, cwdOverride) {
|
|
|
580
585
|
console.log(`Copied default pipeline to ${dest}`)
|
|
581
586
|
}
|
|
582
587
|
}
|
|
588
|
+
const sidecarName = specName.replace(/\.stratum\.ya?ml$/, '.profiles.json')
|
|
589
|
+
const sidecarDest = join(pipelinesDir, sidecarName)
|
|
590
|
+
if (!existsSync(sidecarDest)) {
|
|
591
|
+
const sidecarSrc = join(PACKAGE_ROOT, 'pipelines', sidecarName)
|
|
592
|
+
if (existsSync(sidecarSrc)) copyFileSync(sidecarSrc, sidecarDest)
|
|
593
|
+
}
|
|
583
594
|
}
|
|
584
595
|
|
|
585
596
|
// 9. Sync all compose-owned skills to detected agents
|
|
586
|
-
syncSkills(agents)
|
|
597
|
+
syncSkills(agents, flags)
|
|
587
598
|
|
|
588
599
|
// 10. Summary
|
|
589
600
|
console.log('')
|
|
@@ -597,7 +608,7 @@ async function runInit(flags, cwdOverride) {
|
|
|
597
608
|
// compose setup — user-global skill setup
|
|
598
609
|
// ---------------------------------------------------------------------------
|
|
599
610
|
|
|
600
|
-
function runSetup() {
|
|
611
|
+
function runSetup(setupFlags = []) {
|
|
601
612
|
// 1. Sync all compose-owned skills to detected agents.
|
|
602
613
|
// If none detected, fall back to Claude — setup is unconditional
|
|
603
614
|
// "install global skill" per its help text.
|
|
@@ -605,7 +616,7 @@ function runSetup() {
|
|
|
605
616
|
if (agents.length === 0) {
|
|
606
617
|
agents = [{ name: 'claude', skillDir: join(homedir(), '.claude', 'skills', 'stratum') }]
|
|
607
618
|
}
|
|
608
|
-
syncSkills(agents)
|
|
619
|
+
syncSkills(agents, setupFlags)
|
|
609
620
|
|
|
610
621
|
}
|
|
611
622
|
|
|
@@ -718,7 +729,7 @@ async function runUpdate(flags) {
|
|
|
718
729
|
// Refresh global skills.
|
|
719
730
|
console.log('')
|
|
720
731
|
console.log('Refreshing global skill installation...')
|
|
721
|
-
runSetup()
|
|
732
|
+
runSetup(args)
|
|
722
733
|
|
|
723
734
|
// If invoked from inside a Compose project, refresh project artifacts too
|
|
724
735
|
if (existsSync(join(cwd, '.compose', 'compose.json'))) {
|
|
@@ -757,11 +768,73 @@ async function runUpdate(flags) {
|
|
|
757
768
|
console.log(`compose updated to v${getPkgVersion()}${style === 'git' ? ` @ ${getGitSha(root) || '?'}` : ''}`)
|
|
758
769
|
}
|
|
759
770
|
|
|
771
|
+
/** Hard ceiling on how long the update nudge may delay a command. */
|
|
772
|
+
const NUDGE_BUDGET_MS = 4000
|
|
773
|
+
|
|
774
|
+
/** Stratum is enabled unless this cwd's workspace manifest explicitly opts out.
|
|
775
|
+
* This deliberately does not use resolveWorkspace(): the nudge runs before
|
|
776
|
+
* argument parsing, and workspace resolution may print errors and exit. */
|
|
777
|
+
function isStratumCapabilityEnabled() {
|
|
778
|
+
try {
|
|
779
|
+
const manifest = join(process.cwd(), '.compose', 'compose.json')
|
|
780
|
+
const config = JSON.parse(readFileSync(manifest, 'utf-8'))
|
|
781
|
+
return config?.capabilities?.stratum !== false
|
|
782
|
+
} catch {
|
|
783
|
+
// Absent, unreadable, or malformed means "enabled" for compatibility.
|
|
784
|
+
return true
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** Print a one-line update nudge at session entry points. NOTIFY ONLY —
|
|
789
|
+
* this must never mutate an installation. Compose has ~109 lazy `await import()`
|
|
790
|
+
* sites, so replacing files under a running process would have it load new
|
|
791
|
+
* modules into an old module graph; safe self-update needs a versioned install
|
|
792
|
+
* (COMP-UPDATE-VERSIONED-INSTALL), not a check here.
|
|
793
|
+
*
|
|
794
|
+
* Swallows everything: a courtesy line may never fail the command it precedes. */
|
|
795
|
+
async function emitDriftNudge() {
|
|
796
|
+
try {
|
|
797
|
+
// Only an interactive terminal pays for a registry lookup. Everything else
|
|
798
|
+
// — a spawned CLI, CI, a script — reads cache and stays silent on a miss.
|
|
799
|
+
//
|
|
800
|
+
// This is not an optimization, it is a correctness fix. The nudge sits on
|
|
801
|
+
// the startup path of init/build/plan, and the test suite alone spawns
|
|
802
|
+
// `compose init` dozens of times in parallel with a cold HOME. Every one of
|
|
803
|
+
// those fired a live fetch, and under that concurrency one failed to settle:
|
|
804
|
+
// node reported "Detected unsettled top-level await" at this call and exited
|
|
805
|
+
// non-zero, breaking `compose init` itself. A courtesy line must never be
|
|
806
|
+
// able to do that, and non-interactive callers were never its audience.
|
|
807
|
+
const cacheOnly = !process.stdout.isTTY
|
|
808
|
+
const stratumCurrent = isStratumCapabilityEnabled()
|
|
809
|
+
? resolveStratumVersion(PACKAGE_ROOT)
|
|
810
|
+
: null
|
|
811
|
+
const work = Promise.all([
|
|
812
|
+
checkPackageVersion('@smartmemory/compose', getPkgVersion(), { cacheOnly }),
|
|
813
|
+
stratumCurrent
|
|
814
|
+
? checkPackageVersion('@smartmemory/stratum', stratumCurrent, { cacheOnly })
|
|
815
|
+
: Promise.resolve(null),
|
|
816
|
+
])
|
|
817
|
+
// Belt and braces: whatever happens below, this await settles. The timer is
|
|
818
|
+
// ref'd deliberately — an unref'd one would let the loop drain and reproduce
|
|
819
|
+
// the very unsettled-await exit this guards against.
|
|
820
|
+
const [composeInfo, stratumInfo] = await Promise.race([
|
|
821
|
+
work,
|
|
822
|
+
new Promise((resolve) => setTimeout(() => resolve([null, null]), NUDGE_BUDGET_MS)),
|
|
823
|
+
])
|
|
824
|
+
for (const line of formatDriftNudge({ compose: composeInfo, stratum: stratumInfo })) {
|
|
825
|
+
console.log(line)
|
|
826
|
+
}
|
|
827
|
+
} catch {
|
|
828
|
+
// never break the command this is attached to
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
760
832
|
// ---------------------------------------------------------------------------
|
|
761
833
|
// Command dispatch
|
|
762
834
|
// ---------------------------------------------------------------------------
|
|
763
835
|
|
|
764
836
|
if (cmd === 'init') {
|
|
837
|
+
await emitDriftNudge()
|
|
765
838
|
await runInit(args)
|
|
766
839
|
process.exit(0)
|
|
767
840
|
}
|
|
@@ -771,7 +844,7 @@ if (cmd === 'setup' || cmd === 'sync') {
|
|
|
771
844
|
// agent skill dirs. The name `sync` better signals
|
|
772
845
|
// the idempotent "reconcile local skills with this install" job (run it after
|
|
773
846
|
// editing skills locally, when there's no new version to `update` to).
|
|
774
|
-
runSetup()
|
|
847
|
+
runSetup(args)
|
|
775
848
|
process.exit(0)
|
|
776
849
|
}
|
|
777
850
|
|
|
@@ -825,7 +898,7 @@ if (cmd === 'migrate-anon') {
|
|
|
825
898
|
if (cmd === 'install') {
|
|
826
899
|
// Backwards-compat: run both init + setup
|
|
827
900
|
await runInit(args)
|
|
828
|
-
runSetup()
|
|
901
|
+
runSetup(args)
|
|
829
902
|
process.exit(0)
|
|
830
903
|
}
|
|
831
904
|
|
|
@@ -1161,38 +1234,122 @@ _This is a seed design doc created by \`compose feature\`. The \`compose build\`
|
|
|
1161
1234
|
process.exit(0)
|
|
1162
1235
|
}
|
|
1163
1236
|
|
|
1237
|
+
if (cmd === 'guard' && ['descriptors', 'sign', 'enrol'].includes(args[0])) {
|
|
1238
|
+
// New signing verbs stay above the canon-guard dispatcher below. They resolve
|
|
1239
|
+
// the target workspace; canon-guard intentionally continues to dogfood this
|
|
1240
|
+
// package root for its own hook state.
|
|
1241
|
+
const sub = args[0]
|
|
1242
|
+
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
1243
|
+
const { runGuardDescriptors, runGuardSign, runGuardEnrol } = await import('../lib/guard-cli.js')
|
|
1244
|
+
const principalAt = args.indexOf('--principal')
|
|
1245
|
+
const principal = principalAt === -1 ? undefined : (args[principalAt + 1] ?? '')
|
|
1246
|
+
const result = sub === 'descriptors' ? await runGuardDescriptors(cwd)
|
|
1247
|
+
: sub === 'sign' ? await runGuardSign(cwd)
|
|
1248
|
+
: await runGuardEnrol(principal === undefined ? {} : { principal }, { emit: (line) => console.log(line) })
|
|
1249
|
+
for (const line of result.lines) console.log(line)
|
|
1250
|
+
process.exit(result.status === 'refused' ? 1 : 0)
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1164
1253
|
if (cmd === 'roadmap') {
|
|
1165
1254
|
const subcmd = args[0]
|
|
1166
1255
|
|
|
1167
1256
|
// compose roadmap generate — regenerate ROADMAP.md from feature.json files,
|
|
1168
1257
|
// converging to a fixed point before finishing.
|
|
1169
1258
|
if (subcmd === 'generate' || subcmd === 'gen') {
|
|
1170
|
-
const { writeRoadmap } = await import('../lib/roadmap-gen.js')
|
|
1171
1259
|
const { checkRoundtrip } = await import('../lib/roadmap-roundtrip.js')
|
|
1260
|
+
const { generateRoadmapFromBase } = await import('../lib/roadmap-gen.js')
|
|
1172
1261
|
const { listFeatures } = await import('../lib/feature-json.js')
|
|
1173
|
-
const { loadExternalPrefixes } = await import('../lib/project-paths.js')
|
|
1262
|
+
const { loadExternalPrefixes, resolveRoadmapPath, loadFeaturesDir } = await import('../lib/project-paths.js')
|
|
1174
1263
|
const { isNarrativeOwned } = await import('../lib/roadmap-config.js')
|
|
1264
|
+
const { readPreservedSections } = await import('../lib/roadmap-preservers.js')
|
|
1265
|
+
const { computeResidue, protectResidue, RoadmapProseLossError } = await import('../lib/roadmap-residue.js')
|
|
1175
1266
|
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
1176
1267
|
// Narrative-owned workspaces (#39): ROADMAP.md is hand-authored, not a render
|
|
1177
|
-
// of feature.json
|
|
1178
|
-
// below would still overwrite the file (or crash if it's absent) — skip the
|
|
1179
|
-
// whole generate path here so the hand-authored file is never touched.
|
|
1268
|
+
// of feature.json — never regenerate or overwrite it.
|
|
1180
1269
|
if (isNarrativeOwned(cwd)) {
|
|
1181
1270
|
console.log('narrative-owned workspace (roadmap.narrative=true) — ROADMAP.md is hand-authored; generate skipped.')
|
|
1182
1271
|
process.exit(0)
|
|
1183
1272
|
}
|
|
1184
|
-
const path =
|
|
1273
|
+
const path = resolveRoadmapPath(cwd)
|
|
1274
|
+
const base = existsSync(path) ? readFileSync(path, 'utf-8') : ''
|
|
1275
|
+
const acceptLoss = args.includes('--accept-loss')
|
|
1276
|
+
const protect = args.includes('--protect')
|
|
1185
1277
|
const externalPrefixes = loadExternalPrefixes(cwd)
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
//
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1278
|
+
const now = new Date().toISOString().slice(0, 10)
|
|
1279
|
+
|
|
1280
|
+
// Guard 1 (COMP-CONFLICT-MERGE): structural preserved-section marker defects
|
|
1281
|
+
// silently drop the content they were meant to protect — an unbalanced open
|
|
1282
|
+
// (typo'd close) or a duplicate id (Map collision). Fail loud, write nothing.
|
|
1283
|
+
try {
|
|
1284
|
+
readPreservedSections(base, { strict: true })
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
if (err?.code === 'ROADMAP_UNBALANCED_MARKER') {
|
|
1287
|
+
console.error(`Refusing to write ${path}: unbalanced preserved-section marker(s):`)
|
|
1288
|
+
for (const m of err.markers) console.error(` line ${m.lineNo}: open "${m.id}" has no matching close`)
|
|
1289
|
+
console.error(err.remediation)
|
|
1290
|
+
process.exit(1)
|
|
1291
|
+
}
|
|
1292
|
+
if (err?.code === 'ROADMAP_DUPLICATE_MARKER') {
|
|
1293
|
+
console.error(`Refusing to write ${path}: duplicate preserved-section id(s):`)
|
|
1294
|
+
for (const m of err.markers) console.error(` line ${m.lineNo}: id "${m.id}" is used more than once`)
|
|
1295
|
+
console.error(err.remediation)
|
|
1296
|
+
process.exit(1)
|
|
1297
|
+
}
|
|
1298
|
+
throw err
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Honor a configured non-default features directory (paths.features), else a
|
|
1302
|
+
// project using one regenerates from the wrong/empty feature set.
|
|
1303
|
+
const featuresDir = loadFeaturesDir(cwd)
|
|
1304
|
+
const features = listFeatures(cwd, featuresDir)
|
|
1305
|
+
|
|
1306
|
+
// Emit the phase-override drift warning + roadmap_drift audit event, which the
|
|
1307
|
+
// old writeRoadmap-first path produced and the pure roundtrip path suppresses.
|
|
1308
|
+
generateRoadmapFromBase(base, features, { cwd, featuresDir, now })
|
|
1309
|
+
|
|
1310
|
+
// Guard 2: compute the FINAL canonical bytes (fixed point over all passes)
|
|
1311
|
+
// BEFORE writing, then diff the base against exactly what will be written.
|
|
1312
|
+
// The duplicate-heading loss only surfaces on a later pass, so checking the
|
|
1313
|
+
// first candidate would pass cleanly and then write the lossy result.
|
|
1314
|
+
const featureCodes = new Set(features.map(f => f.code))
|
|
1315
|
+
const rt = checkRoundtrip(base, features, { now, externalPrefixes, featuresDir })
|
|
1316
|
+
let finalText = rt.canonical
|
|
1317
|
+
let residue = computeResidue(base, finalText, { featureCodes })
|
|
1318
|
+
|
|
1319
|
+
if (residue.length > 0 && protect) {
|
|
1320
|
+
const protectedBase = protectResidue(base, residue)
|
|
1321
|
+
const rt2 = checkRoundtrip(protectedBase, features, { now, externalPrefixes, featuresDir })
|
|
1322
|
+
const stillLost = computeResidue(protectedBase, rt2.canonical, { featureCodes })
|
|
1323
|
+
if (stillLost.length === 0) {
|
|
1324
|
+
writeFileSync(path, rt2.canonical)
|
|
1325
|
+
console.log(`Generated ${path} — wrapped ${residue.length} line(s) in preserved-section markers`)
|
|
1326
|
+
process.exit(0)
|
|
1327
|
+
}
|
|
1328
|
+
// --protect could not fully account for the loss; fall through to the error
|
|
1329
|
+
// with the residual set so the operator sees what remains.
|
|
1330
|
+
finalText = rt2.canonical
|
|
1331
|
+
residue = stillLost
|
|
1195
1332
|
}
|
|
1333
|
+
|
|
1334
|
+
if (residue.length > 0 && !acceptLoss) {
|
|
1335
|
+
const err = new RoadmapProseLossError(residue)
|
|
1336
|
+
console.error(`Refusing to write ${path}: ${err.message}:`)
|
|
1337
|
+
for (const r of err.lines) {
|
|
1338
|
+
console.error(` line ${r.lineNo} (under "${r.nearestHeading ?? '(preamble)'}"): ${r.text}`)
|
|
1339
|
+
}
|
|
1340
|
+
console.error('')
|
|
1341
|
+
console.error(err.remediation)
|
|
1342
|
+
process.exit(1)
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (residue.length > 0 && acceptLoss) {
|
|
1346
|
+
console.log(`--accept-loss: dropping ${residue.length} hand-authored line(s):`)
|
|
1347
|
+
for (const r of residue) console.log(` line ${r.lineNo}: ${r.text}`)
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
writeFileSync(path, finalText)
|
|
1351
|
+
if (!rt.fixedPoint) console.log(`Generated ${path} (canonicalized over ${rt.passes} passes)`)
|
|
1352
|
+
else console.log(`Generated ${path} from feature.json files`)
|
|
1196
1353
|
process.exit(0)
|
|
1197
1354
|
}
|
|
1198
1355
|
|
|
@@ -1592,7 +1749,7 @@ if (cmd === 'record-completion') {
|
|
|
1592
1749
|
//
|
|
1593
1750
|
// Flags:
|
|
1594
1751
|
// --commit-sha=<sha> required; full 40-char hex SHA (Decision 9)
|
|
1595
|
-
// --tests-pass=<bool> default
|
|
1752
|
+
// --tests-pass=<bool> REQUIRED (true|false) — no default (COMP-COMPLETION-GATE)
|
|
1596
1753
|
// --notes=<string> optional
|
|
1597
1754
|
// --files-changed-from-stdin read newline-separated paths from stdin
|
|
1598
1755
|
// --no-status set_status: false (don't flip status to COMPLETE)
|
|
@@ -1637,7 +1794,8 @@ if (cmd === 'record-completion') {
|
|
|
1637
1794
|
console.log('')
|
|
1638
1795
|
console.log('Options:')
|
|
1639
1796
|
console.log(' --commit-sha=<sha> Full 40-char hex SHA (required)')
|
|
1640
|
-
console.log(' --tests-pass=<bool> Whether tests passed
|
|
1797
|
+
console.log(' --tests-pass=<bool> Whether tests passed — REQUIRED, no default')
|
|
1798
|
+
console.log(' (configure guard.testCommand to attest by running instead)')
|
|
1641
1799
|
console.log(' --notes=<string> Optional provenance notes')
|
|
1642
1800
|
console.log(' --files-changed-from-stdin Read newline-separated repo-relative paths from stdin')
|
|
1643
1801
|
console.log(' --no-status Do not flip feature status to COMPLETE')
|
|
@@ -1660,8 +1818,13 @@ if (cmd === 'record-completion') {
|
|
|
1660
1818
|
process.exit(1)
|
|
1661
1819
|
}
|
|
1662
1820
|
|
|
1663
|
-
// Parse tests-pass: default
|
|
1664
|
-
|
|
1821
|
+
// Parse tests-pass. COMP-COMPLETION-GATE (BREAKING): there is NO default.
|
|
1822
|
+
// This used to default to `true`, which meant every completion recorded from
|
|
1823
|
+
// the CLI attested a test run the operator may never have made — the record
|
|
1824
|
+
// said "tests passed" on the strength of nothing. An attestation nobody made
|
|
1825
|
+
// is worse than an absent one, so the flag is now required unless a
|
|
1826
|
+
// `guard.testCommand` is configured to attest by running.
|
|
1827
|
+
let testsPass
|
|
1665
1828
|
if (flags['tests-pass'] !== undefined) {
|
|
1666
1829
|
const tp = flags['tests-pass']
|
|
1667
1830
|
if (tp === 'false' || tp === false) testsPass = false
|
|
@@ -1698,7 +1861,48 @@ if (cmd === 'record-completion') {
|
|
|
1698
1861
|
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
1699
1862
|
const { recordCompletion } = await import('../lib/completion-writer.js')
|
|
1700
1863
|
try {
|
|
1701
|
-
|
|
1864
|
+
let result
|
|
1865
|
+
if (completionArgs.set_status === false) {
|
|
1866
|
+
// --no-status records evidence without completing, so it does not drive
|
|
1867
|
+
// the guard. tests_pass still has no default; the writer requires a boolean.
|
|
1868
|
+
if (testsPass === undefined) {
|
|
1869
|
+
console.error(
|
|
1870
|
+
'Error: --tests-pass is required (true|false). It no longer defaults to true — ' +
|
|
1871
|
+
'a completion record must not attest a test run that was never made.'
|
|
1872
|
+
)
|
|
1873
|
+
process.exit(1)
|
|
1874
|
+
}
|
|
1875
|
+
result = await recordCompletion(cwd, completionArgs)
|
|
1876
|
+
} else {
|
|
1877
|
+
const { completionGate } = await import('../lib/completion-gate.js')
|
|
1878
|
+
const gated = await completionGate({
|
|
1879
|
+
featureCode: featureCode,
|
|
1880
|
+
commitSha: commitSha,
|
|
1881
|
+
testsPass: testsPass,
|
|
1882
|
+
filesChanged: filesChanged,
|
|
1883
|
+
notes: flags['notes'] || undefined,
|
|
1884
|
+
force: flags['force'] === true || undefined,
|
|
1885
|
+
idempotencyKey: flags['idempotency-key'] || undefined,
|
|
1886
|
+
workspaceRoot: cwd,
|
|
1887
|
+
})
|
|
1888
|
+
if (!gated.ok) {
|
|
1889
|
+
console.error(`Refused at ${gated.refusedAt}:\n - ${gated.reasons.join('\n - ')}`)
|
|
1890
|
+
if (gated.refusedAt === 'evidence' && testsPass === undefined) {
|
|
1891
|
+
console.error(
|
|
1892
|
+
'\nHint: --tests-pass no longer defaults to true. Pass --tests-pass true|false, ' +
|
|
1893
|
+
'or configure guard.testCommand in .compose/compose.json so the test run itself attests.'
|
|
1894
|
+
)
|
|
1895
|
+
}
|
|
1896
|
+
process.exit(1)
|
|
1897
|
+
}
|
|
1898
|
+
result = gated.result
|
|
1899
|
+
if (gated.partial) {
|
|
1900
|
+
console.warn(
|
|
1901
|
+
`Completed, but a projection failed — ` +
|
|
1902
|
+
gated.failures.map(f => `${f.step}: ${f.message} (recover: ${f.recover})`).join('; ')
|
|
1903
|
+
)
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1702
1906
|
console.log(JSON.stringify({
|
|
1703
1907
|
completion_id: result.completion_id,
|
|
1704
1908
|
idempotent: result.idempotent,
|
|
@@ -2142,11 +2346,42 @@ if (cmd === 'guard') {
|
|
|
2142
2346
|
else if (st.state === 'stale') console.log('canon-guard: installed (stale — re-run `compose guard install`)')
|
|
2143
2347
|
else console.log('canon-guard: absent — run `compose guard install`')
|
|
2144
2348
|
console.log(` hook script: ${scriptState} (${hookScript})`)
|
|
2145
|
-
|
|
2349
|
+
// Derived from the registry, never hardcoded: a literal string here
|
|
2350
|
+
// silently under-reported the guarded set as soon as COMP-CANON-OVERRIDE
|
|
2351
|
+
// widened it. "Claude-runtime only" is not modesty — Bash and Codex writes
|
|
2352
|
+
// never reach this hook (drift detection, not enforcement).
|
|
2353
|
+
const { guardedDisplaysFor } = await import('../lib/canon-registry.js')
|
|
2354
|
+
for (const pattern of guardedDisplaysFor('hook')) {
|
|
2355
|
+
console.log(` guards: ${pattern} (Write|Edit|NotebookEdit) — Claude-runtime only`)
|
|
2356
|
+
}
|
|
2357
|
+
// `compose guard status` must keep working outside a workspace (it always
|
|
2358
|
+
// has, for the canon-guard hook state above); only the newer `signing:`
|
|
2359
|
+
// block needs a resolved workspace, so resolve it tolerantly here instead
|
|
2360
|
+
// of going through resolveCwdWithWorkspace -> dieOnWorkspaceError.
|
|
2361
|
+
let signingWorkspaceRoot = null
|
|
2362
|
+
try {
|
|
2363
|
+
const wsId = getWorkspaceFlag(args)
|
|
2364
|
+
signingWorkspaceRoot = resolveWorkspace({ workspaceId: wsId === '__COMPOSE_WORKSPACE_ID__' ? null : wsId }).root
|
|
2365
|
+
} catch { /* no workspace resolved — signing block is skipped below */ }
|
|
2366
|
+
|
|
2367
|
+
if (!signingWorkspaceRoot) {
|
|
2368
|
+
console.log('signing: (no workspace resolved — run inside a compose workspace or pass --workspace)')
|
|
2369
|
+
if (args.includes('--prune')) process.exit(1)
|
|
2370
|
+
process.exit(0)
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
const { pruneGuardGenerations, signingStatusLines } = await import('../lib/guard-cli.js')
|
|
2374
|
+
if (args.includes('--prune')) {
|
|
2375
|
+
const pruned = await pruneGuardGenerations(signingWorkspaceRoot)
|
|
2376
|
+
for (const line of pruned.lines) console.log(line)
|
|
2377
|
+
if (pruned.status === 'refused') process.exit(1)
|
|
2378
|
+
}
|
|
2379
|
+
const signing = await signingStatusLines(signingWorkspaceRoot, { coverage: args.includes('--coverage') })
|
|
2380
|
+
for (const line of signing.lines) console.log(line)
|
|
2146
2381
|
process.exit(0)
|
|
2147
2382
|
}
|
|
2148
2383
|
|
|
2149
|
-
console.error(`Unknown guard subcommand: "${sub}". Use: install | uninstall | status | init | verify [--fix]`)
|
|
2384
|
+
console.error(`Unknown guard subcommand: "${sub}". Use: install | uninstall | status [--prune] [--coverage] | init | verify [--fix] | enrol | sign | descriptors`)
|
|
2150
2385
|
process.exit(1)
|
|
2151
2386
|
}
|
|
2152
2387
|
|
|
@@ -2357,6 +2592,7 @@ if (cmd === 'experiment') {
|
|
|
2357
2592
|
}
|
|
2358
2593
|
|
|
2359
2594
|
if (cmd === 'build') {
|
|
2595
|
+
await emitDriftNudge()
|
|
2360
2596
|
// Parse --cwd <path> for cross-repo builds
|
|
2361
2597
|
let agentWorkDir = null
|
|
2362
2598
|
const cwdIdx = args.indexOf('--cwd')
|
|
@@ -2390,6 +2626,29 @@ if (cmd === 'build') {
|
|
|
2390
2626
|
console.error('Error: --template requires a name argument')
|
|
2391
2627
|
process.exit(1)
|
|
2392
2628
|
}
|
|
2629
|
+
// COMP-PIPELINE-QUARANTINE round 3: refuse templates that belong to another
|
|
2630
|
+
// command's runner. `compose build` is feature mode, which sends
|
|
2631
|
+
// {featureCode, description, implementer_agent, reviewer_agent}; these four
|
|
2632
|
+
// specs declare a different runner's envelope (task / projectName+intent /
|
|
2633
|
+
// gateCommands), so selecting one here resolves the spec and then fails at
|
|
2634
|
+
// plan time with an unhelpful error. Point at the command that can drive it.
|
|
2635
|
+
const MODE_BOUND_TEMPLATES = {
|
|
2636
|
+
'bug-fix': 'compose fix <bug-code>',
|
|
2637
|
+
plan: 'compose plan "<intent>"',
|
|
2638
|
+
new: 'compose new',
|
|
2639
|
+
gsd: 'compose gsd <feature-code>',
|
|
2640
|
+
}
|
|
2641
|
+
// Compare the BASENAME: resolveTemplatePath builds `<cwd>/pipelines/<name>.stratum.yaml`
|
|
2642
|
+
// via join(), which normalizes away a leading `./`, so `--template ./bug-fix`
|
|
2643
|
+
// resolved the same spec while slipping past an exact-string check.
|
|
2644
|
+
const templateKey = basename(String(templateValue)).replace(/\.stratum\.ya?ml$/, '')
|
|
2645
|
+
if (MODE_BOUND_TEMPLATES[templateKey]) {
|
|
2646
|
+
console.error(
|
|
2647
|
+
`Error: --template ${templateValue} is not a build template — it belongs to ` +
|
|
2648
|
+
`\`${MODE_BOUND_TEMPLATES[templateKey]}\`, which supplies the inputs it declares.`
|
|
2649
|
+
)
|
|
2650
|
+
process.exit(1)
|
|
2651
|
+
}
|
|
2393
2652
|
templateName = templateValue
|
|
2394
2653
|
}
|
|
2395
2654
|
if (teamTemplate && !templateName) {
|
|
@@ -2437,10 +2696,16 @@ if (cmd === 'build') {
|
|
|
2437
2696
|
}
|
|
2438
2697
|
}
|
|
2439
2698
|
|
|
2699
|
+
const { root: buildCwd } = resolveCwdWithWorkspace(args)
|
|
2700
|
+
|
|
2440
2701
|
// Multiple codes: compose build FEAT-1 FEAT-2 FEAT-3
|
|
2441
2702
|
const isMulti = featureCodes.length > 1
|
|
2442
|
-
// Single prefix: compose build STRAT-COMP (no trailing digit)
|
|
2443
|
-
|
|
2703
|
+
// Single prefix: compose build STRAT-COMP (no trailing digit). An exact
|
|
2704
|
+
// feature on disk always wins over the prefix heuristic — codes like
|
|
2705
|
+
// COMP-SEMVER-STRICT have no trailing digit but are single features.
|
|
2706
|
+
const isExactFeature = featureCodes.length === 1 && featureCode &&
|
|
2707
|
+
existsSync(join(buildCwd, 'docs', 'features', featureCode, 'feature.json'))
|
|
2708
|
+
const isPrefix = featureCodes.length === 1 && featureCode && !isExactFeature && !/\d$/.test(featureCode)
|
|
2444
2709
|
const isBatch = all || isPrefix || isMulti
|
|
2445
2710
|
|
|
2446
2711
|
if (abort && isBatch) {
|
|
@@ -2513,7 +2778,6 @@ if (cmd === 'build') {
|
|
|
2513
2778
|
// and there's no bundled preset fallback — so when --quick is requested and
|
|
2514
2779
|
// that file is absent, treat it as init-needed (runInit re-seeds it) rather
|
|
2515
2780
|
// than letting runBuild fail later with "Lifecycle spec not found".
|
|
2516
|
-
const { root: buildCwd } = resolveCwdWithWorkspace(args)
|
|
2517
2781
|
const needsInit =
|
|
2518
2782
|
!existsSync(join(buildCwd, '.compose', 'compose.json')) ||
|
|
2519
2783
|
!existsSync(join(buildCwd, 'pipelines', 'build.stratum.yaml')) ||
|
|
@@ -2626,7 +2890,11 @@ if (cmd === 'build') {
|
|
|
2626
2890
|
const { root: fixCwd } = resolveCwdWithWorkspace(args)
|
|
2627
2891
|
if (!existsSync(join(fixCwd, '.compose', 'compose.json')) || !existsSync(join(fixCwd, 'pipelines', 'bug-fix.stratum.yaml'))) {
|
|
2628
2892
|
console.log('Running compose init...\n')
|
|
2629
|
-
|
|
2893
|
+
// Thread the RESOLVED workspace root, exactly as the build path does. Without
|
|
2894
|
+
// it runInit seeds process.cwd(), which differs from fixCwd when `compose fix`
|
|
2895
|
+
// is invoked from a subdirectory — the guard checks fixCwd but the seed lands
|
|
2896
|
+
// elsewhere, so the run still fails with the same missing-spec error.
|
|
2897
|
+
await runInit(args.filter(a => a.startsWith('--')), fixCwd)
|
|
2630
2898
|
console.log('')
|
|
2631
2899
|
}
|
|
2632
2900
|
|
|
@@ -2703,6 +2971,7 @@ if (cmd === 'build') {
|
|
|
2703
2971
|
})
|
|
2704
2972
|
})
|
|
2705
2973
|
} else if (cmd === 'plan') {
|
|
2974
|
+
await emitDriftNudge()
|
|
2706
2975
|
// compose plan "<intent>" — runs the plan.stratum.yaml product-planning
|
|
2707
2976
|
// lifecycle (explore_design → plan → ship). Thin delegation to runBuild() with
|
|
2708
2977
|
// template='plan', mode='plan'. Derives a PLAN-<slug> code from the intent
|
|
@@ -3053,306 +3322,82 @@ if (cmd === 'build') {
|
|
|
3053
3322
|
}
|
|
3054
3323
|
process.exit(1)
|
|
3055
3324
|
})
|
|
3056
|
-
} else if (cmd === '
|
|
3325
|
+
} else if (cmd === 'judgment') {
|
|
3057
3326
|
// ---------------------------------------------------------------------------
|
|
3058
|
-
// compose
|
|
3327
|
+
// compose judgment trace <slug> — causal ancestry of a position
|
|
3328
|
+
// (COMP-JUDGMENT-PRECEDENT slice A). Read-only. The store has always persisted
|
|
3329
|
+
// revision chains and the `supersedes` ref; nothing surfaced them.
|
|
3059
3330
|
// ---------------------------------------------------------------------------
|
|
3060
|
-
const
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
if (existsSync(cfgPath)) {
|
|
3067
|
-
try { return JSON.parse(readFileSync(cfgPath, 'utf-8')) } catch {}
|
|
3068
|
-
}
|
|
3069
|
-
return {}
|
|
3070
|
-
}
|
|
3071
|
-
function getIdeaboxRelPath(cwd) {
|
|
3072
|
-
return loadComposeConfig(cwd)?.paths?.ideabox || 'docs/product/ideabox.md'
|
|
3073
|
-
}
|
|
3074
|
-
const ibConfig = loadComposeConfig(ibCwd)
|
|
3075
|
-
|
|
3076
|
-
const {
|
|
3077
|
-
parseIdeabox: _parseIdeabox,
|
|
3078
|
-
serializeIdeabox: _serializeIdeabox,
|
|
3079
|
-
addIdea: _addIdea,
|
|
3080
|
-
promoteIdea: _promoteIdea,
|
|
3081
|
-
killIdea: _killIdea,
|
|
3082
|
-
setPriority: _setPriority,
|
|
3083
|
-
loadLens: _loadLens,
|
|
3084
|
-
readIdeabox: _readIdeabox,
|
|
3085
|
-
writeIdeabox: _writeIdeabox,
|
|
3086
|
-
addDiscussion: _addDiscussion,
|
|
3087
|
-
} = await import('../lib/ideabox.js')
|
|
3088
|
-
|
|
3089
|
-
const ibRelPath = getIdeaboxRelPath(ibCwd)
|
|
3090
|
-
// ibFullPath is used for direct file ops (existsSync/mkdirSync/writeFileSync/
|
|
3091
|
-
// display); resolve it absolute-safe. ibRelPath stays relative because the
|
|
3092
|
-
// lib readIdeabox/writeIdeabox readers re-join it under ibCwd.
|
|
3093
|
-
const { resolveIdeaboxPath: _resolveIdeaboxPathCli } = await import('../lib/project-paths.js')
|
|
3094
|
-
const ibFullPath = _resolveIdeaboxPathCli(ibCwd)
|
|
3095
|
-
|
|
3096
|
-
if (!ibSubcmd || ibSubcmd === '--help' || ibSubcmd === '-h') {
|
|
3097
|
-
console.log('Usage: compose ideabox <subcommand>')
|
|
3098
|
-
console.log('')
|
|
3099
|
-
console.log('Subcommands:')
|
|
3100
|
-
console.log(' add "<title>" Add a new idea')
|
|
3101
|
-
console.log(' list List all ideas')
|
|
3102
|
-
console.log(' promote <ID> Mark idea as PROMOTED (creates feature folder)')
|
|
3103
|
-
console.log(' kill <ID> "<reason>" Move idea to Killed Ideas')
|
|
3104
|
-
console.log(' pri <ID> <P0|P1|P2> Set priority')
|
|
3105
|
-
console.log(' discuss <ID> "<comment>" Add a discussion comment')
|
|
3106
|
-
console.log(' triage [--lens <name>] Walk untriaged ideas and assign priorities')
|
|
3107
|
-
process.exit(0)
|
|
3331
|
+
// `args` excludes the command itself (const [,, cmd, ...args]), so the
|
|
3332
|
+
// subcommand is args[0] — same shape as `subcmd = args[0]` elsewhere.
|
|
3333
|
+
const sub = args[0]
|
|
3334
|
+
if (sub !== 'trace') {
|
|
3335
|
+
console.log('Usage: compose judgment trace <slug> [--json]')
|
|
3336
|
+
process.exit(sub ? 1 : 0)
|
|
3108
3337
|
}
|
|
3109
|
-
|
|
3110
|
-
if (
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
console.error('Usage: compose ideabox add "<title>" [--source "..."] [--desc "..."] [--cluster "..."]')
|
|
3114
|
-
process.exit(1)
|
|
3115
|
-
}
|
|
3116
|
-
// Parse optional flags
|
|
3117
|
-
const sourceIdx = args.indexOf('--source')
|
|
3118
|
-
const descIdx = args.indexOf('--desc')
|
|
3119
|
-
const clusterIdx = args.indexOf('--cluster')
|
|
3120
|
-
const tagsIdx = args.indexOf('--tags')
|
|
3121
|
-
const source = sourceIdx !== -1 ? args[sourceIdx + 1] : ''
|
|
3122
|
-
const description = descIdx !== -1 ? args[descIdx + 1] : ''
|
|
3123
|
-
const cluster = clusterIdx !== -1 ? args[clusterIdx + 1] : null
|
|
3124
|
-
const tagsRaw = tagsIdx !== -1 ? args[tagsIdx + 1] : ''
|
|
3125
|
-
const tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim().startsWith('#') ? t.trim() : `#${t.trim()}`) : []
|
|
3126
|
-
|
|
3127
|
-
if (!existsSync(ibFullPath)) {
|
|
3128
|
-
const { IDEABOX_TEMPLATE } = await import('../lib/ideabox.js')
|
|
3129
|
-
mkdirSync(dirname(ibFullPath), { recursive: true })
|
|
3130
|
-
writeFileSync(ibFullPath, IDEABOX_TEMPLATE)
|
|
3131
|
-
}
|
|
3132
|
-
|
|
3133
|
-
const parsed = _readIdeabox(ibCwd, ibRelPath)
|
|
3134
|
-
_addIdea(parsed, { title, description, source, tags, cluster })
|
|
3135
|
-
_writeIdeabox(ibCwd, ibRelPath, parsed)
|
|
3136
|
-
const newIdea = parsed.ideas[parsed.ideas.length - 1]
|
|
3137
|
-
console.log(`Added ${newIdea.id}: ${newIdea.title}`)
|
|
3138
|
-
process.exit(0)
|
|
3338
|
+
const slug = args[1]
|
|
3339
|
+
if (!slug || slug.startsWith('--')) {
|
|
3340
|
+
console.error('Usage: compose judgment trace <slug> [--json]')
|
|
3341
|
+
process.exit(1)
|
|
3139
3342
|
}
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
const parsed = _readIdeabox(ibCwd, ibRelPath)
|
|
3147
|
-
if (parsed.ideas.length === 0 && parsed.killed.length === 0) {
|
|
3148
|
-
console.log('No ideas yet.')
|
|
3343
|
+
const { root: jCwd } = resolveCwdWithWorkspace(args)
|
|
3344
|
+
const { getJudgmentTrace } = await import('../lib/judgment-writer.js')
|
|
3345
|
+
try {
|
|
3346
|
+
const t = await getJudgmentTrace(jCwd, slug)
|
|
3347
|
+
if (args.includes('--json')) {
|
|
3348
|
+
console.log(JSON.stringify(t, null, 2))
|
|
3149
3349
|
process.exit(0)
|
|
3150
3350
|
}
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
const
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
for (const idea of group) {
|
|
3169
|
-
const pri = idea.priority !== '—' ? ` [${idea.priority}]` : ''
|
|
3170
|
-
const tags = idea.tags.length ? ` ${idea.tags.join(' ')}` : ''
|
|
3171
|
-
console.log(` ${idea.id}${pri} ${idea.title}${tags}`)
|
|
3172
|
-
}
|
|
3173
|
-
}
|
|
3174
|
-
|
|
3175
|
-
if (parsed.killed.length > 0) {
|
|
3176
|
-
console.log(`\n[KILLED] (${parsed.killed.length})`)
|
|
3177
|
-
for (const idea of parsed.killed) {
|
|
3178
|
-
console.log(` ${idea.id} ${idea.title} — ${idea.killedReason}`)
|
|
3351
|
+
console.log(`${t.slug} [${t.status}]`)
|
|
3352
|
+
for (const r of t.revisions) {
|
|
3353
|
+
const marks = [r.retracted ? 'retracted' : null, r.conviction ? `conviction=${r.conviction}` : null]
|
|
3354
|
+
.filter(Boolean).join(' ')
|
|
3355
|
+
const d = Array.isArray(r.delta) && r.delta.length ? ` (${r.delta.join('; ')})` : ''
|
|
3356
|
+
console.log(` r${r.rev} ${r.written_at ?? '—'} ${r.claim_count} claim(s)${marks ? ' ' + marks : ''}${d}`)
|
|
3357
|
+
}
|
|
3358
|
+
if (Array.isArray(t.supersededBy) && t.supersededBy.length) {
|
|
3359
|
+
console.log(` superseded by: ${t.supersededBy.map(s => `${s.slug}#r${s.rev}`).join(', ')}`)
|
|
3360
|
+
}
|
|
3361
|
+
let anc = t.supersedes, depth = 1
|
|
3362
|
+
while (anc) {
|
|
3363
|
+
const pin = anc.pinned ? `#r${anc.rev}` : ''
|
|
3364
|
+
console.log(`${' '.repeat(depth)}↳ supersedes ${anc.slug}${pin} [${anc.status}]`)
|
|
3365
|
+
for (const r of anc.revisions) {
|
|
3366
|
+
const d = Array.isArray(r.delta) && r.delta.length ? ` (${r.delta.join('; ')})` : ''
|
|
3367
|
+
console.log(`${' '.repeat(depth + 1)}r${r.rev} ${r.written_at ?? '—'} conviction=${r.conviction ?? '—'}${d}`)
|
|
3179
3368
|
}
|
|
3369
|
+
anc = anc.supersedes; depth++
|
|
3180
3370
|
}
|
|
3371
|
+
if (t.cycle) console.error(` WARNING: supersession cycle — ${t.cycle}`)
|
|
3372
|
+
for (const w of t.warnings) console.error(` WARNING: ${w}`)
|
|
3181
3373
|
process.exit(0)
|
|
3374
|
+
} catch (err) {
|
|
3375
|
+
console.error(err?.message || String(err))
|
|
3376
|
+
process.exit(1)
|
|
3182
3377
|
}
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
const idea = parsed.ideas.find(i => i.id.toUpperCase() === ideaId.toUpperCase())
|
|
3199
|
-
if (!idea) {
|
|
3200
|
-
console.error(`Idea not found: ${ideaId}`)
|
|
3201
|
-
process.exit(1)
|
|
3202
|
-
}
|
|
3203
|
-
|
|
3204
|
-
// Generate feature code if not provided
|
|
3205
|
-
let resolvedCode = featureCode
|
|
3206
|
-
if (!resolvedCode) {
|
|
3207
|
-
// Derive a slug from the title
|
|
3208
|
-
const slug = idea.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 20).replace(/-+$/, '')
|
|
3209
|
-
resolvedCode = `IDEA-${idea.num}-${slug}`.toUpperCase()
|
|
3210
|
-
}
|
|
3211
|
-
|
|
3212
|
-
// Create feature folder if missing — respect paths.features (may be
|
|
3213
|
-
// relocated outside ibCwd — COMP-PATHS-EXTERNAL; resolve absolute, no re-root)
|
|
3214
|
-
const featuresBase = resolveFeaturesPathFromConfig(ibCwd, ibConfig)
|
|
3215
|
-
const featuresDir = join(featuresBase, resolvedCode)
|
|
3216
|
-
if (!existsSync(featuresDir)) {
|
|
3217
|
-
// COMP-MCP-VALIDATE-1: route through the validated writer (schema-guarded)
|
|
3218
|
-
// instead of a raw writeFileSync.
|
|
3219
|
-
const { writeFeature } = await import('../lib/feature-json.js')
|
|
3220
|
-
writeFeature(ibCwd, {
|
|
3221
|
-
code: resolvedCode,
|
|
3222
|
-
description: idea.title,
|
|
3223
|
-
status: 'PLANNED',
|
|
3224
|
-
promotedFrom: ideaId,
|
|
3225
|
-
createdAt: new Date().toISOString(),
|
|
3226
|
-
}, featuresBase)
|
|
3227
|
-
console.log(`Created feature folder: ${join(featuresBase, resolvedCode)}/`)
|
|
3228
|
-
}
|
|
3229
|
-
|
|
3230
|
-
_promoteIdea(parsed, ideaId, resolvedCode)
|
|
3231
|
-
_writeIdeabox(ibCwd, ibRelPath, parsed)
|
|
3232
|
-
console.log(`Promoted ${ideaId} → ${resolvedCode}`)
|
|
3233
|
-
process.exit(0)
|
|
3234
|
-
}
|
|
3235
|
-
|
|
3236
|
-
if (ibSubcmd === 'kill') {
|
|
3237
|
-
const ideaId = args[1]
|
|
3238
|
-
const reason = args[2] || ''
|
|
3239
|
-
if (!ideaId) {
|
|
3240
|
-
console.error('Usage: compose ideabox kill <ID> "<reason>"')
|
|
3241
|
-
process.exit(1)
|
|
3242
|
-
}
|
|
3243
|
-
if (!existsSync(ibFullPath)) {
|
|
3244
|
-
console.error(`Ideabox not found at ${ibFullPath}`)
|
|
3245
|
-
process.exit(1)
|
|
3246
|
-
}
|
|
3247
|
-
|
|
3248
|
-
const parsed = _readIdeabox(ibCwd, ibRelPath)
|
|
3249
|
-
_killIdea(parsed, ideaId, reason)
|
|
3250
|
-
_writeIdeabox(ibCwd, ibRelPath, parsed)
|
|
3251
|
-
console.log(`Killed ${ideaId}: ${reason}`)
|
|
3252
|
-
process.exit(0)
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
if (ibSubcmd === 'pri') {
|
|
3256
|
-
const ideaId = args[1]
|
|
3257
|
-
const priority = args[2]
|
|
3258
|
-
if (!ideaId || !priority) {
|
|
3259
|
-
console.error('Usage: compose ideabox pri <ID> <P0|P1|P2>')
|
|
3260
|
-
process.exit(1)
|
|
3261
|
-
}
|
|
3262
|
-
if (!existsSync(ibFullPath)) {
|
|
3263
|
-
console.error(`Ideabox not found at ${ibFullPath}`)
|
|
3264
|
-
process.exit(1)
|
|
3265
|
-
}
|
|
3266
|
-
|
|
3267
|
-
const parsed = _readIdeabox(ibCwd, ibRelPath)
|
|
3268
|
-
_setPriority(parsed, ideaId, priority)
|
|
3269
|
-
_writeIdeabox(ibCwd, ibRelPath, parsed)
|
|
3270
|
-
console.log(`Set ${ideaId} priority → ${priority}`)
|
|
3271
|
-
process.exit(0)
|
|
3272
|
-
}
|
|
3273
|
-
|
|
3274
|
-
if (ibSubcmd === 'discuss') {
|
|
3275
|
-
const ideaId = args[1]
|
|
3276
|
-
const comment = args[2]
|
|
3277
|
-
if (!ideaId || !comment) {
|
|
3278
|
-
console.error('Usage: compose ideabox discuss <ID> "<comment>"')
|
|
3279
|
-
process.exit(1)
|
|
3280
|
-
}
|
|
3281
|
-
if (!existsSync(ibFullPath)) {
|
|
3282
|
-
console.error(`Ideabox not found at ${ibFullPath}`)
|
|
3283
|
-
process.exit(1)
|
|
3284
|
-
}
|
|
3285
|
-
|
|
3286
|
-
const parsed = _readIdeabox(ibCwd, ibRelPath)
|
|
3287
|
-
_addDiscussion(parsed, ideaId, 'human', comment)
|
|
3288
|
-
_writeIdeabox(ibCwd, ibRelPath, parsed)
|
|
3289
|
-
const today = new Date().toISOString().slice(0, 10)
|
|
3290
|
-
console.log(`[${today}] human: ${comment}`)
|
|
3291
|
-
process.exit(0)
|
|
3378
|
+
} else if (cmd === 'ideabox') {
|
|
3379
|
+
// ---------------------------------------------------------------------------
|
|
3380
|
+
// compose ideabox — idea management CLI
|
|
3381
|
+
//
|
|
3382
|
+
// The implementation lives in lib/ideabox-cli.js (COMP-PLAN-IDEA-UNIFY S3b-1).
|
|
3383
|
+
// It was ~300 lines inline here, which made the cutover to the record store
|
|
3384
|
+
// testable only by spawning a subprocess. Behaviour is unchanged; the backing
|
|
3385
|
+
// store is not — every mutation now writes a record and re-renders
|
|
3386
|
+
// docs/product/ideabox.md as a projection of them.
|
|
3387
|
+
// ---------------------------------------------------------------------------
|
|
3388
|
+
const { root: ibCwd } = resolveCwdWithWorkspace(args)
|
|
3389
|
+
let ibConfig = {}
|
|
3390
|
+
const ibCfgPath = join(ibCwd, '.compose', 'compose.json')
|
|
3391
|
+
if (existsSync(ibCfgPath)) {
|
|
3392
|
+
try { ibConfig = JSON.parse(readFileSync(ibCfgPath, 'utf-8')) } catch {}
|
|
3292
3393
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
console.log('No ideabox found. Run: compose ideabox add "<title>" first.')
|
|
3300
|
-
process.exit(0)
|
|
3301
|
-
}
|
|
3302
|
-
|
|
3303
|
-
const parsed = _readIdeabox(ibCwd, ibRelPath)
|
|
3304
|
-
const untriaged = parsed.ideas.filter(i => i.priority === '—' && i.status === 'NEW')
|
|
3305
|
-
|
|
3306
|
-
if (untriaged.length === 0) {
|
|
3307
|
-
console.log('No untriaged ideas.')
|
|
3308
|
-
process.exit(0)
|
|
3309
|
-
}
|
|
3310
|
-
|
|
3311
|
-
let lens = null
|
|
3312
|
-
if (lensName) {
|
|
3313
|
-
lens = _loadLens(ibCwd, lensName)
|
|
3314
|
-
if (!lens) {
|
|
3315
|
-
console.warn(`Lens not found: docs/product/ideabox-priority-${lensName}.md`)
|
|
3316
|
-
} else {
|
|
3317
|
-
console.log(`Using lens: ${lensName}`)
|
|
3318
|
-
}
|
|
3319
|
-
}
|
|
3320
|
-
|
|
3321
|
-
// Interactive triage using readline
|
|
3322
|
-
const { createInterface } = await import('node:readline')
|
|
3323
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
3324
|
-
const question = (q) => new Promise(resolve => rl.question(q, resolve))
|
|
3325
|
-
|
|
3326
|
-
let changed = false
|
|
3327
|
-
for (const idea of untriaged) {
|
|
3328
|
-
console.log(`\n${idea.id}: ${idea.title}`)
|
|
3329
|
-
if (idea.description) console.log(` ${idea.description.slice(0, 120)}`)
|
|
3330
|
-
if (lens) console.log(` [lens: ${lensName}]`)
|
|
3331
|
-
|
|
3332
|
-
const ans = await question(' Priority [P0/P1/P2/skip]: ')
|
|
3333
|
-
const p = ans.trim().toUpperCase()
|
|
3334
|
-
if (['P0', 'P1', 'P2'].includes(p)) {
|
|
3335
|
-
_setPriority(parsed, idea.id, p)
|
|
3336
|
-
changed = true
|
|
3337
|
-
console.log(` Set ${idea.id} → ${p}`)
|
|
3338
|
-
} else {
|
|
3339
|
-
console.log(' Skipped')
|
|
3340
|
-
}
|
|
3341
|
-
}
|
|
3342
|
-
|
|
3343
|
-
rl.close()
|
|
3344
|
-
|
|
3345
|
-
if (changed) {
|
|
3346
|
-
_writeIdeabox(ibCwd, ibRelPath, parsed)
|
|
3347
|
-
console.log('\nSaved.')
|
|
3348
|
-
}
|
|
3349
|
-
process.exit(0)
|
|
3394
|
+
const { runIdeaboxCommand } = await import('../lib/ideabox-cli.js')
|
|
3395
|
+
try {
|
|
3396
|
+
process.exit(await runIdeaboxCommand(ibCwd, args, { config: ibConfig }))
|
|
3397
|
+
} catch (err) {
|
|
3398
|
+
console.error(err?.message || String(err))
|
|
3399
|
+
process.exit(1)
|
|
3350
3400
|
}
|
|
3351
|
-
|
|
3352
|
-
console.error(`Unknown ideabox subcommand: ${ibSubcmd}`)
|
|
3353
|
-
console.error('Run: compose ideabox --help')
|
|
3354
|
-
process.exit(1)
|
|
3355
|
-
|
|
3356
3401
|
} else if (cmd === 'qa-scope') {
|
|
3357
3402
|
// ---------------------------------------------------------------------------
|
|
3358
3403
|
// compose qa-scope <featureCode>
|
|
@@ -3477,6 +3522,96 @@ if (cmd === 'build') {
|
|
|
3477
3522
|
process.exit(1)
|
|
3478
3523
|
}
|
|
3479
3524
|
|
|
3525
|
+
} else if (cmd === 'lineage') {
|
|
3526
|
+
// ---------------------------------------------------------------------------
|
|
3527
|
+
// compose lineage <stamp|stale|show> --feature <FC> [--changed <file>] [--format text|json]
|
|
3528
|
+
// COMP-PROV-LINEAGE: W3C PROV-O artifact lineage (vocabulary only, no RDF).
|
|
3529
|
+
// stamp — materialise wasGeneratedBy/wasDerivedFrom markers into artifacts
|
|
3530
|
+
// stale — reachability query: given a --changed artifact, list stale descendants
|
|
3531
|
+
// show — print the derivation graph for the feature's canonical artifacts
|
|
3532
|
+
// See docs/features/COMP-PROV-LINEAGE/. Read-only except `stamp`; no server.
|
|
3533
|
+
// ---------------------------------------------------------------------------
|
|
3534
|
+
const lineageSub = args[0]
|
|
3535
|
+
const flagVal = (flag) => {
|
|
3536
|
+
const i = args.indexOf(flag)
|
|
3537
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : null
|
|
3538
|
+
}
|
|
3539
|
+
const feature = flagVal('--feature')
|
|
3540
|
+
const format = flagVal('--format') || 'text'
|
|
3541
|
+
if (!feature || !['stamp', 'stale', 'show'].includes(lineageSub)) {
|
|
3542
|
+
console.error('usage: compose lineage <stamp|stale|show> --feature <FC> [--changed <file>] [--format text|json]')
|
|
3543
|
+
process.exit(1)
|
|
3544
|
+
}
|
|
3545
|
+
const { isFeatureCode } = await import('../lib/feature-code.js')
|
|
3546
|
+
if (!isFeatureCode(feature)) {
|
|
3547
|
+
console.error(`Invalid feature code: ${JSON.stringify(feature)} (must match ${'^[A-Z][A-Z0-9-]*[A-Z0-9]$'})`)
|
|
3548
|
+
process.exit(1)
|
|
3549
|
+
}
|
|
3550
|
+
const { join } = await import('node:path')
|
|
3551
|
+
const { existsSync } = await import('node:fs')
|
|
3552
|
+
const { resolveFeaturesPath } = await import('../lib/project-paths.js')
|
|
3553
|
+
const {
|
|
3554
|
+
stampFeatureLineage, findStaleDescendants, buildDerivationGraph,
|
|
3555
|
+
} = await import('../lib/lineage.js')
|
|
3556
|
+
const featureDir = join(resolveFeaturesPath(process.cwd()), feature)
|
|
3557
|
+
if (!existsSync(featureDir)) {
|
|
3558
|
+
console.error(`No feature folder at ${featureDir}`)
|
|
3559
|
+
process.exit(1)
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
if (lineageSub === 'stamp') {
|
|
3563
|
+
const results = stampFeatureLineage(featureDir)
|
|
3564
|
+
if (format === 'json') {
|
|
3565
|
+
console.log(JSON.stringify(results, null, 2))
|
|
3566
|
+
} else if (results.length === 0) {
|
|
3567
|
+
console.log(`No canonical artifacts found in ${feature}.`)
|
|
3568
|
+
} else {
|
|
3569
|
+
const changed = results.filter(r => r.changed).length
|
|
3570
|
+
console.log(`Stamped lineage on ${feature} — ${changed} of ${results.length} artifact(s) updated:\n`)
|
|
3571
|
+
for (const r of results) {
|
|
3572
|
+
const from = r.wasDerivedFrom.length ? r.wasDerivedFrom.join(', ') : '(origin)'
|
|
3573
|
+
console.log(` ${r.changed ? '✓' : '·'} ${r.file} wasGeneratedBy=${r.wasGeneratedBy ?? '?'} wasDerivedFrom=${from}`)
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
} else if (lineageSub === 'stale') {
|
|
3577
|
+
const changedFile = flagVal('--changed')
|
|
3578
|
+
if (!changedFile) {
|
|
3579
|
+
console.error('compose lineage stale: --changed <file> is required')
|
|
3580
|
+
process.exit(1)
|
|
3581
|
+
}
|
|
3582
|
+
// Must be a bare filename inside the feature folder — no path traversal.
|
|
3583
|
+
if (changedFile.includes('/') || changedFile.includes('\\') || changedFile.includes('..')) {
|
|
3584
|
+
console.error(`compose lineage stale: --changed must be a bare filename, got ${JSON.stringify(changedFile)}`)
|
|
3585
|
+
process.exit(1)
|
|
3586
|
+
}
|
|
3587
|
+
const results = findStaleDescendants(featureDir, changedFile)
|
|
3588
|
+
if (format === 'json') {
|
|
3589
|
+
console.log(JSON.stringify(results, null, 2))
|
|
3590
|
+
} else {
|
|
3591
|
+
const stale = results.filter(r => r.stale)
|
|
3592
|
+
if (results.length === 0) {
|
|
3593
|
+
console.log(`${changedFile} has no downstream artifacts in ${feature}.`)
|
|
3594
|
+
} else if (stale.length === 0) {
|
|
3595
|
+
console.log(`No stale descendants of ${changedFile} — all ${results.length} downstream artifact(s) are newer.`)
|
|
3596
|
+
} else {
|
|
3597
|
+
console.log(`${stale.length} artifact(s) downstream of ${changedFile} are STALE (older than the change):\n`)
|
|
3598
|
+
for (const r of stale) console.log(` ⚠ ${r.file}`)
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
} else if (lineageSub === 'show') {
|
|
3602
|
+
const graph = buildDerivationGraph(featureDir)
|
|
3603
|
+
if (format === 'json') {
|
|
3604
|
+
console.log(JSON.stringify(Object.fromEntries(graph), null, 2))
|
|
3605
|
+
} else if (graph.size === 0) {
|
|
3606
|
+
console.log(`No canonical artifacts found in ${feature}.`)
|
|
3607
|
+
} else {
|
|
3608
|
+
console.log(`Derivation graph for ${feature}:\n`)
|
|
3609
|
+
for (const [file, parents] of graph) {
|
|
3610
|
+
console.log(parents.length ? ` ${file} ⟵ ${parents.join(', ')}` : ` ${file} (origin)`)
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3480
3615
|
} else if (cmd === 'gates' || cmd === 'gate') {
|
|
3481
3616
|
// ---------------------------------------------------------------------------
|
|
3482
3617
|
// compose gate list [--item <id>] [--status pending|all|resolved] [--format text|json]
|