@smartmemory/compose 0.3.7 → 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/.compose-deps.json +1 -13
- package/README.md +72 -5
- package/bin/compose.js +470 -351
- 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/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
|
@@ -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,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
|
|
|
@@ -568,10 +543,24 @@ async function runInit(flags, cwdOverride) {
|
|
|
568
543
|
}
|
|
569
544
|
}
|
|
570
545
|
|
|
571
|
-
// 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.
|
|
572
554
|
const pipelinesDir = join(cwd, 'pipelines')
|
|
573
555
|
mkdirSync(pipelinesDir, { recursive: true })
|
|
574
|
-
|
|
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) {
|
|
575
564
|
const dest = join(pipelinesDir, specName)
|
|
576
565
|
if (!existsSync(dest)) {
|
|
577
566
|
const src = join(PACKAGE_ROOT, 'pipelines', specName)
|
|
@@ -580,6 +569,12 @@ async function runInit(flags, cwdOverride) {
|
|
|
580
569
|
console.log(`Copied default pipeline to ${dest}`)
|
|
581
570
|
}
|
|
582
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
|
+
}
|
|
583
578
|
}
|
|
584
579
|
|
|
585
580
|
// 9. Sync all compose-owned skills to detected agents
|
|
@@ -757,11 +752,73 @@ async function runUpdate(flags) {
|
|
|
757
752
|
console.log(`compose updated to v${getPkgVersion()}${style === 'git' ? ` @ ${getGitSha(root) || '?'}` : ''}`)
|
|
758
753
|
}
|
|
759
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
|
+
|
|
760
816
|
// ---------------------------------------------------------------------------
|
|
761
817
|
// Command dispatch
|
|
762
818
|
// ---------------------------------------------------------------------------
|
|
763
819
|
|
|
764
820
|
if (cmd === 'init') {
|
|
821
|
+
await emitDriftNudge()
|
|
765
822
|
await runInit(args)
|
|
766
823
|
process.exit(0)
|
|
767
824
|
}
|
|
@@ -1161,38 +1218,122 @@ _This is a seed design doc created by \`compose feature\`. The \`compose build\`
|
|
|
1161
1218
|
process.exit(0)
|
|
1162
1219
|
}
|
|
1163
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
|
+
|
|
1164
1237
|
if (cmd === 'roadmap') {
|
|
1165
1238
|
const subcmd = args[0]
|
|
1166
1239
|
|
|
1167
1240
|
// compose roadmap generate — regenerate ROADMAP.md from feature.json files,
|
|
1168
1241
|
// converging to a fixed point before finishing.
|
|
1169
1242
|
if (subcmd === 'generate' || subcmd === 'gen') {
|
|
1170
|
-
const { writeRoadmap } = await import('../lib/roadmap-gen.js')
|
|
1171
1243
|
const { checkRoundtrip } = await import('../lib/roadmap-roundtrip.js')
|
|
1244
|
+
const { generateRoadmapFromBase } = await import('../lib/roadmap-gen.js')
|
|
1172
1245
|
const { listFeatures } = await import('../lib/feature-json.js')
|
|
1173
|
-
const { loadExternalPrefixes } = await import('../lib/project-paths.js')
|
|
1246
|
+
const { loadExternalPrefixes, resolveRoadmapPath, loadFeaturesDir } = await import('../lib/project-paths.js')
|
|
1174
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')
|
|
1175
1250
|
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
1176
1251
|
// 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.
|
|
1252
|
+
// of feature.json — never regenerate or overwrite it.
|
|
1180
1253
|
if (isNarrativeOwned(cwd)) {
|
|
1181
1254
|
console.log('narrative-owned workspace (roadmap.narrative=true) — ROADMAP.md is hand-authored; generate skipped.')
|
|
1182
1255
|
process.exit(0)
|
|
1183
1256
|
}
|
|
1184
|
-
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')
|
|
1185
1261
|
const externalPrefixes = loadExternalPrefixes(cwd)
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
//
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
}
|
|
1194
|
-
|
|
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
|
|
1195
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}`)
|
|
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`)
|
|
1196
1337
|
process.exit(0)
|
|
1197
1338
|
}
|
|
1198
1339
|
|
|
@@ -1592,7 +1733,7 @@ if (cmd === 'record-completion') {
|
|
|
1592
1733
|
//
|
|
1593
1734
|
// Flags:
|
|
1594
1735
|
// --commit-sha=<sha> required; full 40-char hex SHA (Decision 9)
|
|
1595
|
-
// --tests-pass=<bool> default
|
|
1736
|
+
// --tests-pass=<bool> REQUIRED (true|false) — no default (COMP-COMPLETION-GATE)
|
|
1596
1737
|
// --notes=<string> optional
|
|
1597
1738
|
// --files-changed-from-stdin read newline-separated paths from stdin
|
|
1598
1739
|
// --no-status set_status: false (don't flip status to COMPLETE)
|
|
@@ -1637,7 +1778,8 @@ if (cmd === 'record-completion') {
|
|
|
1637
1778
|
console.log('')
|
|
1638
1779
|
console.log('Options:')
|
|
1639
1780
|
console.log(' --commit-sha=<sha> Full 40-char hex SHA (required)')
|
|
1640
|
-
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)')
|
|
1641
1783
|
console.log(' --notes=<string> Optional provenance notes')
|
|
1642
1784
|
console.log(' --files-changed-from-stdin Read newline-separated repo-relative paths from stdin')
|
|
1643
1785
|
console.log(' --no-status Do not flip feature status to COMPLETE')
|
|
@@ -1660,8 +1802,13 @@ if (cmd === 'record-completion') {
|
|
|
1660
1802
|
process.exit(1)
|
|
1661
1803
|
}
|
|
1662
1804
|
|
|
1663
|
-
// Parse tests-pass: default
|
|
1664
|
-
|
|
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
|
|
1665
1812
|
if (flags['tests-pass'] !== undefined) {
|
|
1666
1813
|
const tp = flags['tests-pass']
|
|
1667
1814
|
if (tp === 'false' || tp === false) testsPass = false
|
|
@@ -1698,7 +1845,48 @@ if (cmd === 'record-completion') {
|
|
|
1698
1845
|
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
1699
1846
|
const { recordCompletion } = await import('../lib/completion-writer.js')
|
|
1700
1847
|
try {
|
|
1701
|
-
|
|
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
|
+
}
|
|
1702
1890
|
console.log(JSON.stringify({
|
|
1703
1891
|
completion_id: result.completion_id,
|
|
1704
1892
|
idempotent: result.idempotent,
|
|
@@ -2142,11 +2330,42 @@ if (cmd === 'guard') {
|
|
|
2142
2330
|
else if (st.state === 'stale') console.log('canon-guard: installed (stale — re-run `compose guard install`)')
|
|
2143
2331
|
else console.log('canon-guard: absent — run `compose guard install`')
|
|
2144
2332
|
console.log(` hook script: ${scriptState} (${hookScript})`)
|
|
2145
|
-
|
|
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)
|
|
2146
2365
|
process.exit(0)
|
|
2147
2366
|
}
|
|
2148
2367
|
|
|
2149
|
-
console.error(`Unknown guard subcommand: "${sub}". Use: install | uninstall | status | init | verify [--fix]`)
|
|
2368
|
+
console.error(`Unknown guard subcommand: "${sub}". Use: install | uninstall | status [--prune] [--coverage] | init | verify [--fix] | enrol | sign | descriptors`)
|
|
2150
2369
|
process.exit(1)
|
|
2151
2370
|
}
|
|
2152
2371
|
|
|
@@ -2357,6 +2576,7 @@ if (cmd === 'experiment') {
|
|
|
2357
2576
|
}
|
|
2358
2577
|
|
|
2359
2578
|
if (cmd === 'build') {
|
|
2579
|
+
await emitDriftNudge()
|
|
2360
2580
|
// Parse --cwd <path> for cross-repo builds
|
|
2361
2581
|
let agentWorkDir = null
|
|
2362
2582
|
const cwdIdx = args.indexOf('--cwd')
|
|
@@ -2390,6 +2610,29 @@ if (cmd === 'build') {
|
|
|
2390
2610
|
console.error('Error: --template requires a name argument')
|
|
2391
2611
|
process.exit(1)
|
|
2392
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
|
+
}
|
|
2393
2636
|
templateName = templateValue
|
|
2394
2637
|
}
|
|
2395
2638
|
if (teamTemplate && !templateName) {
|
|
@@ -2437,10 +2680,16 @@ if (cmd === 'build') {
|
|
|
2437
2680
|
}
|
|
2438
2681
|
}
|
|
2439
2682
|
|
|
2683
|
+
const { root: buildCwd } = resolveCwdWithWorkspace(args)
|
|
2684
|
+
|
|
2440
2685
|
// Multiple codes: compose build FEAT-1 FEAT-2 FEAT-3
|
|
2441
2686
|
const isMulti = featureCodes.length > 1
|
|
2442
|
-
// Single prefix: compose build STRAT-COMP (no trailing digit)
|
|
2443
|
-
|
|
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)
|
|
2444
2693
|
const isBatch = all || isPrefix || isMulti
|
|
2445
2694
|
|
|
2446
2695
|
if (abort && isBatch) {
|
|
@@ -2513,7 +2762,6 @@ if (cmd === 'build') {
|
|
|
2513
2762
|
// and there's no bundled preset fallback — so when --quick is requested and
|
|
2514
2763
|
// that file is absent, treat it as init-needed (runInit re-seeds it) rather
|
|
2515
2764
|
// than letting runBuild fail later with "Lifecycle spec not found".
|
|
2516
|
-
const { root: buildCwd } = resolveCwdWithWorkspace(args)
|
|
2517
2765
|
const needsInit =
|
|
2518
2766
|
!existsSync(join(buildCwd, '.compose', 'compose.json')) ||
|
|
2519
2767
|
!existsSync(join(buildCwd, 'pipelines', 'build.stratum.yaml')) ||
|
|
@@ -2626,7 +2874,11 @@ if (cmd === 'build') {
|
|
|
2626
2874
|
const { root: fixCwd } = resolveCwdWithWorkspace(args)
|
|
2627
2875
|
if (!existsSync(join(fixCwd, '.compose', 'compose.json')) || !existsSync(join(fixCwd, 'pipelines', 'bug-fix.stratum.yaml'))) {
|
|
2628
2876
|
console.log('Running compose init...\n')
|
|
2629
|
-
|
|
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)
|
|
2630
2882
|
console.log('')
|
|
2631
2883
|
}
|
|
2632
2884
|
|
|
@@ -2703,6 +2955,7 @@ if (cmd === 'build') {
|
|
|
2703
2955
|
})
|
|
2704
2956
|
})
|
|
2705
2957
|
} else if (cmd === 'plan') {
|
|
2958
|
+
await emitDriftNudge()
|
|
2706
2959
|
// compose plan "<intent>" — runs the plan.stratum.yaml product-planning
|
|
2707
2960
|
// lifecycle (explore_design → plan → ship). Thin delegation to runBuild() with
|
|
2708
2961
|
// template='plan', mode='plan'. Derives a PLAN-<slug> code from the intent
|
|
@@ -3053,306 +3306,82 @@ if (cmd === 'build') {
|
|
|
3053
3306
|
}
|
|
3054
3307
|
process.exit(1)
|
|
3055
3308
|
})
|
|
3056
|
-
} else if (cmd === '
|
|
3309
|
+
} else if (cmd === 'judgment') {
|
|
3057
3310
|
// ---------------------------------------------------------------------------
|
|
3058
|
-
// 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.
|
|
3059
3314
|
// ---------------------------------------------------------------------------
|
|
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)
|
|
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)
|
|
3108
3321
|
}
|
|
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)
|
|
3322
|
+
const slug = args[1]
|
|
3323
|
+
if (!slug || slug.startsWith('--')) {
|
|
3324
|
+
console.error('Usage: compose judgment trace <slug> [--json]')
|
|
3325
|
+
process.exit(1)
|
|
3139
3326
|
}
|
|
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.')
|
|
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))
|
|
3149
3333
|
process.exit(0)
|
|
3150
3334
|
}
|
|
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}`)
|
|
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}`)
|
|
3179
3352
|
}
|
|
3353
|
+
anc = anc.supersedes; depth++
|
|
3180
3354
|
}
|
|
3355
|
+
if (t.cycle) console.error(` WARNING: supersession cycle — ${t.cycle}`)
|
|
3356
|
+
for (const w of t.warnings) console.error(` WARNING: ${w}`)
|
|
3181
3357
|
process.exit(0)
|
|
3358
|
+
} catch (err) {
|
|
3359
|
+
console.error(err?.message || String(err))
|
|
3360
|
+
process.exit(1)
|
|
3182
3361
|
}
|
|
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)
|
|
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 {}
|
|
3292
3377
|
}
|
|
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)
|
|
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)
|
|
3350
3384
|
}
|
|
3351
|
-
|
|
3352
|
-
console.error(`Unknown ideabox subcommand: ${ibSubcmd}`)
|
|
3353
|
-
console.error('Run: compose ideabox --help')
|
|
3354
|
-
process.exit(1)
|
|
3355
|
-
|
|
3356
3385
|
} else if (cmd === 'qa-scope') {
|
|
3357
3386
|
// ---------------------------------------------------------------------------
|
|
3358
3387
|
// compose qa-scope <featureCode>
|
|
@@ -3477,6 +3506,96 @@ if (cmd === 'build') {
|
|
|
3477
3506
|
process.exit(1)
|
|
3478
3507
|
}
|
|
3479
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
|
+
|
|
3480
3599
|
} else if (cmd === 'gates' || cmd === 'gate') {
|
|
3481
3600
|
// ---------------------------------------------------------------------------
|
|
3482
3601
|
// compose gate list [--item <id>] [--status pending|all|resolved] [--format text|json]
|