@smartmemory/compose 0.2.49-beta → 0.2.51-beta
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 +24 -1
- package/.claude/skills/compose/references/hermes-tools.md +80 -0
- package/README.md +1 -1
- package/bin/compose.js +340 -70
- package/dist/assets/App-CT0vXPgd.js +724 -0
- package/dist/assets/_baseUniq-CsOHc_iS.js +1 -0
- package/dist/assets/{arc-B1TqAC_R.js → arc-BXht3LyE.js} +1 -1
- package/dist/assets/architectureDiagram-Q4EWVU46-BV1r86-w.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-BgcMJjAR.js +132 -0
- package/dist/assets/{browser-DW2JlJCC.js → browser-CnKiSnlr.js} +6 -6
- package/dist/assets/{c4Diagram-AAUBKEIU-Dc6swoQo.js → c4Diagram-AHTNJAMY-nBIRLJTv.js} +1 -1
- package/dist/assets/channel-Dd4XaiYv.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-CdqXgd68.js → chunk-4BX2VUAB-CNmGyhrp.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-MLT7I7w4.js +206 -0
- package/dist/assets/{chunk-55IACEB6-D7VauOmU.js → chunk-55IACEB6-D8KuKVLa.js} +1 -1
- package/dist/assets/{chunk-2J33WTMH-6HKDSnOw.js → chunk-EDXVE4YY-TPlt1bS2.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C1Z5m8DP.js → chunk-FMBD7UC4-tYY8xcjr.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-yBPwT1AT.js +231 -0
- package/dist/assets/{chunk-QZHKN3VN-DY9GU2uD.js → chunk-QZHKN3VN-B4Wfybww.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-DmqZMEQe.js → chunk-YZCP3GAM-DE6_5aqh.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DshSQDF9.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DshSQDF9.js +1 -0
- package/dist/assets/clone-CBsbNGAa.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-B34g9xzV.js → cose-bilkent-S5V4N54A-DzzRyJtB.js} +1 -1
- package/dist/assets/dagre-KV5264BT-CTcTVGdU.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-qR6mc8kJ.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-CUcGXyNb.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B68cPonH.js +43 -0
- package/dist/assets/diagram-TYMM5635-q8Id8eFG.js +24 -0
- package/dist/assets/{erDiagram-TEJ5UH35-C05_Iqmb.js → erDiagram-SMLLAGMA-CtxJi57K.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-lXpnb-Ch.js → flowDiagram-DWJPFMVM-BzzO4JYU.js} +1 -1
- package/dist/assets/ganttDiagram-T4ZO3ILL-Dn0wIyhR.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-tnNFXfMM.js +106 -0
- package/dist/assets/graph-DZe55uk8.js +331 -0
- package/dist/assets/graph-Tq_bs_r0.js +1 -0
- package/dist/assets/index-8UhRLbGq.js +123 -0
- package/dist/assets/index-CRqB9els.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BXLvLeS0.js +2 -0
- package/dist/assets/{ishikawaDiagram-YF4QCWOH-CgdsWdgu.js → ishikawaDiagram-UXIWVN3A-DWx-7BBy.js} +5 -5
- package/dist/assets/{journeyDiagram-JHISSGLW-CyvHmE3z.js → journeyDiagram-VCZTEJTY-C42hXNha.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-Bqjdyv0T.js → kanban-definition-6JOO6SKY-CEq930ew.js} +1 -1
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-qtUgN9BC.js +1 -0
- package/dist/assets/{linear-DecnRXf0.js → linear-D158F7OT.js} +1 -1
- package/dist/assets/min-hESxN-c0.js +1 -0
- package/dist/assets/{mindmap-definition-RKZ34NQL-DM1x9vNY.js → mindmap-definition-QFDTVHPH-d9SSh2nG.js} +7 -7
- package/dist/assets/mobile-1gVCT0OK.css +1 -0
- package/dist/assets/mobile-Chw8RWyH.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-gglmeMav.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-CqFC_htU.js +7 -0
- package/dist/assets/{requirementDiagram-4Y6WPE33-DD-Gg_y5.js → requirementDiagram-MS252O5E-B1ZFIotK.js} +1 -1
- package/dist/assets/sankeyDiagram-XADWPNL6-BvItvZt5.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-CAIn5Z3U.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CyJrGzib.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DprhF_Ho.js +1 -0
- package/dist/assets/{timeline-definition-PNZ67QCA-BPm2AEQj.js → timeline-definition-GMOUNBTQ-k_0vJQAK.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-DQCDWwlj.js → vennDiagram-DHZGUBPP-TZAW60b0.js} +4 -4
- package/dist/assets/wardley-RL74JXVD-DzQEoG6X.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-BAoVVIjZ.js +20 -0
- package/dist/assets/{xychartDiagram-2RQKCTM6-Ba05SXXF.js → xychartDiagram-5P7HB3ND-CynkZcUb.js} +1 -1
- package/dist/index.html +4 -4
- package/lib/build-all.js +2 -1
- package/lib/build.js +363 -38
- package/lib/checkpoint/checkpoint-writer.js +3 -2
- package/lib/codex-preflight.js +151 -0
- package/lib/completion-writer.js +59 -15
- package/lib/feature-json.js +14 -3
- package/lib/feature-validator.js +27 -10
- package/lib/feature-write-guard.js +5 -7
- package/lib/feature-writer.js +3 -3
- package/lib/followup-writer.js +2 -2
- package/lib/get-roadmap.js +2 -2
- package/lib/gsd.js +2 -1
- package/lib/hooks-status.js +135 -0
- package/lib/ideabox.js +3 -2
- package/lib/install-agent-defs.js +22 -0
- package/lib/journal-writer.js +3 -3
- package/lib/migrate-anon.js +277 -0
- package/lib/migrate-roadmap.js +2 -3
- package/lib/paths-core.js +47 -0
- package/lib/project-paths.js +46 -37
- package/lib/roadmap-gen.js +3 -4
- package/lib/roadmap-graph/collect.js +5 -5
- package/lib/roadmap-graph/index.js +2 -2
- package/lib/roadmap-parser.js +1 -1
- package/lib/state-migrations.js +3 -3
- package/lib/test-bootstrap.js +165 -2
- package/lib/tracker/local-provider.js +2 -2
- package/lib/triage.js +7 -5
- package/lib/vocabulary-inject.js +103 -0
- package/lib/xref-push.js +4 -4
- package/lib/xref-sync.js +4 -4
- package/package.json +2 -1
- package/pipelines/build-quick.stratum.yaml +433 -0
- package/pipelines/build.stratum.yaml +60 -3
- package/scripts/watch-server.sh +32 -0
- package/server/build-routes.js +89 -9
- package/server/build-stream-bridge.js +12 -0
- package/server/compose-mcp-tools.js +73 -1
- package/server/compose-mcp.js +27 -0
- package/server/design-routes.js +9 -4
- package/server/drift-axes.js +5 -3
- package/server/feature-scaffold-routes.js +82 -0
- package/server/feature-scan.js +8 -2
- package/server/file-watcher.js +13 -7
- package/server/health-routes.js +261 -0
- package/server/ideabox-routes.js +12 -11
- package/server/project-root.js +7 -4
- package/server/qa-scope-routes.js +96 -0
- package/server/session-routes.js +2 -2
- package/server/stratum-sync.js +85 -9
- package/server/validate-routes.js +70 -0
- package/server/vision-routes.js +46 -2
- package/server/vision-server.js +25 -3
- package/server/vision-utils.js +23 -4
- package/dist/assets/App-DOerhoDp.js +0 -706
- package/dist/assets/architectureDiagram-3BPJPVTR-C4hfxNOw.js +0 -36
- package/dist/assets/blockDiagram-GPEHLZMM-DRqkGncy.js +0 -132
- package/dist/assets/channel-KsY1x-7s.js +0 -1
- package/dist/assets/chunk-727SXJPM-CeKpXDif.js +0 -206
- package/dist/assets/chunk-AQP2D5EJ-y2XL-YHV.js +0 -231
- package/dist/assets/classDiagram-4FO5ZUOK-zDEDoVPk.js +0 -1
- package/dist/assets/classDiagram-v2-Q7XG4LA2-zDEDoVPk.js +0 -1
- package/dist/assets/dagre-BM42HDAG-klZ_GOcw.js +0 -4
- package/dist/assets/diagram-2AECGRRQ-D0FmM6Wf.js +0 -43
- package/dist/assets/diagram-5GNKFQAL-DYuhby6q.js +0 -10
- package/dist/assets/diagram-KO2AKTUF-CCtQZgal.js +0 -3
- package/dist/assets/diagram-LMA3HP47-Cd1OqEln.js +0 -24
- package/dist/assets/diagram-OG6HWLK6-CiJrhoxo.js +0 -24
- package/dist/assets/ganttDiagram-6RSMTGT7-EPbL_2Hr.js +0 -292
- package/dist/assets/gitGraphDiagram-PVQCEYII-COCoF_vW.js +0 -106
- package/dist/assets/graph-BBXaecIU.js +0 -331
- package/dist/assets/graph-CAnANduQ.js +0 -1
- package/dist/assets/index-BwLfbbOu.css +0 -1
- package/dist/assets/index-DlgxA8dX.js +0 -123
- package/dist/assets/infoDiagram-5YYISTIA-B1sJw6j5.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DGIYPm2g.js +0 -1
- package/dist/assets/mobile-Cag5dHlF.css +0 -1
- package/dist/assets/mobile-DwmxS_O4.js +0 -17
- package/dist/assets/pieDiagram-4H26LBE5-CRtmCvwV.js +0 -30
- package/dist/assets/quadrantDiagram-W4KKPZXB-CsBLWg27.js +0 -7
- package/dist/assets/sankeyDiagram-5OEKKPKP-BPdMN8UU.js +0 -40
- package/dist/assets/sequenceDiagram-3UESZ5HK-D4AENdze.js +0 -162
- package/dist/assets/stateDiagram-AJRCARHV-DDplpCf_.js +0 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-J4t9AVzF.js +0 -1
- package/dist/assets/wardley-L42UT6IY-WERqOmJt.js +0 -173
- package/dist/assets/wardleyDiagram-YWT4CUSO-5teUSOiI.js +0 -78
package/bin/compose.js
CHANGED
|
@@ -17,6 +17,8 @@ import { fileURLToPath } from 'url'
|
|
|
17
17
|
import { findProjectRoot } from '../server/find-root.js'
|
|
18
18
|
import { resolveWorkspace, getWorkspaceFlag } from '../lib/resolve-workspace.js'
|
|
19
19
|
import { resolvePort } from '../lib/resolve-port.js'
|
|
20
|
+
import { resolveRoadmapPath, resolveFeaturesPath, resolveContextPathFromConfig, resolveFeaturesPathFromConfig, resolveRoadmapPathFromConfig } from '../lib/project-paths.js'
|
|
21
|
+
import { installAgentDefs } from '../lib/install-agent-defs.js'
|
|
20
22
|
|
|
21
23
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
22
24
|
const PACKAGE_ROOT = resolve(__dirname, '..')
|
|
@@ -83,6 +85,7 @@ function resolveCwdWithWorkspace(args) {
|
|
|
83
85
|
import { parseTeamFlag } from '../lib/team-flag.js';
|
|
84
86
|
import { loadDeps, checkExternalSkills, printDepReport, buildDepReport, checkExternalBinaries, printBinaryReport, buildBinaryReport } from '../lib/deps.js';
|
|
85
87
|
import { checkLatestVersion } from '../lib/version-check.js';
|
|
88
|
+
import { computeHooksStatus, formatHookStatusLines, HOOK_MARKERS } from '../lib/hooks-status.js';
|
|
86
89
|
|
|
87
90
|
const [,, cmd, ...args] = process.argv
|
|
88
91
|
|
|
@@ -123,10 +126,14 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
|
123
126
|
console.log(' roadmap check Verify feature.json and ROADMAP.md are in sync')
|
|
124
127
|
console.log(' roadmap xref-sync Pull-reconcile feature.json external links to live state')
|
|
125
128
|
console.log(' roadmap xref-push Push-write GitHub trackers to match expect= (dry-run; --apply to write)')
|
|
129
|
+
console.log(' migrate-anon Promote historical anonymous ROADMAP rows to typed features (interactive)')
|
|
126
130
|
console.log(' items List vision items from local state (no server)')
|
|
127
131
|
console.log(' items show <id> Show detail for a specific vision item')
|
|
128
132
|
console.log(' triage Analyze a feature and recommend build profile')
|
|
129
133
|
console.log(' qa-scope Show affected routes from a feature\'s changed files')
|
|
134
|
+
console.log(' context decisions Show the build decision log (--feature <FC>, --format text|json)')
|
|
135
|
+
console.log(' gate list List pending gates (--item <id>, --status pending|all|resolved)')
|
|
136
|
+
console.log(' gate resolve <id> Resolve a gate (--approve|--revise|--kill, --comment <text>)')
|
|
130
137
|
console.log(' init Initialize Compose in the current project')
|
|
131
138
|
console.log(' setup Install/sync global skills + register stratum-mcp (alias: sync)')
|
|
132
139
|
console.log(' sync Re-sync global skills from this install (alias of setup)')
|
|
@@ -243,6 +250,17 @@ function syncSkills(agents) {
|
|
|
243
250
|
copyFileSync(depsSrc, join(composeSkillDir, '.compose-deps.json'))
|
|
244
251
|
}
|
|
245
252
|
|
|
253
|
+
// COMP-AGENT-VENDOR-1: install the vendored Claude subagents the compose
|
|
254
|
+
// SKILL.md depends on (compose-explorer/compose-architect) into the Claude
|
|
255
|
+
// agents dir — sibling of the skills root. Claude tree only (basename check):
|
|
256
|
+
// gemini is skipped above and codex shares the claude root.
|
|
257
|
+
if (basename(dirname(agentSkillsRoot)) === '.claude') {
|
|
258
|
+
const agentDefsDest = join(dirname(agentSkillsRoot), 'agents')
|
|
259
|
+
for (const d of installAgentDefs(join(PACKAGE_ROOT, '.claude', 'agents'), agentDefsDest)) {
|
|
260
|
+
console.log(` + ${agent.name}/agents/${d}`)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
246
264
|
// Remove skills we previously installed that no longer exist in source
|
|
247
265
|
const removed = previousSkills.filter(name => !sourceSkills.has(name))
|
|
248
266
|
for (const name of removed) {
|
|
@@ -412,6 +430,7 @@ async function runInit(flags, cwdOverride) {
|
|
|
412
430
|
},
|
|
413
431
|
paths: {
|
|
414
432
|
docs: 'docs',
|
|
433
|
+
roadmap: 'ROADMAP.md',
|
|
415
434
|
features: 'docs/features',
|
|
416
435
|
journal: 'docs/journal',
|
|
417
436
|
context: 'docs/context',
|
|
@@ -447,8 +466,9 @@ async function runInit(flags, cwdOverride) {
|
|
|
447
466
|
}
|
|
448
467
|
}
|
|
449
468
|
|
|
450
|
-
// 5b. Scaffold docs/context/ with ambient context templates
|
|
451
|
-
|
|
469
|
+
// 5b. Scaffold docs/context/ with ambient context templates.
|
|
470
|
+
// COMP-PATHS-EXTERNAL: resolve from the in-memory config (may be relocated).
|
|
471
|
+
const contextDir = resolveContextPathFromConfig(cwd, config)
|
|
452
472
|
mkdirSync(contextDir, { recursive: true })
|
|
453
473
|
const contextTemplates = {
|
|
454
474
|
'tech-stack.md': '# Tech Stack\n\nDescribe your technology stack here.\n',
|
|
@@ -464,8 +484,8 @@ async function runInit(flags, cwdOverride) {
|
|
|
464
484
|
}
|
|
465
485
|
|
|
466
486
|
// 5c. Scaffold docs/product/ideabox.md if absent
|
|
467
|
-
const
|
|
468
|
-
const ideaboxDest =
|
|
487
|
+
const { resolveIdeaboxPath: _resolveIdeaboxPathInit } = await import('../lib/project-paths.js')
|
|
488
|
+
const ideaboxDest = _resolveIdeaboxPathInit(cwd)
|
|
469
489
|
if (!existsSync(ideaboxDest)) {
|
|
470
490
|
mkdirSync(dirname(ideaboxDest), { recursive: true })
|
|
471
491
|
const { IDEABOX_TEMPLATE } = await import('../lib/ideabox.js')
|
|
@@ -473,6 +493,16 @@ async function runInit(flags, cwdOverride) {
|
|
|
473
493
|
console.log(`Created ${ideaboxDest}`)
|
|
474
494
|
}
|
|
475
495
|
|
|
496
|
+
// 5d. STRAT-VOCAB-3: scaffold contracts/vocabulary.yaml (starter is comments-only,
|
|
497
|
+
// so vocabulary enforcement stays inert until the user fills it in).
|
|
498
|
+
const { VOCABULARY_FILE, VOCABULARY_TEMPLATE } = await import('../lib/vocabulary-inject.js')
|
|
499
|
+
const vocabDest = join(cwd, VOCABULARY_FILE)
|
|
500
|
+
if (!existsSync(vocabDest)) {
|
|
501
|
+
mkdirSync(dirname(vocabDest), { recursive: true })
|
|
502
|
+
writeFileSync(vocabDest, VOCABULARY_TEMPLATE)
|
|
503
|
+
console.log(`Created ${vocabDest}`)
|
|
504
|
+
}
|
|
505
|
+
|
|
476
506
|
// 6. Register compose-mcp in .mcp.json
|
|
477
507
|
const mcpPath = join(cwd, '.mcp.json')
|
|
478
508
|
let mcpConfig = {}
|
|
@@ -513,8 +543,10 @@ async function runInit(flags, cwdOverride) {
|
|
|
513
543
|
}
|
|
514
544
|
}
|
|
515
545
|
|
|
516
|
-
// 7. Scaffold ROADMAP.md from template if absent
|
|
517
|
-
|
|
546
|
+
// 7. Scaffold ROADMAP.md from template if absent. COMP-PATHS-EXTERNAL:
|
|
547
|
+
// honor a configured (possibly relocated) paths.roadmap instead of always
|
|
548
|
+
// seeding <cwd>/ROADMAP.md. Byte-identical for the default.
|
|
549
|
+
const roadmapDest = resolveRoadmapPathFromConfig(cwd, config)
|
|
518
550
|
if (!existsSync(roadmapDest)) {
|
|
519
551
|
const roadmapSrc = join(PACKAGE_ROOT, 'templates', 'ROADMAP.md')
|
|
520
552
|
if (existsSync(roadmapSrc)) {
|
|
@@ -533,7 +565,7 @@ async function runInit(flags, cwdOverride) {
|
|
|
533
565
|
// 8. Copy default pipeline specs if absent
|
|
534
566
|
const pipelinesDir = join(cwd, 'pipelines')
|
|
535
567
|
mkdirSync(pipelinesDir, { recursive: true })
|
|
536
|
-
for (const specName of ['build.stratum.yaml', 'new.stratum.yaml']) {
|
|
568
|
+
for (const specName of ['build.stratum.yaml', 'build-quick.stratum.yaml', 'new.stratum.yaml']) {
|
|
537
569
|
const dest = join(pipelinesDir, specName)
|
|
538
570
|
if (!existsSync(dest)) {
|
|
539
571
|
const src = join(PACKAGE_ROOT, 'pipelines', specName)
|
|
@@ -756,6 +788,24 @@ if (cmd === 'migrate-state') {
|
|
|
756
788
|
process.exit(0)
|
|
757
789
|
}
|
|
758
790
|
|
|
791
|
+
if (cmd === 'migrate-anon') {
|
|
792
|
+
// COMP-MCP-MIGRATION-2-1-1-1 — interactively promote historical anonymous
|
|
793
|
+
// ROADMAP rows to typed features. The non-TTY guard lives here (where
|
|
794
|
+
// process.stdin does): piped stdin / --non-interactive / --dry-run → list-only,
|
|
795
|
+
// never hang waiting for a prompt.
|
|
796
|
+
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
797
|
+
const nonInteractive = args.includes('--non-interactive') || args.includes('--dry-run') || !process.stdin.isTTY
|
|
798
|
+
const dryRun = args.includes('--dry-run')
|
|
799
|
+
const { runMigrateAnon } = await import('../lib/migrate-anon.js')
|
|
800
|
+
try {
|
|
801
|
+
await runMigrateAnon(cwd, { nonInteractive, dryRun })
|
|
802
|
+
} catch (err) {
|
|
803
|
+
console.error(`\nError: ${err.message}`)
|
|
804
|
+
process.exit(1)
|
|
805
|
+
}
|
|
806
|
+
process.exit(0)
|
|
807
|
+
}
|
|
808
|
+
|
|
759
809
|
if (cmd === 'install') {
|
|
760
810
|
// Backwards-compat: run both init + setup
|
|
761
811
|
await runInit(args)
|
|
@@ -942,10 +992,12 @@ if (cmd === 'feature') {
|
|
|
942
992
|
|
|
943
993
|
let config = {}
|
|
944
994
|
try { config = JSON.parse(readFileSync(configPath, 'utf-8')) } catch {}
|
|
945
|
-
|
|
995
|
+
// COMP-PATHS-EXTERNAL: absolute features dir (may be relocated outside cwd).
|
|
996
|
+
// Never join(cwd, …) below — that would re-root an absolute/../-escaping override.
|
|
997
|
+
const featuresDir = resolveFeaturesPath(cwd)
|
|
946
998
|
|
|
947
999
|
// 1. Create feature folder + seed design doc
|
|
948
|
-
const featureDir = join(
|
|
1000
|
+
const featureDir = join(featuresDir, featureCode)
|
|
949
1001
|
const designPath = join(featureDir, 'design.md')
|
|
950
1002
|
|
|
951
1003
|
if (existsSync(designPath)) {
|
|
@@ -985,7 +1037,7 @@ if (cmd === 'feature') {
|
|
|
985
1037
|
// Count existing features for complexity estimate
|
|
986
1038
|
const existingFeatureCount = (() => {
|
|
987
1039
|
try {
|
|
988
|
-
const fdir =
|
|
1040
|
+
const fdir = featuresDir
|
|
989
1041
|
if (!existsSync(fdir)) return 0
|
|
990
1042
|
return readdirSync(fdir, { withFileTypes: true })
|
|
991
1043
|
.filter(e => e.isDirectory() && existsSync(join(fdir, e.name, 'feature.json')))
|
|
@@ -1042,7 +1094,7 @@ _This is a seed design doc created by \`compose feature\`. The \`compose build\`
|
|
|
1042
1094
|
console.log(`Created ${designPath}`)
|
|
1043
1095
|
|
|
1044
1096
|
// 2. Add entry to ROADMAP.md
|
|
1045
|
-
const roadmapPath =
|
|
1097
|
+
const roadmapPath = resolveRoadmapPath(cwd)
|
|
1046
1098
|
if (existsSync(roadmapPath)) {
|
|
1047
1099
|
let roadmap = readFileSync(roadmapPath, 'utf-8')
|
|
1048
1100
|
|
|
@@ -1157,7 +1209,7 @@ if (cmd === 'roadmap') {
|
|
|
1157
1209
|
const { checkRoundtrip, describeLossyDiff } = await import('../lib/roadmap-roundtrip.js')
|
|
1158
1210
|
const { loadExternalPrefixes } = await import('../lib/project-paths.js')
|
|
1159
1211
|
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
1160
|
-
const roadmapPath =
|
|
1212
|
+
const roadmapPath = resolveRoadmapPath(cwd)
|
|
1161
1213
|
if (!existsSync(roadmapPath)) {
|
|
1162
1214
|
console.error('No ROADMAP.md found. Run: compose roadmap generate')
|
|
1163
1215
|
process.exit(1)
|
|
@@ -1350,7 +1402,7 @@ if (cmd === 'roadmap') {
|
|
|
1350
1402
|
}
|
|
1351
1403
|
|
|
1352
1404
|
const { root: cwd } = resolveCwdWithWorkspace(args)
|
|
1353
|
-
const roadmapPath =
|
|
1405
|
+
const roadmapPath = resolveRoadmapPath(cwd)
|
|
1354
1406
|
|
|
1355
1407
|
if (existsSync(roadmapPath)) {
|
|
1356
1408
|
// Show cwd roadmap
|
|
@@ -1593,12 +1645,12 @@ if (cmd === 'hooks') {
|
|
|
1593
1645
|
const HOOK_TYPES = {
|
|
1594
1646
|
'post-commit': {
|
|
1595
1647
|
template: pjoin(presolve(futp(import.meta.url), '..'), 'git-hooks', 'post-commit.template'),
|
|
1596
|
-
marker: '
|
|
1648
|
+
marker: HOOK_MARKERS['post-commit'],
|
|
1597
1649
|
dest: pjoin(hooksDir, 'post-commit'),
|
|
1598
1650
|
},
|
|
1599
1651
|
'pre-push': {
|
|
1600
1652
|
template: pjoin(presolve(futp(import.meta.url), '..'), 'git-hooks', 'pre-push.template'),
|
|
1601
|
-
marker: '
|
|
1653
|
+
marker: HOOK_MARKERS['pre-push'],
|
|
1602
1654
|
dest: pjoin(hooksDir, 'pre-push'),
|
|
1603
1655
|
},
|
|
1604
1656
|
}
|
|
@@ -1667,48 +1719,14 @@ if (cmd === 'hooks') {
|
|
|
1667
1719
|
return 0
|
|
1668
1720
|
}
|
|
1669
1721
|
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
function statusOne(type) {
|
|
1676
|
-
const { marker, dest } = HOOK_TYPES[type]
|
|
1677
|
-
if (!exSync(dest)) {
|
|
1678
|
-
console.log(`${type}: absent — no hook installed`)
|
|
1679
|
-
return
|
|
1680
|
-
}
|
|
1681
|
-
const content = rfSync(dest, 'utf-8')
|
|
1682
|
-
if (!content.includes(marker)) {
|
|
1683
|
-
console.log(`${type}: foreign — hook exists but is not a Compose hook`)
|
|
1684
|
-
return
|
|
1685
|
-
}
|
|
1722
|
+
// Resolve the expected workspace id once (identical across hook types).
|
|
1723
|
+
// Hook-status detection itself lives in lib/hooks-status.js (shared with the
|
|
1724
|
+
// /api/environment-health endpoint); this just supplies the comparison id.
|
|
1725
|
+
function resolveExpectedWsId() {
|
|
1686
1726
|
const wsHint = hookFlags['workspace']
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
} else {
|
|
1691
|
-
try { expectedWsId = resolveWorkspace({ cwd: projectRoot }).id } catch { /* ignore for status */ }
|
|
1692
|
-
}
|
|
1693
|
-
const nodeMatch = content.includes(`COMPOSE_NODE="${composeNode}"`)
|
|
1694
|
-
const binMatch = content.includes(`COMPOSE_BIN="${composeBin}"`)
|
|
1695
|
-
const hasRawToken = content.includes('__COMPOSE_WORKSPACE_ID__')
|
|
1696
|
-
const wsMatch = hasRawToken ? false
|
|
1697
|
-
: expectedWsId ? content.includes(`COMPOSE_WORKSPACE_ID="${expectedWsId}"`)
|
|
1698
|
-
: true
|
|
1699
|
-
if (nodeMatch && binMatch && wsMatch && !hasRawToken) {
|
|
1700
|
-
console.log(`${type}: installed (current)`)
|
|
1701
|
-
const baked = extractBakedWorkspaceId(content)
|
|
1702
|
-
if (baked) console.log(` workspace: ${baked}`)
|
|
1703
|
-
} else {
|
|
1704
|
-
const reason = hasRawToken ? 'MISSING_WORKSPACE_ID'
|
|
1705
|
-
: (expectedWsId && !wsMatch) ? 'STALE_WORKSPACE_ID'
|
|
1706
|
-
: 'stale paths'
|
|
1707
|
-
console.log(`${type}: installed (${reason} — re-run install)`)
|
|
1708
|
-
if (expectedWsId && !wsMatch && !hasRawToken) console.log(` expected COMPOSE_WORKSPACE_ID="${expectedWsId}"`)
|
|
1709
|
-
if (!nodeMatch) console.log(` expected COMPOSE_NODE="${composeNode}"`)
|
|
1710
|
-
if (!binMatch) console.log(` expected COMPOSE_BIN="${composeBin}"`)
|
|
1711
|
-
}
|
|
1727
|
+
try {
|
|
1728
|
+
return resolveWorkspace(wsHint ? { cwd: projectRoot, workspaceId: wsHint } : { cwd: projectRoot }).id
|
|
1729
|
+
} catch { return null /* ignore for status */ }
|
|
1712
1730
|
}
|
|
1713
1731
|
|
|
1714
1732
|
if (sub === 'install') {
|
|
@@ -1738,7 +1756,17 @@ if (cmd === 'hooks') {
|
|
|
1738
1756
|
if (!sub || sub === 'status') {
|
|
1739
1757
|
// Status reports on ALL known hook types (selection flags ignored), so users
|
|
1740
1758
|
// see the full picture. Selection only affects install/uninstall.
|
|
1741
|
-
|
|
1759
|
+
const computed = computeHooksStatus({
|
|
1760
|
+
projectRoot,
|
|
1761
|
+
expectedWsId: resolveExpectedWsId(),
|
|
1762
|
+
composeNode,
|
|
1763
|
+
composeBin,
|
|
1764
|
+
})
|
|
1765
|
+
for (const t of Object.keys(HOOK_TYPES)) {
|
|
1766
|
+
for (const line of formatHookStatusLines(t, computed[t], { composeNode, composeBin })) {
|
|
1767
|
+
console.log(line)
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1742
1770
|
process.exit(0)
|
|
1743
1771
|
}
|
|
1744
1772
|
|
|
@@ -1971,6 +1999,14 @@ if (cmd === 'build') {
|
|
|
1971
1999
|
const all = filteredArgs2.includes('--all')
|
|
1972
2000
|
const dryRun = filteredArgs2.includes('--dry-run')
|
|
1973
2001
|
const skipTriage = filteredArgs2.includes('--skip-triage')
|
|
2002
|
+
// COMP-BUILD-QUICK: --quick selects the trimmed build-quick pipeline
|
|
2003
|
+
// (design → implement → ship). Symmetric to fix mode's Quick path; the real
|
|
2004
|
+
// mirror is the existing --template mechanism (fix dispatches template:'bug-fix'),
|
|
2005
|
+
// so --quick is sugar for template:'build-quick'. Single-feature only.
|
|
2006
|
+
const quick = filteredArgs2.includes('--quick')
|
|
2007
|
+
// COMP-CODEX-IMPL: --codex flips the implementer to Codex (Claude reviews). v1 is
|
|
2008
|
+
// full-build, single-feature only — mutually exclusive with --quick and batch.
|
|
2009
|
+
const codex = filteredArgs2.includes('--codex')
|
|
1974
2010
|
|
|
1975
2011
|
// Multiple codes: compose build FEAT-1 FEAT-2 FEAT-3
|
|
1976
2012
|
const isMulti = featureCodes.length > 1
|
|
@@ -1983,12 +2019,38 @@ if (cmd === 'build') {
|
|
|
1983
2019
|
process.exit(1)
|
|
1984
2020
|
}
|
|
1985
2021
|
|
|
2022
|
+
// COMP-BUILD-QUICK: --quick is a single-feature shortcut for a specific
|
|
2023
|
+
// template, so it conflicts with an explicit --template and with batch builds.
|
|
2024
|
+
if (quick && templateName) {
|
|
2025
|
+
console.error('Error: --quick and --template are mutually exclusive (--quick selects the build-quick template)')
|
|
2026
|
+
process.exit(1)
|
|
2027
|
+
}
|
|
2028
|
+
if (quick && isBatch) {
|
|
2029
|
+
console.error('Error: --quick cannot be combined with --all/prefix/multi (single feature only)')
|
|
2030
|
+
process.exit(1)
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// COMP-CODEX-IMPL: v1 scopes --codex to the full build, single feature.
|
|
2034
|
+
if (codex && quick) {
|
|
2035
|
+
console.error('Error: --codex and --quick are mutually exclusive in v1 (build-quick Codex parity is a follow-up)')
|
|
2036
|
+
process.exit(1)
|
|
2037
|
+
}
|
|
2038
|
+
if (codex && templateName) {
|
|
2039
|
+
console.error('Error: --codex and --template are mutually exclusive (--codex parameterizes the build template)')
|
|
2040
|
+
process.exit(1)
|
|
2041
|
+
}
|
|
2042
|
+
if (codex && isBatch) {
|
|
2043
|
+
console.error('Error: --codex cannot be combined with --all/prefix/multi (single feature only in v1)')
|
|
2044
|
+
process.exit(1)
|
|
2045
|
+
}
|
|
2046
|
+
|
|
1986
2047
|
if (!featureCode && !abort && !all) {
|
|
1987
2048
|
console.error('Usage: compose build <feature-code> [feature-code...]')
|
|
1988
2049
|
console.error(' compose build STRAT-COMP (prefix — builds all matching)')
|
|
1989
2050
|
console.error(' compose build --all (builds entire roadmap)')
|
|
1990
2051
|
console.error('')
|
|
1991
2052
|
console.error('Options:')
|
|
2053
|
+
console.error(' --quick Trimmed lifecycle (design → implement → ship) for small additive work')
|
|
1992
2054
|
console.error(' --abort Abort the active build')
|
|
1993
2055
|
console.error(' --all Build all PLANNED features in dependency order')
|
|
1994
2056
|
console.error(' --dry-run Print build order without executing')
|
|
@@ -1996,11 +2058,23 @@ if (cmd === 'build') {
|
|
|
1996
2058
|
process.exit(1)
|
|
1997
2059
|
}
|
|
1998
2060
|
|
|
1999
|
-
// Auto-init if needed
|
|
2061
|
+
// Auto-init if needed. COMP-BUILD-QUICK: a workspace initialized before the
|
|
2062
|
+
// build-quick pipeline existed won't have pipelines/build-quick.stratum.yaml,
|
|
2063
|
+
// and there's no bundled preset fallback — so when --quick is requested and
|
|
2064
|
+
// that file is absent, treat it as init-needed (runInit re-seeds it) rather
|
|
2065
|
+
// than letting runBuild fail later with "Lifecycle spec not found".
|
|
2000
2066
|
const { root: buildCwd } = resolveCwdWithWorkspace(args)
|
|
2001
|
-
|
|
2067
|
+
const needsInit =
|
|
2068
|
+
!existsSync(join(buildCwd, '.compose', 'compose.json')) ||
|
|
2069
|
+
!existsSync(join(buildCwd, 'pipelines', 'build.stratum.yaml')) ||
|
|
2070
|
+
(quick && !existsSync(join(buildCwd, 'pipelines', 'build-quick.stratum.yaml')))
|
|
2071
|
+
if (needsInit) {
|
|
2002
2072
|
console.log('Running compose init...\n')
|
|
2003
|
-
|
|
2073
|
+
// Thread the RESOLVED workspace root into runInit. Without it runInit seeds
|
|
2074
|
+
// process.cwd(), which differs from buildCwd when invoked from a subdirectory
|
|
2075
|
+
// — the guard checks buildCwd but the seed would land in the wrong dir, so
|
|
2076
|
+
// runBuild would still fail with the same missing-spec error (Codex review).
|
|
2077
|
+
await runInit(args.filter(a => a.startsWith('--')), buildCwd)
|
|
2004
2078
|
console.log('')
|
|
2005
2079
|
}
|
|
2006
2080
|
|
|
@@ -2031,6 +2105,8 @@ if (cmd === 'build') {
|
|
|
2031
2105
|
if (agentWorkDir) singleOpts.workingDirectory = agentWorkDir
|
|
2032
2106
|
if (skipTriage) singleOpts.skipTriage = true
|
|
2033
2107
|
if (templateName) singleOpts.template = templateName
|
|
2108
|
+
if (quick) singleOpts.template = 'build-quick' // COMP-BUILD-QUICK
|
|
2109
|
+
if (codex) singleOpts.codex = true // COMP-CODEX-IMPL
|
|
2034
2110
|
runBuild(featureCode, singleOpts).then(() => {
|
|
2035
2111
|
process.exit(0)
|
|
2036
2112
|
}).catch((err) => {
|
|
@@ -2426,7 +2502,11 @@ if (cmd === 'build') {
|
|
|
2426
2502
|
} = await import('../lib/ideabox.js')
|
|
2427
2503
|
|
|
2428
2504
|
const ibRelPath = getIdeaboxRelPath(ibCwd)
|
|
2429
|
-
|
|
2505
|
+
// ibFullPath is used for direct file ops (existsSync/mkdirSync/writeFileSync/
|
|
2506
|
+
// display); resolve it absolute-safe. ibRelPath stays relative because the
|
|
2507
|
+
// lib readIdeabox/writeIdeabox readers re-join it under ibCwd.
|
|
2508
|
+
const { resolveIdeaboxPath: _resolveIdeaboxPathCli } = await import('../lib/project-paths.js')
|
|
2509
|
+
const ibFullPath = _resolveIdeaboxPathCli(ibCwd)
|
|
2430
2510
|
|
|
2431
2511
|
if (!ibSubcmd || ibSubcmd === '--help' || ibSubcmd === '-h') {
|
|
2432
2512
|
console.log('Usage: compose ideabox <subcommand>')
|
|
@@ -2544,9 +2624,10 @@ if (cmd === 'build') {
|
|
|
2544
2624
|
resolvedCode = `IDEA-${idea.num}-${slug}`.toUpperCase()
|
|
2545
2625
|
}
|
|
2546
2626
|
|
|
2547
|
-
// Create feature folder if missing — respect paths.features
|
|
2548
|
-
|
|
2549
|
-
const
|
|
2627
|
+
// Create feature folder if missing — respect paths.features (may be
|
|
2628
|
+
// relocated outside ibCwd — COMP-PATHS-EXTERNAL; resolve absolute, no re-root)
|
|
2629
|
+
const featuresBase = resolveFeaturesPathFromConfig(ibCwd, ibConfig)
|
|
2630
|
+
const featuresDir = join(featuresBase, resolvedCode)
|
|
2550
2631
|
if (!existsSync(featuresDir)) {
|
|
2551
2632
|
// COMP-MCP-VALIDATE-1: route through the validated writer (schema-guarded)
|
|
2552
2633
|
// instead of a raw writeFileSync.
|
|
@@ -2557,8 +2638,8 @@ if (cmd === 'build') {
|
|
|
2557
2638
|
status: 'PLANNED',
|
|
2558
2639
|
promotedFrom: ideaId,
|
|
2559
2640
|
createdAt: new Date().toISOString(),
|
|
2560
|
-
},
|
|
2561
|
-
console.log(`Created feature folder: ${
|
|
2641
|
+
}, featuresBase)
|
|
2642
|
+
console.log(`Created feature folder: ${join(featuresBase, resolvedCode)}/`)
|
|
2562
2643
|
}
|
|
2563
2644
|
|
|
2564
2645
|
_promoteIdea(parsed, ideaId, resolvedCode)
|
|
@@ -2750,11 +2831,76 @@ if (cmd === 'build') {
|
|
|
2750
2831
|
process.exit(1)
|
|
2751
2832
|
})
|
|
2752
2833
|
|
|
2753
|
-
} else if (cmd === '
|
|
2834
|
+
} else if (cmd === 'context') {
|
|
2835
|
+
// ---------------------------------------------------------------------------
|
|
2836
|
+
// compose context decisions [--feature <FC>] [--format text|json]
|
|
2837
|
+
// COMP-CTX-3: read-side of the decision log. During `compose build` each gate
|
|
2838
|
+
// outcome is auto-appended to docs/context/decisions.md (appendDecisionEntry);
|
|
2839
|
+
// this surfaces it. Read-only; no server.
|
|
2840
|
+
// ---------------------------------------------------------------------------
|
|
2841
|
+
const contextSubcmd = args[0]
|
|
2842
|
+
if (contextSubcmd === 'decisions') {
|
|
2843
|
+
const { existsSync, readFileSync } = await import('node:fs')
|
|
2844
|
+
const { join } = await import('node:path')
|
|
2845
|
+
const { resolveContextPath } = await import('../lib/project-paths.js')
|
|
2846
|
+
const flagVal = (flag) => {
|
|
2847
|
+
const i = args.indexOf(flag)
|
|
2848
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : null
|
|
2849
|
+
}
|
|
2850
|
+
const featureFilter = flagVal('--feature')
|
|
2851
|
+
const format = flagVal('--format') || 'text'
|
|
2852
|
+
|
|
2853
|
+
const decisionsPath = join(resolveContextPath(process.cwd()), 'decisions.md')
|
|
2854
|
+
if (!existsSync(decisionsPath)) {
|
|
2855
|
+
console.error(`No decision log at ${decisionsPath} — run \`compose init\` to scaffold docs/context/, then decisions accrue during builds.`)
|
|
2856
|
+
process.exit(1)
|
|
2857
|
+
}
|
|
2858
|
+
const raw = readFileSync(decisionsPath, 'utf-8')
|
|
2859
|
+
|
|
2860
|
+
// Each entry is a "## [YYYY-MM-DD] FEATURE — step" heading + body
|
|
2861
|
+
// (see appendDecisionEntry in lib/build.js). Slice raw on the headings.
|
|
2862
|
+
const heads = []
|
|
2863
|
+
const re = /^## \[(\d{4}-\d{2}-\d{2})\] (\S+) — (.+)$/gm
|
|
2864
|
+
let m
|
|
2865
|
+
while ((m = re.exec(raw)) !== null) {
|
|
2866
|
+
heads.push({ idx: m.index, date: m[1], feature: m[2], step: m[3] })
|
|
2867
|
+
}
|
|
2868
|
+
const blocks = heads.map((h, i) => ({
|
|
2869
|
+
...h,
|
|
2870
|
+
text: raw.slice(h.idx, i + 1 < heads.length ? heads[i + 1].idx : raw.length).trim(),
|
|
2871
|
+
}))
|
|
2872
|
+
const filtered = featureFilter ? blocks.filter(b => b.feature === featureFilter) : blocks
|
|
2873
|
+
|
|
2874
|
+
if (format === 'json') {
|
|
2875
|
+
const out = filtered.map(b => ({
|
|
2876
|
+
date: b.date,
|
|
2877
|
+
feature: b.feature,
|
|
2878
|
+
step: b.step,
|
|
2879
|
+
outcome: (b.text.match(/\*\*Outcome:\*\* (.+)/) || [])[1] || null,
|
|
2880
|
+
rationale: (b.text.match(/\*\*Rationale:\*\* ([\s\S]+?)(?:\n## |\s*$)/) || [])[1]?.trim() || null,
|
|
2881
|
+
}))
|
|
2882
|
+
console.log(JSON.stringify(out, null, 2))
|
|
2883
|
+
} else if (filtered.length === 0) {
|
|
2884
|
+
console.log(featureFilter ? `No decisions recorded for ${featureFilter}.` : 'No decisions recorded yet.')
|
|
2885
|
+
} else {
|
|
2886
|
+
const label = featureFilter ? ` for ${featureFilter}` : ''
|
|
2887
|
+
console.log(`Decision log${label} — ${filtered.length} entr${filtered.length === 1 ? 'y' : 'ies'}:\n`)
|
|
2888
|
+
for (const b of filtered) console.log(b.text + '\n')
|
|
2889
|
+
}
|
|
2890
|
+
} else {
|
|
2891
|
+
console.error('usage: compose context decisions [--feature <FC>] [--format text|json]')
|
|
2892
|
+
process.exit(1)
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
} else if (cmd === 'gates' || cmd === 'gate') {
|
|
2754
2896
|
// ---------------------------------------------------------------------------
|
|
2897
|
+
// compose gate list [--item <id>] [--status pending|all|resolved] [--format text|json]
|
|
2898
|
+
// compose gate resolve <gateId> (--approve|--revise|--kill) [--comment <text>] [--reason <text>]
|
|
2899
|
+
// COMP-PARITY-1: CLI gate resolution — wraps GET/POST /api/vision/gates[/:id/resolve]
|
|
2755
2900
|
// compose gates report [--since 24h|7d|1h|<ISO>] [--feature <FC>]
|
|
2756
2901
|
// [--format text|json] [--rubber-stamp-ms <N>]
|
|
2757
|
-
//
|
|
2902
|
+
// COMP-OBS-GATELOG: audit gate log report (Decision 5)
|
|
2903
|
+
// (`gate` and `gates` are aliases.)
|
|
2758
2904
|
// ---------------------------------------------------------------------------
|
|
2759
2905
|
const gatesSubcmd = args[0]
|
|
2760
2906
|
|
|
@@ -2828,8 +2974,132 @@ if (cmd === 'build') {
|
|
|
2828
2974
|
process.exit(0)
|
|
2829
2975
|
}
|
|
2830
2976
|
|
|
2831
|
-
|
|
2832
|
-
|
|
2977
|
+
if (gatesSubcmd === 'list' || gatesSubcmd === 'resolve') {
|
|
2978
|
+
// --- COMP-PARITY-1: CLI gate list/resolve over the :4001 vision endpoints ---
|
|
2979
|
+
const flagVal = (flag) => { const i = args.indexOf(flag); return i !== -1 && args[i + 1] ? args[i + 1] : null }
|
|
2980
|
+
const hasFlag = (flag) => args.includes(flag)
|
|
2981
|
+
const baseUrl = process.env.COMPOSE_URL || `http://127.0.0.1:${resolvePort()}`
|
|
2982
|
+
|
|
2983
|
+
// Tolerant workspace resolution (mirror `compose loops`): attach the header
|
|
2984
|
+
// when resolvable, otherwise send none (server soft-falls back to boot ws).
|
|
2985
|
+
let _wsId = null
|
|
2986
|
+
try {
|
|
2987
|
+
const wsId = getWorkspaceFlag(args)
|
|
2988
|
+
const ws = resolveWorkspace({ workspaceId: wsId === '__COMPOSE_WORKSPACE_ID__' ? null : wsId })
|
|
2989
|
+
_wsId = ws.id || null
|
|
2990
|
+
} catch { /* no header */ }
|
|
2991
|
+
|
|
2992
|
+
const reqJson = async (method, urlStr, body) => {
|
|
2993
|
+
const { default: http } = await import(urlStr.startsWith('https') ? 'https' : 'http')
|
|
2994
|
+
return new Promise((resolveP, reject) => {
|
|
2995
|
+
const u = new URL(urlStr)
|
|
2996
|
+
const data = body ? JSON.stringify(body) : null
|
|
2997
|
+
const headers = {}
|
|
2998
|
+
if (_wsId) headers['X-Compose-Workspace-Id'] = _wsId
|
|
2999
|
+
if (data) { headers['Content-Type'] = 'application/json'; headers['Content-Length'] = Buffer.byteLength(data) }
|
|
3000
|
+
// Sensitive-token header — only consumed when capabilities.guardAuth is on.
|
|
3001
|
+
if (method === 'POST' && process.env.COMPOSE_API_TOKEN) headers['x-compose-token'] = process.env.COMPOSE_API_TOKEN
|
|
3002
|
+
const r = http.request({
|
|
3003
|
+
hostname: u.hostname,
|
|
3004
|
+
port: u.port || (urlStr.startsWith('https') ? 443 : 80),
|
|
3005
|
+
path: u.pathname + u.search,
|
|
3006
|
+
method,
|
|
3007
|
+
headers,
|
|
3008
|
+
}, (res) => {
|
|
3009
|
+
let buf = ''
|
|
3010
|
+
res.on('data', c => { buf += c })
|
|
3011
|
+
res.on('end', () => { try { resolveP({ status: res.statusCode, body: JSON.parse(buf) }) } catch { resolveP({ status: res.statusCode, body: buf }) } })
|
|
3012
|
+
})
|
|
3013
|
+
r.on('error', reject)
|
|
3014
|
+
if (data) r.end(data); else r.end()
|
|
3015
|
+
})
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
const dieUnreachable = (err) => {
|
|
3019
|
+
if (err && (err.code === 'ECONNREFUSED' || /ECONNREFUSED/.test(err.message || ''))) {
|
|
3020
|
+
console.error(`compose server not reachable on :${resolvePort()} — start it with \`npm run dev:server\` (or \`npm run dev:watch\`)`)
|
|
3021
|
+
} else {
|
|
3022
|
+
console.error(`gate ${gatesSubcmd}: ${err.message}`)
|
|
3023
|
+
}
|
|
3024
|
+
process.exit(1)
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
if (gatesSubcmd === 'list') {
|
|
3028
|
+
const itemId = flagVal('--item')
|
|
3029
|
+
const status = flagVal('--status') || 'pending'
|
|
3030
|
+
const format = flagVal('--format') || 'text'
|
|
3031
|
+
if (!['pending', 'all', 'resolved'].includes(status)) {
|
|
3032
|
+
console.error(`gate list: --status must be one of pending|all|resolved (got '${status}')`)
|
|
3033
|
+
process.exit(1)
|
|
3034
|
+
}
|
|
3035
|
+
if (!['text', 'json'].includes(format)) {
|
|
3036
|
+
console.error(`gate list: --format must be one of text|json (got '${format}')`)
|
|
3037
|
+
process.exit(1)
|
|
3038
|
+
}
|
|
3039
|
+
const params = new URLSearchParams()
|
|
3040
|
+
if (status !== 'pending') params.set('status', status)
|
|
3041
|
+
if (itemId) params.set('itemId', itemId)
|
|
3042
|
+
const qs = params.toString() ? `?${params.toString()}` : ''
|
|
3043
|
+
let resp
|
|
3044
|
+
try { resp = await reqJson('GET', `${baseUrl}/api/vision/gates${qs}`) } catch (e) { dieUnreachable(e) }
|
|
3045
|
+
if (resp.status !== 200) {
|
|
3046
|
+
console.error(`gate list failed (HTTP ${resp.status}): ${resp.body?.error || JSON.stringify(resp.body)}`)
|
|
3047
|
+
process.exit(1)
|
|
3048
|
+
}
|
|
3049
|
+
// The server only honors itemId on the pending path; filter client-side so
|
|
3050
|
+
// --item works uniformly across --status all|resolved too (Codex review).
|
|
3051
|
+
let gates = resp.body.gates || []
|
|
3052
|
+
if (itemId) gates = gates.filter(g => g.itemId === itemId)
|
|
3053
|
+
if (format === 'json') { console.log(JSON.stringify(gates, null, 2)); process.exit(0) }
|
|
3054
|
+
if (gates.length === 0) { console.log(`No ${status} gates.`); process.exit(0) }
|
|
3055
|
+
const ageOf = (iso) => {
|
|
3056
|
+
const m = Math.floor((Date.now() - new Date(iso).getTime()) / 60000)
|
|
3057
|
+
if (m < 60) return `${m}m`
|
|
3058
|
+
const h = Math.floor(m / 60)
|
|
3059
|
+
return h < 24 ? `${h}h` : `${Math.floor(h / 24)}d`
|
|
3060
|
+
}
|
|
3061
|
+
// Record-per-gate, not a fixed-width table: real gate ids are long
|
|
3062
|
+
// `<uuid>:<step>:<round>` strings, so columns smush together. Keep the full
|
|
3063
|
+
// copy-pasteable id on its own line (it's the arg to `gate resolve`).
|
|
3064
|
+
console.log(`${gates.length} ${status} gate${gates.length === 1 ? '' : 's'}:\n`)
|
|
3065
|
+
for (const g of gates) {
|
|
3066
|
+
console.log(`${g.id} [${g.status || 'pending'}]`)
|
|
3067
|
+
console.log(` item ${g.itemId || '·'} step ${g.stepId || '·'} ${g.fromPhase || '·'}→${g.toPhase || '·'} ${ageOf(g.createdAt)}\n`)
|
|
3068
|
+
}
|
|
3069
|
+
process.exit(0)
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
if (gatesSubcmd === 'resolve') {
|
|
3073
|
+
const gateId = args[1] && !args[1].startsWith('-') ? args[1] : null
|
|
3074
|
+
if (!gateId) {
|
|
3075
|
+
console.error('usage: compose gate resolve <gateId> (--approve|--revise|--kill) [--comment <text>] [--reason <text>]')
|
|
3076
|
+
process.exit(1)
|
|
3077
|
+
}
|
|
3078
|
+
const outcomes = ['approve', 'revise', 'kill'].filter(o => hasFlag(`--${o}`))
|
|
3079
|
+
if (outcomes.length !== 1) {
|
|
3080
|
+
console.error('gate resolve: exactly one of --approve | --revise | --kill is required')
|
|
3081
|
+
process.exit(1)
|
|
3082
|
+
}
|
|
3083
|
+
const outcome = outcomes[0]
|
|
3084
|
+
const comment = flagVal('--comment') ?? flagVal('--reason') ?? undefined
|
|
3085
|
+
let resp
|
|
3086
|
+
try {
|
|
3087
|
+
resp = await reqJson('POST', `${baseUrl}/api/vision/gates/${encodeURIComponent(gateId)}/resolve`, { outcome, comment, resolvedBy: 'cli' })
|
|
3088
|
+
} catch (e) { dieUnreachable(e) }
|
|
3089
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
3090
|
+
console.error(`gate resolve failed (HTTP ${resp.status}): ${resp.body?.error || JSON.stringify(resp.body)}`)
|
|
3091
|
+
process.exit(1)
|
|
3092
|
+
}
|
|
3093
|
+
console.log(`Gate ${gateId} resolved: ${outcome}${comment ? ` — ${comment}` : ''}`)
|
|
3094
|
+
process.exit(0)
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
console.error(`Unknown gate subcommand: ${gatesSubcmd}`)
|
|
3099
|
+
console.error('Usage:')
|
|
3100
|
+
console.error(' compose gate list [--item <id>] [--status pending|all|resolved] [--format text|json]')
|
|
3101
|
+
console.error(' compose gate resolve <gateId> (--approve|--revise|--kill) [--comment <text>] [--reason <text>]')
|
|
3102
|
+
console.error(' compose gates report [--since 24h] [--feature FC] [--format text|json] [--rubber-stamp-ms N]')
|
|
2833
3103
|
process.exit(1)
|
|
2834
3104
|
|
|
2835
3105
|
} else if (cmd === 'loops') {
|