@smartmemory/compose 0.2.50-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.
Files changed (118) hide show
  1. package/.claude/skills/compose/SKILL.md +24 -1
  2. package/.claude/skills/compose/references/hermes-tools.md +80 -0
  3. package/README.md +1 -1
  4. package/bin/compose.js +310 -52
  5. package/dist/assets/App-CT0vXPgd.js +724 -0
  6. package/dist/assets/_baseUniq-CsOHc_iS.js +1 -0
  7. package/dist/assets/{arc-DeHak63Z.js → arc-BXht3LyE.js} +1 -1
  8. package/dist/assets/architectureDiagram-Q4EWVU46-BV1r86-w.js +36 -0
  9. package/dist/assets/blockDiagram-DXYQGD6D-BgcMJjAR.js +132 -0
  10. package/dist/assets/{browser-DW2JlJCC.js → browser-CnKiSnlr.js} +6 -6
  11. package/dist/assets/{c4Diagram-AAUBKEIU-BsFJmPuS.js → c4Diagram-AHTNJAMY-nBIRLJTv.js} +1 -1
  12. package/dist/assets/channel-Dd4XaiYv.js +1 -0
  13. package/dist/assets/{chunk-4BX2VUAB-XO4S2_89.js → chunk-4BX2VUAB-CNmGyhrp.js} +1 -1
  14. package/dist/assets/chunk-4TB4RGXK-MLT7I7w4.js +206 -0
  15. package/dist/assets/{chunk-55IACEB6-CtFSLInj.js → chunk-55IACEB6-D8KuKVLa.js} +1 -1
  16. package/dist/assets/{chunk-2J33WTMH-AEu6HIoY.js → chunk-EDXVE4YY-TPlt1bS2.js} +1 -1
  17. package/dist/assets/{chunk-FMBD7UC4-BMGd_Y9a.js → chunk-FMBD7UC4-tYY8xcjr.js} +1 -1
  18. package/dist/assets/chunk-OYMX7WX6-yBPwT1AT.js +231 -0
  19. package/dist/assets/{chunk-QZHKN3VN-DHhUJ8RK.js → chunk-QZHKN3VN-B4Wfybww.js} +1 -1
  20. package/dist/assets/{chunk-ND2GUHAM-BlrOECEr.js → chunk-YZCP3GAM-DE6_5aqh.js} +1 -1
  21. package/dist/assets/classDiagram-6PBFFD2Q-DshSQDF9.js +1 -0
  22. package/dist/assets/classDiagram-v2-HSJHXN6E-DshSQDF9.js +1 -0
  23. package/dist/assets/clone-CBsbNGAa.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-C3M0mZqk.js → cose-bilkent-S5V4N54A-DzzRyJtB.js} +1 -1
  25. package/dist/assets/dagre-KV5264BT-CTcTVGdU.js +4 -0
  26. package/dist/assets/diagram-5BDNPKRD-qR6mc8kJ.js +10 -0
  27. package/dist/assets/diagram-G4DWMVQ6-CUcGXyNb.js +24 -0
  28. package/dist/assets/diagram-MMDJMWI5-B68cPonH.js +43 -0
  29. package/dist/assets/diagram-TYMM5635-q8Id8eFG.js +24 -0
  30. package/dist/assets/{erDiagram-TEJ5UH35-AMDitV0b.js → erDiagram-SMLLAGMA-CtxJi57K.js} +1 -1
  31. package/dist/assets/{flowDiagram-I6XJVG4X-C0t2ZGQv.js → flowDiagram-DWJPFMVM-BzzO4JYU.js} +1 -1
  32. package/dist/assets/ganttDiagram-T4ZO3ILL-Dn0wIyhR.js +292 -0
  33. package/dist/assets/gitGraphDiagram-UUTBAWPF-tnNFXfMM.js +106 -0
  34. package/dist/assets/graph-DZe55uk8.js +331 -0
  35. package/dist/assets/graph-Tq_bs_r0.js +1 -0
  36. package/dist/assets/index-8UhRLbGq.js +123 -0
  37. package/dist/assets/index-CRqB9els.css +1 -0
  38. package/dist/assets/infoDiagram-42DDH7IO-BXLvLeS0.js +2 -0
  39. package/dist/assets/{ishikawaDiagram-YF4QCWOH-CCBwgjFs.js → ishikawaDiagram-UXIWVN3A-DWx-7BBy.js} +5 -5
  40. package/dist/assets/{journeyDiagram-JHISSGLW-zUJN38EU.js → journeyDiagram-VCZTEJTY-C42hXNha.js} +1 -1
  41. package/dist/assets/{kanban-definition-UN3LZRKU-BNxWi-8J.js → kanban-definition-6JOO6SKY-CEq930ew.js} +1 -1
  42. package/dist/assets/katex-DkKDou_j.js +257 -0
  43. package/dist/assets/layout-qtUgN9BC.js +1 -0
  44. package/dist/assets/{linear-CRH9b4g7.js → linear-D158F7OT.js} +1 -1
  45. package/dist/assets/min-hESxN-c0.js +1 -0
  46. package/dist/assets/{mindmap-definition-RKZ34NQL-DolYwq7Y.js → mindmap-definition-QFDTVHPH-d9SSh2nG.js} +7 -7
  47. package/dist/assets/mobile-1gVCT0OK.css +1 -0
  48. package/dist/assets/mobile-Chw8RWyH.js +17 -0
  49. package/dist/assets/pieDiagram-DEJITSTG-gglmeMav.js +30 -0
  50. package/dist/assets/quadrantDiagram-34T5L4WZ-CqFC_htU.js +7 -0
  51. package/dist/assets/{requirementDiagram-4Y6WPE33-DsXZ05jo.js → requirementDiagram-MS252O5E-B1ZFIotK.js} +1 -1
  52. package/dist/assets/sankeyDiagram-XADWPNL6-BvItvZt5.js +10 -0
  53. package/dist/assets/sequenceDiagram-FGHM5R23-CAIn5Z3U.js +157 -0
  54. package/dist/assets/stateDiagram-FHFEXIEX-CyJrGzib.js +1 -0
  55. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DprhF_Ho.js +1 -0
  56. package/dist/assets/{timeline-definition-PNZ67QCA-Bvnhw58e.js → timeline-definition-GMOUNBTQ-k_0vJQAK.js} +1 -1
  57. package/dist/assets/{vennDiagram-CIIHVFJN-5WnyjZqf.js → vennDiagram-DHZGUBPP-TZAW60b0.js} +4 -4
  58. package/dist/assets/wardley-RL74JXVD-DzQEoG6X.js +162 -0
  59. package/dist/assets/wardleyDiagram-NUSXRM2D-BAoVVIjZ.js +20 -0
  60. package/dist/assets/{xychartDiagram-2RQKCTM6-BCShes6B.js → xychartDiagram-5P7HB3ND-CynkZcUb.js} +1 -1
  61. package/dist/index.html +4 -4
  62. package/lib/build.js +272 -33
  63. package/lib/codex-preflight.js +151 -0
  64. package/lib/completion-writer.js +21 -1
  65. package/lib/feature-validator.js +5 -1
  66. package/lib/hooks-status.js +135 -0
  67. package/lib/install-agent-defs.js +22 -0
  68. package/lib/migrate-anon.js +277 -0
  69. package/lib/roadmap-parser.js +1 -1
  70. package/lib/test-bootstrap.js +165 -2
  71. package/lib/vocabulary-inject.js +103 -0
  72. package/package.json +2 -1
  73. package/pipelines/build-quick.stratum.yaml +433 -0
  74. package/pipelines/build.stratum.yaml +60 -3
  75. package/scripts/watch-server.sh +32 -0
  76. package/server/build-routes.js +89 -9
  77. package/server/build-stream-bridge.js +12 -0
  78. package/server/compose-mcp-tools.js +73 -1
  79. package/server/compose-mcp.js +27 -0
  80. package/server/feature-scaffold-routes.js +82 -0
  81. package/server/health-routes.js +261 -0
  82. package/server/qa-scope-routes.js +96 -0
  83. package/server/validate-routes.js +70 -0
  84. package/server/vision-routes.js +29 -0
  85. package/server/vision-server.js +21 -0
  86. package/dist/assets/App-Bxyif1yv.js +0 -706
  87. package/dist/assets/architectureDiagram-3BPJPVTR-C5Nkyl9y.js +0 -36
  88. package/dist/assets/blockDiagram-GPEHLZMM-BilnUBG2.js +0 -132
  89. package/dist/assets/channel-ZgbQ1k0u.js +0 -1
  90. package/dist/assets/chunk-727SXJPM-CZeBRpM9.js +0 -206
  91. package/dist/assets/chunk-AQP2D5EJ-_vmxcBc4.js +0 -231
  92. package/dist/assets/classDiagram-4FO5ZUOK-KXxOorUx.js +0 -1
  93. package/dist/assets/classDiagram-v2-Q7XG4LA2-KXxOorUx.js +0 -1
  94. package/dist/assets/dagre-BM42HDAG-DJlm4fEk.js +0 -4
  95. package/dist/assets/diagram-2AECGRRQ-BmcONQG5.js +0 -43
  96. package/dist/assets/diagram-5GNKFQAL-5JbWGIwd.js +0 -10
  97. package/dist/assets/diagram-KO2AKTUF-DAnIvyI0.js +0 -3
  98. package/dist/assets/diagram-LMA3HP47-Bmh7tvmI.js +0 -24
  99. package/dist/assets/diagram-OG6HWLK6-hnHVMNnk.js +0 -24
  100. package/dist/assets/ganttDiagram-6RSMTGT7-BPlM-BEN.js +0 -292
  101. package/dist/assets/gitGraphDiagram-PVQCEYII-Lv_YTxnb.js +0 -106
  102. package/dist/assets/graph-BBXaecIU.js +0 -331
  103. package/dist/assets/graph-CAnANduQ.js +0 -1
  104. package/dist/assets/index-BwLfbbOu.css +0 -1
  105. package/dist/assets/index-COq21Zym.js +0 -119
  106. package/dist/assets/infoDiagram-5YYISTIA-Dul1vdUm.js +0 -2
  107. package/dist/assets/katex-C5jXJg4s.js +0 -257
  108. package/dist/assets/layout-DGIYPm2g.js +0 -1
  109. package/dist/assets/mobile-Cag5dHlF.css +0 -1
  110. package/dist/assets/mobile-DwmxS_O4.js +0 -17
  111. package/dist/assets/pieDiagram-4H26LBE5-oxNfr2TX.js +0 -30
  112. package/dist/assets/quadrantDiagram-W4KKPZXB-BmZCvD-z.js +0 -7
  113. package/dist/assets/sankeyDiagram-5OEKKPKP-DsnYaawP.js +0 -40
  114. package/dist/assets/sequenceDiagram-3UESZ5HK-Dt5mu3g8.js +0 -162
  115. package/dist/assets/stateDiagram-AJRCARHV-BFR5ZINQ.js +0 -1
  116. package/dist/assets/stateDiagram-v2-BHNVJYJU-BDrQD8fR.js +0 -1
  117. package/dist/assets/wardley-L42UT6IY-CTxW4xow.js +0 -173
  118. package/dist/assets/wardleyDiagram-YWT4CUSO-K8Y1EOvb.js +0 -78
@@ -16,6 +16,14 @@ Both modes share Phase 6 (plan), Phase 7 (TDD execute + E2E + review loop + cove
16
16
 
17
17
  Before any phase begins, read project state via the `compose` MCP server (get_vision_items, get_phase_summary, get_blocked_items). Every feature OR bug maps to a vision item — check it exists, create it if not.
18
18
 
19
+ ## Platform Adaptation
20
+
21
+ This SKILL.md uses **Claude Code** tool names (Skill, Agent/Task, EnterPlanMode, TodoWrite). On other harnesses, resolve each action to that harness's equivalent:
22
+
23
+ - **Nous Hermes Agent** → `references/hermes-tools.md` (skills, `delegate_task` for subagents, MCP servers, AGENTS.md; Phase-6 plan mode degrades to a flat gate).
24
+
25
+ The headless `compose build`/`compose fix` CLI is harness-agnostic — it runs from any shell and delegates agent execution to Stratum (Claude/Codex) regardless of the launching harness. The mapping files only matter for running the *interactive* lifecycle under a non-Claude-Code harness.
26
+
19
27
  ## Mode Selection
20
28
 
21
29
  `/compose` with no verb defaults to `build` for back-compat. Explicit verbs:
@@ -23,10 +31,25 @@ Before any phase begins, read project state via the `compose` MCP server (get_vi
23
31
  | Verb | Use | Folder |
24
32
  |---|---|---|
25
33
  | `/compose build <feature-ref>` | New feature, 3+ files, 2+ phases, needs design | `docs/features/<feature-code>/` |
34
+ | `/compose build --quick <feature-ref>` | **Small-but-real** additive work — one flag + a test + changelog entry; well-scoped, single component | `docs/features/<feature-code>/` |
26
35
  | `/compose fix <bug-ref>` | Non-trivial bug, test failure, regression, hotfix | `docs/bugs/<bug-code>/` |
27
36
 
28
37
  **Skip Compose entirely for:** typos, one-line obvious fixes, cosmetic changes — just fix those directly.
29
38
 
39
+ ### Build Quick path: `/compose build --quick` (COMP-BUILD-QUICK)
40
+
41
+ `--quick` collapses the build lifecycle to **design → implement → ship** with a *single design gate*. The phases `prd`, `architecture`, `blueprint`, `blueprint-verification`, `plan`, and `report` are **omitted** (not just self-skipping). Use it for small additive work that would otherwise pay full-lifecycle ceremony: a new flag plus its test, a small helper with a golden test, a one-component addition.
42
+
43
+ This is the symmetric counterpart to fix mode's Quick path. Note the implementation asymmetry: fix's Quick path is a **triage decision** the agent makes at Phase F1, whereas build's `--quick` is a **CLI flag** that selects the trimmed `build-quick` pipeline (headless CLI: `compose build --quick <code>` → `template: 'build-quick'`, the same `--template` mechanism fix uses with `bug-fix`).
44
+
45
+ **What `--quick` keeps (NOT OpenSpec's no-gates model):**
46
+ - The design gate and ship gate.
47
+ - All Phase-7 enforcement: TDD per task, verification-before-completion, the parallel + Codex review loop, the coverage sweep, and the generated-test review. Only phase *count* shrinks — enforcement is the differentiator and stays.
48
+
49
+ **Escalation guardrail.** If, during design or decomposition, the work proves multi-file, cross-cutting, or in need of architecture/PRD-level design, **stop and recommend the user re-run with full `/compose build` (no `--quick`)** rather than silently under-scoping. Note the recommendation in the design doc.
50
+
51
+ `--quick` is single-feature only — it is mutually exclusive with `--template` (it already selects one) and with batch builds (`--all`, prefix, or multiple codes).
52
+
30
53
  ## Partial Execution: `--through <phase>`
31
54
 
32
55
  By default, `/compose` runs the full lifecycle. Use `--through` to stop after a specific phase's gate is approved:
@@ -580,7 +603,7 @@ After completing a feature OR bug-fix lifecycle, update project memory:
580
603
 
581
604
  ## Dependencies
582
605
 
583
- `compose setup` (see `bin/compose.js` `syncSkills()`) installs compose-owned skills from `PACKAGE_ROOT/.claude/skills/` and `PACKAGE_ROOT/skills/` to `~/.claude/skills/` and tracks them in `~/.claude/skills/.compose-skills.json`.
606
+ `compose setup` (see `bin/compose.js` `syncSkills()`) installs compose-owned skills from `PACKAGE_ROOT/.claude/skills/` and `PACKAGE_ROOT/skills/` to `~/.claude/skills/` and tracks them in `~/.claude/skills/.compose-skills.json`. It also installs the vendored agent definitions from `PACKAGE_ROOT/.claude/agents/` (`compose-explorer`, `compose-architect`) to `~/.claude/agents/` (COMP-AGENT-VENDOR-1).
584
607
 
585
608
  ### Vendored (ship with compose)
586
609
 
@@ -0,0 +1,80 @@
1
+ # Compose on Hermes Agent — Tool Mapping
2
+
3
+ SKILL.md is written against **Claude Code** tool names. This file maps each
4
+ primitive the compose lifecycle uses to its **Nous Hermes Agent** equivalent, so
5
+ the lifecycle can run under Hermes. Scope: the *interactive* `/compose` lifecycle
6
+ (the headless `compose` CLI is already harness-agnostic — it runs from any shell
7
+ and delegates agent execution to Stratum).
8
+
9
+ Hermes shares the three primitives the lifecycle depends on — **skills**, an
10
+ **MCP client**, and **parallel subagent delegation** — so this is a name/packaging
11
+ adaptation, not an architecture port. The one genuine gap is plan mode (Phase 6);
12
+ see below.
13
+
14
+ ## Action mapping
15
+
16
+ | Action compose requests (Claude Code) | Hermes equivalent |
17
+ |---|---|
18
+ | Invoke a skill (`/compose`, `superpowers:*`) — **Skill** tool | Skills load natively from `~/.hermes/skills/`. Install with `hermes skills install <git-url\|file\|registry-id>` (SKILL.md frontmatter convention). |
19
+ | Dispatch a subagent (`compose-explorer`, `compose-architect`, `general-purpose`) — **Agent/Task** tool | `delegate_task` (runtime tool) — spawns an isolated child agent with its own conversation, terminal, and toolset; only the final summary returns. |
20
+ | Dispatch 2-3 subagents **in parallel** (Phase 1 explorers, Phase 3 architects) | `delegate_task` with a **`tasks` array** — runs in parallel via a thread pool, `max_concurrent_children` (default **3**, matches compose's fan-out). Orchestrator→leaf nesting needs `delegation.max_spawn_depth: 2`. |
21
+ | **Plan mode** (Phase 6) — `EnterPlanMode` / `ExitPlanMode` | **No direct equivalent.** Degrade to a flat gate: write `plan.md`, present it in chat, human approves before Phase 7. Compose already supports this fallback ("If Stratum unavailable, continue with flat prompt chain"). |
22
+ | Task tracking ("create a todo", "mark complete") — **TodoWrite** | `hermes kanban` (statuses todo/triage/scheduled/blocked/running/done; `hermes kanban create "<title>"`). Optional/advisory — skip if not wired. |
23
+ | Read a file — **Read** | Native file read, `@file` context injection, or shell (`cat`/`head`). |
24
+ | Create / edit a file — **Write / Edit** | Native file tools, or a filesystem MCP server (`hermes mcp add fs --command ...`). |
25
+ | Run a shell command — **Bash** | Hermes shell / per-subagent terminal session. |
26
+ | Search contents / find files — **Grep / Glob** | Shell (`rg`, `grep`, `find`). |
27
+ | compose tracker (`mcp__compose__*`) | `hermes mcp add compose --command "<compose-mcp-launch>"`, then call natively. |
28
+ | Stratum (`mcp__stratum__stratum_*`, incl. `stratum_agent_run type=codex`) | `hermes mcp add stratum --command "<stratum-mcp-launch>"`, then call natively. **Codex review works unchanged** — it's an MCP call, harness-agnostic. |
29
+ | Bind flow → vision item — `POST /api/stratum/bind` (HTTP) | Shell `curl` (harness-agnostic). |
30
+ | Project instructions — **CLAUDE.md** | **AGENTS.md** (Hermes auto-injects `AGENTS.md`/`SOUL.md`/`USER.md`/`MEMORY.md` unless `--ignore-rules`). |
31
+
32
+ ## Setup (one-time, per workspace)
33
+
34
+ ```bash
35
+ # 1. Install the compose skill into Hermes
36
+ hermes skills install <compose-skill-git-url> # → ~/.hermes/skills/compose/
37
+
38
+ # 2. Register the MCP servers the lifecycle reads/writes through
39
+ hermes mcp add compose --command "<compose-mcp-launch-command>"
40
+ hermes mcp add stratum --command "<stratum-mcp-launch-command>"
41
+ hermes mcp test compose && hermes mcp test stratum # verify discovery
42
+
43
+ # 3. Mirror compose's project guidance for auto-injection
44
+ # (compose's CLAUDE.md content → AGENTS.md at the project root)
45
+ ```
46
+
47
+ ## What does NOT port 1:1 (the real work)
48
+
49
+ 1. **`compose-explorer` / `compose-architect` are Claude agent files** (installed to
50
+ `~/.claude/agents/` by `compose setup` → `syncSkills()`). Hermes has no agent-file
51
+ format; re-express each as a **`delegate_task` prompt template** carrying the same
52
+ mandate (explorer = read-only codebase mapping; architect = competing-mandate
53
+ proposals). The prompts in SKILL.md Phases 1/3/4 are the source of truth — port
54
+ those strings, not the `.md` agent files.
55
+
56
+ 2. **`compose setup` targets Claude paths** (`~/.claude/skills/`, `~/.claude/agents/`,
57
+ `.compose-skills.json`). A Hermes path would install to `~/.hermes/skills/` via
58
+ `hermes skills install`. This is a `bin/compose.js syncSkills()` branch, not a
59
+ lifecycle change.
60
+
61
+ 3. **Plan mode (Phase 6)** has no Hermes analog — use the flat-gate degrade above.
62
+ This is the only lifecycle *behavior* that changes shape; everything else is a
63
+ rename.
64
+
65
+ 4. **Tier→model mapping is Claude-centric** (`server/model-tiers.js` maps
66
+ `critical/standard/fast` → Opus/Sonnet/Haiku). Under Hermes-as-driver this only
67
+ matters if you also make Hermes the *executing agent* — which is a **Stratum**
68
+ change (add a `hermes` provider to Stratum's agent runner), out of scope for this
69
+ harness-driver adaptation. See SKILL.md "Stratum Integration".
70
+
71
+ ## Quick decision guide
72
+
73
+ - **Just want to launch builds from Hermes?** Don't port anything — run the headless
74
+ `compose build`/`compose fix` CLI from the Hermes shell. It executes via Claude/Codex
75
+ under Stratum regardless of the launching harness.
76
+ - **Want the interactive lifecycle inside Hermes?** Use this mapping: install the skill,
77
+ register the two MCP servers, port the two agent prompts to `delegate_task`, accept the
78
+ flat-gate Phase-6 degrade.
79
+ - **Want Hermes to be the building agent?** That's the Stratum provider-addition path,
80
+ not this file.
package/README.md CHANGED
@@ -55,7 +55,7 @@ Then in your project:
55
55
 
56
56
  ```bash
57
57
  cd /path/to/your/project
58
- compose init # writes .compose/, registers MCP, scaffolds ROADMAP and pipeline specs
58
+ compose init # writes .compose/, registers MCP, scaffolds ROADMAP, pipeline specs, contracts/vocabulary.yaml
59
59
  compose new "what you want to build"
60
60
  ```
61
61
 
package/bin/compose.js CHANGED
@@ -18,6 +18,7 @@ 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
20
  import { resolveRoadmapPath, resolveFeaturesPath, resolveContextPathFromConfig, resolveFeaturesPathFromConfig, resolveRoadmapPathFromConfig } from '../lib/project-paths.js'
21
+ import { installAgentDefs } from '../lib/install-agent-defs.js'
21
22
 
22
23
  const __dirname = dirname(fileURLToPath(import.meta.url))
23
24
  const PACKAGE_ROOT = resolve(__dirname, '..')
@@ -84,6 +85,7 @@ function resolveCwdWithWorkspace(args) {
84
85
  import { parseTeamFlag } from '../lib/team-flag.js';
85
86
  import { loadDeps, checkExternalSkills, printDepReport, buildDepReport, checkExternalBinaries, printBinaryReport, buildBinaryReport } from '../lib/deps.js';
86
87
  import { checkLatestVersion } from '../lib/version-check.js';
88
+ import { computeHooksStatus, formatHookStatusLines, HOOK_MARKERS } from '../lib/hooks-status.js';
87
89
 
88
90
  const [,, cmd, ...args] = process.argv
89
91
 
@@ -124,10 +126,14 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
124
126
  console.log(' roadmap check Verify feature.json and ROADMAP.md are in sync')
125
127
  console.log(' roadmap xref-sync Pull-reconcile feature.json external links to live state')
126
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)')
127
130
  console.log(' items List vision items from local state (no server)')
128
131
  console.log(' items show <id> Show detail for a specific vision item')
129
132
  console.log(' triage Analyze a feature and recommend build profile')
130
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>)')
131
137
  console.log(' init Initialize Compose in the current project')
132
138
  console.log(' setup Install/sync global skills + register stratum-mcp (alias: sync)')
133
139
  console.log(' sync Re-sync global skills from this install (alias of setup)')
@@ -244,6 +250,17 @@ function syncSkills(agents) {
244
250
  copyFileSync(depsSrc, join(composeSkillDir, '.compose-deps.json'))
245
251
  }
246
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
+
247
264
  // Remove skills we previously installed that no longer exist in source
248
265
  const removed = previousSkills.filter(name => !sourceSkills.has(name))
249
266
  for (const name of removed) {
@@ -476,6 +493,16 @@ async function runInit(flags, cwdOverride) {
476
493
  console.log(`Created ${ideaboxDest}`)
477
494
  }
478
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
+
479
506
  // 6. Register compose-mcp in .mcp.json
480
507
  const mcpPath = join(cwd, '.mcp.json')
481
508
  let mcpConfig = {}
@@ -538,7 +565,7 @@ async function runInit(flags, cwdOverride) {
538
565
  // 8. Copy default pipeline specs if absent
539
566
  const pipelinesDir = join(cwd, 'pipelines')
540
567
  mkdirSync(pipelinesDir, { recursive: true })
541
- 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']) {
542
569
  const dest = join(pipelinesDir, specName)
543
570
  if (!existsSync(dest)) {
544
571
  const src = join(PACKAGE_ROOT, 'pipelines', specName)
@@ -761,6 +788,24 @@ if (cmd === 'migrate-state') {
761
788
  process.exit(0)
762
789
  }
763
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
+
764
809
  if (cmd === 'install') {
765
810
  // Backwards-compat: run both init + setup
766
811
  await runInit(args)
@@ -1600,12 +1645,12 @@ if (cmd === 'hooks') {
1600
1645
  const HOOK_TYPES = {
1601
1646
  'post-commit': {
1602
1647
  template: pjoin(presolve(futp(import.meta.url), '..'), 'git-hooks', 'post-commit.template'),
1603
- marker: '# Compose post-commit hook —',
1648
+ marker: HOOK_MARKERS['post-commit'],
1604
1649
  dest: pjoin(hooksDir, 'post-commit'),
1605
1650
  },
1606
1651
  'pre-push': {
1607
1652
  template: pjoin(presolve(futp(import.meta.url), '..'), 'git-hooks', 'pre-push.template'),
1608
- marker: '# Compose pre-push hook —',
1653
+ marker: HOOK_MARKERS['pre-push'],
1609
1654
  dest: pjoin(hooksDir, 'pre-push'),
1610
1655
  },
1611
1656
  }
@@ -1674,48 +1719,14 @@ if (cmd === 'hooks') {
1674
1719
  return 0
1675
1720
  }
1676
1721
 
1677
- function extractBakedWorkspaceId(content) {
1678
- const m = content.match(/^COMPOSE_WORKSPACE_ID="([^"]*)"$/m)
1679
- return m ? m[1] : null
1680
- }
1681
-
1682
- function statusOne(type) {
1683
- const { marker, dest } = HOOK_TYPES[type]
1684
- if (!exSync(dest)) {
1685
- console.log(`${type}: absent — no hook installed`)
1686
- return
1687
- }
1688
- const content = rfSync(dest, 'utf-8')
1689
- if (!content.includes(marker)) {
1690
- console.log(`${type}: foreign — hook exists but is not a Compose hook`)
1691
- return
1692
- }
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() {
1693
1726
  const wsHint = hookFlags['workspace']
1694
- let expectedWsId = null
1695
- if (wsHint) {
1696
- try { expectedWsId = resolveWorkspace({ cwd: projectRoot, workspaceId: wsHint }).id } catch { /* ignore for status */ }
1697
- } else {
1698
- try { expectedWsId = resolveWorkspace({ cwd: projectRoot }).id } catch { /* ignore for status */ }
1699
- }
1700
- const nodeMatch = content.includes(`COMPOSE_NODE="${composeNode}"`)
1701
- const binMatch = content.includes(`COMPOSE_BIN="${composeBin}"`)
1702
- const hasRawToken = content.includes('__COMPOSE_WORKSPACE_ID__')
1703
- const wsMatch = hasRawToken ? false
1704
- : expectedWsId ? content.includes(`COMPOSE_WORKSPACE_ID="${expectedWsId}"`)
1705
- : true
1706
- if (nodeMatch && binMatch && wsMatch && !hasRawToken) {
1707
- console.log(`${type}: installed (current)`)
1708
- const baked = extractBakedWorkspaceId(content)
1709
- if (baked) console.log(` workspace: ${baked}`)
1710
- } else {
1711
- const reason = hasRawToken ? 'MISSING_WORKSPACE_ID'
1712
- : (expectedWsId && !wsMatch) ? 'STALE_WORKSPACE_ID'
1713
- : 'stale paths'
1714
- console.log(`${type}: installed (${reason} — re-run install)`)
1715
- if (expectedWsId && !wsMatch && !hasRawToken) console.log(` expected COMPOSE_WORKSPACE_ID="${expectedWsId}"`)
1716
- if (!nodeMatch) console.log(` expected COMPOSE_NODE="${composeNode}"`)
1717
- if (!binMatch) console.log(` expected COMPOSE_BIN="${composeBin}"`)
1718
- }
1727
+ try {
1728
+ return resolveWorkspace(wsHint ? { cwd: projectRoot, workspaceId: wsHint } : { cwd: projectRoot }).id
1729
+ } catch { return null /* ignore for status */ }
1719
1730
  }
1720
1731
 
1721
1732
  if (sub === 'install') {
@@ -1745,7 +1756,17 @@ if (cmd === 'hooks') {
1745
1756
  if (!sub || sub === 'status') {
1746
1757
  // Status reports on ALL known hook types (selection flags ignored), so users
1747
1758
  // see the full picture. Selection only affects install/uninstall.
1748
- for (const t of Object.keys(HOOK_TYPES)) statusOne(t)
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
+ }
1749
1770
  process.exit(0)
1750
1771
  }
1751
1772
 
@@ -1978,6 +1999,14 @@ if (cmd === 'build') {
1978
1999
  const all = filteredArgs2.includes('--all')
1979
2000
  const dryRun = filteredArgs2.includes('--dry-run')
1980
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')
1981
2010
 
1982
2011
  // Multiple codes: compose build FEAT-1 FEAT-2 FEAT-3
1983
2012
  const isMulti = featureCodes.length > 1
@@ -1990,12 +2019,38 @@ if (cmd === 'build') {
1990
2019
  process.exit(1)
1991
2020
  }
1992
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
+
1993
2047
  if (!featureCode && !abort && !all) {
1994
2048
  console.error('Usage: compose build <feature-code> [feature-code...]')
1995
2049
  console.error(' compose build STRAT-COMP (prefix — builds all matching)')
1996
2050
  console.error(' compose build --all (builds entire roadmap)')
1997
2051
  console.error('')
1998
2052
  console.error('Options:')
2053
+ console.error(' --quick Trimmed lifecycle (design → implement → ship) for small additive work')
1999
2054
  console.error(' --abort Abort the active build')
2000
2055
  console.error(' --all Build all PLANNED features in dependency order')
2001
2056
  console.error(' --dry-run Print build order without executing')
@@ -2003,11 +2058,23 @@ if (cmd === 'build') {
2003
2058
  process.exit(1)
2004
2059
  }
2005
2060
 
2006
- // 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".
2007
2066
  const { root: buildCwd } = resolveCwdWithWorkspace(args)
2008
- if (!existsSync(join(buildCwd, '.compose', 'compose.json')) || !existsSync(join(buildCwd, 'pipelines', 'build.stratum.yaml'))) {
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) {
2009
2072
  console.log('Running compose init...\n')
2010
- await runInit(args.filter(a => a.startsWith('--')))
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)
2011
2078
  console.log('')
2012
2079
  }
2013
2080
 
@@ -2038,6 +2105,8 @@ if (cmd === 'build') {
2038
2105
  if (agentWorkDir) singleOpts.workingDirectory = agentWorkDir
2039
2106
  if (skipTriage) singleOpts.skipTriage = true
2040
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
2041
2110
  runBuild(featureCode, singleOpts).then(() => {
2042
2111
  process.exit(0)
2043
2112
  }).catch((err) => {
@@ -2762,11 +2831,76 @@ if (cmd === 'build') {
2762
2831
  process.exit(1)
2763
2832
  })
2764
2833
 
2765
- } else if (cmd === 'gates') {
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') {
2766
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]
2767
2900
  // compose gates report [--since 24h|7d|1h|<ISO>] [--feature <FC>]
2768
2901
  // [--format text|json] [--rubber-stamp-ms <N>]
2769
- // COMP-OBS-GATELOG: audit gate log report (Decision 5)
2902
+ // COMP-OBS-GATELOG: audit gate log report (Decision 5)
2903
+ // (`gate` and `gates` are aliases.)
2770
2904
  // ---------------------------------------------------------------------------
2771
2905
  const gatesSubcmd = args[0]
2772
2906
 
@@ -2840,8 +2974,132 @@ if (cmd === 'build') {
2840
2974
  process.exit(0)
2841
2975
  }
2842
2976
 
2843
- console.error(`Unknown gates subcommand: ${gatesSubcmd}`)
2844
- console.error('Usage: compose gates report [--since 24h] [--feature FC] [--format text|json] [--rubber-stamp-ms N]')
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]')
2845
3103
  process.exit(1)
2846
3104
 
2847
3105
  } else if (cmd === 'loops') {