@smartmemory/compose 0.1.1-beta → 0.1.2-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 (124) hide show
  1. package/.claude/skills/bug-fix/SKILL.md +143 -0
  2. package/.claude/skills/compose/SKILL.md +604 -0
  3. package/.compose-deps.json +89 -0
  4. package/README.md +14 -3
  5. package/bin/compose.js +473 -0
  6. package/contracts/comp-obs-contract.schema.json +362 -0
  7. package/contracts/cross-model-review-result.json +78 -0
  8. package/contracts/review-result.json +126 -0
  9. package/dist/assets/{_baseUniq-CQwX6VLz.js → _baseUniq-D-avYfn5.js} +1 -1
  10. package/dist/assets/{arc-SxJ2J1sh.js → arc-BC4dfQ-X.js} +1 -1
  11. package/dist/assets/{architectureDiagram-Q4EWVU46-BykunY1F.js → architectureDiagram-Q4EWVU46-BZmFXnGI.js} +1 -1
  12. package/dist/assets/{blockDiagram-DXYQGD6D-ohAKBOUw.js → blockDiagram-DXYQGD6D-DlfWSuux.js} +1 -1
  13. package/dist/assets/{c4Diagram-AHTNJAMY-DBDC3ENB.js → c4Diagram-AHTNJAMY-Y__uJrRx.js} +1 -1
  14. package/dist/assets/channel-LRG9kHqJ.js +1 -0
  15. package/dist/assets/{chunk-4BX2VUAB-Cv93Z7uM.js → chunk-4BX2VUAB-BfMePfTp.js} +1 -1
  16. package/dist/assets/{chunk-4TB4RGXK-DE0WBDkj.js → chunk-4TB4RGXK-BdlMSdEA.js} +1 -1
  17. package/dist/assets/{chunk-55IACEB6-CE1EXenG.js → chunk-55IACEB6-vrQHZTdv.js} +1 -1
  18. package/dist/assets/{chunk-EDXVE4YY-DA7Ana6H.js → chunk-EDXVE4YY-B8wioVlW.js} +1 -1
  19. package/dist/assets/{chunk-FMBD7UC4-CTDIPA3p.js → chunk-FMBD7UC4-Cd6Hrux2.js} +1 -1
  20. package/dist/assets/{chunk-OYMX7WX6-uGBaPaTX.js → chunk-OYMX7WX6-CfrhdQXY.js} +1 -1
  21. package/dist/assets/{chunk-QZHKN3VN-CYlnXuUO.js → chunk-QZHKN3VN-B9JQerOU.js} +1 -1
  22. package/dist/assets/{chunk-YZCP3GAM-ojGkzcZK.js → chunk-YZCP3GAM-DFN9X99H.js} +1 -1
  23. package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +1 -0
  24. package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +1 -0
  25. package/dist/assets/clone-dRxgFrBv.js +1 -0
  26. package/dist/assets/{cose-bilkent-S5V4N54A-Bktn9hL-.js → cose-bilkent-S5V4N54A-BAn0ap_E.js} +1 -1
  27. package/dist/assets/{dagre-KV5264BT-DFaSzuRF.js → dagre-KV5264BT-DyxnVq1g.js} +1 -1
  28. package/dist/assets/{diagram-5BDNPKRD-DnfmDzEm.js → diagram-5BDNPKRD-XCrzqski.js} +1 -1
  29. package/dist/assets/{diagram-G4DWMVQ6-Bm8W9YnG.js → diagram-G4DWMVQ6-MBCAXft_.js} +1 -1
  30. package/dist/assets/{diagram-MMDJMWI5-B5-TSKvp.js → diagram-MMDJMWI5-DbtB2yS6.js} +1 -1
  31. package/dist/assets/{diagram-TYMM5635-ls4rqlky.js → diagram-TYMM5635-Bb5NzX61.js} +1 -1
  32. package/dist/assets/{erDiagram-SMLLAGMA-giG6WO-r.js → erDiagram-SMLLAGMA-CpIeCOh2.js} +1 -1
  33. package/dist/assets/{flowDiagram-DWJPFMVM-XvlUuz-7.js → flowDiagram-DWJPFMVM-CHyoKnhW.js} +1 -1
  34. package/dist/assets/{ganttDiagram-T4ZO3ILL-hLBV57oV.js → ganttDiagram-T4ZO3ILL-DErKteO_.js} +1 -1
  35. package/dist/assets/{gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js → gitGraphDiagram-UUTBAWPF-KFVAtj2F.js} +1 -1
  36. package/dist/assets/{graph-D0Cfv00Y.js → graph-CRnO_ifT.js} +1 -1
  37. package/dist/assets/index-DKBsEUJ-.css +1 -0
  38. package/dist/assets/index-DkRKLuNr.js +1144 -0
  39. package/dist/assets/{infoDiagram-42DDH7IO-DbqRsOo3.js → infoDiagram-42DDH7IO-BZFnuSp5.js} +1 -1
  40. package/dist/assets/{ishikawaDiagram-UXIWVN3A-DnCdx7zb.js → ishikawaDiagram-UXIWVN3A-4Xe2Szde.js} +1 -1
  41. package/dist/assets/{journeyDiagram-VCZTEJTY-CfD7eNcP.js → journeyDiagram-VCZTEJTY-CZRByfS-.js} +1 -1
  42. package/dist/assets/{kanban-definition-6JOO6SKY-BYaO9-mK.js → kanban-definition-6JOO6SKY-B95sk6Fk.js} +1 -1
  43. package/dist/assets/{layout-Bj72wOEB.js → layout-BqNQzxWT.js} +1 -1
  44. package/dist/assets/{linear-BRFo114D.js → linear-CUh7qb64.js} +1 -1
  45. package/dist/assets/{min-GCHnKlJS.js → min-wXgOS3ig.js} +1 -1
  46. package/dist/assets/{mindmap-definition-QFDTVHPH-n0PMebY4.js → mindmap-definition-QFDTVHPH-DB6iaAbO.js} +1 -1
  47. package/dist/assets/{pieDiagram-DEJITSTG-pN4CljHF.js → pieDiagram-DEJITSTG-CHkZHrTW.js} +1 -1
  48. package/dist/assets/{quadrantDiagram-34T5L4WZ-DNoAy8-D.js → quadrantDiagram-34T5L4WZ-DoTEO8e3.js} +1 -1
  49. package/dist/assets/{requirementDiagram-MS252O5E-BhtY05PT.js → requirementDiagram-MS252O5E-Dn8peXYp.js} +1 -1
  50. package/dist/assets/{sankeyDiagram-XADWPNL6-B6AD-16A.js → sankeyDiagram-XADWPNL6-DRXs6Ipb.js} +1 -1
  51. package/dist/assets/{sequenceDiagram-FGHM5R23-DShHM-uk.js → sequenceDiagram-FGHM5R23-wBBYZ0aq.js} +1 -1
  52. package/dist/assets/{stateDiagram-FHFEXIEX-DMxn7HTo.js → stateDiagram-FHFEXIEX-DPlBNGmf.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +1 -0
  54. package/dist/assets/{timeline-definition-GMOUNBTQ-Cdu6uq52.js → timeline-definition-GMOUNBTQ-CbbyTlHk.js} +1 -1
  55. package/dist/assets/{vennDiagram-DHZGUBPP-CpK29iRe.js → vennDiagram-DHZGUBPP-Bj4GaFfj.js} +1 -1
  56. package/dist/assets/{wardley-RL74JXVD-BQgSkdcO.js → wardley-RL74JXVD-RtNzq8KU.js} +55 -55
  57. package/dist/assets/{wardleyDiagram-NUSXRM2D-DJHYev6O.js → wardleyDiagram-NUSXRM2D-CDfE3zSj.js} +1 -1
  58. package/dist/assets/{xychartDiagram-5P7HB3ND-1d75pbaO.js → xychartDiagram-5P7HB3ND-CZXHHYD5.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/lib/budget-ledger.js +45 -0
  61. package/lib/bug-bisect.js +292 -0
  62. package/lib/bug-checkpoint.js +191 -0
  63. package/lib/bug-escalation.js +306 -0
  64. package/lib/bug-index-gen.js +136 -0
  65. package/lib/bug-ledger.js +126 -0
  66. package/lib/build-stream-schema.js +176 -0
  67. package/lib/build-stream-writer.js +3 -1
  68. package/lib/build.js +854 -284
  69. package/lib/connector-factory-shim.js +167 -0
  70. package/lib/constants.js +18 -0
  71. package/lib/debug-discipline.js +176 -27
  72. package/lib/deps.js +205 -0
  73. package/lib/health-score.js +4 -4
  74. package/lib/import.js +26 -13
  75. package/lib/inject-schema.js +21 -0
  76. package/lib/new.js +27 -53
  77. package/lib/result-normalizer.js +160 -144
  78. package/lib/review-lenses.js +5 -5
  79. package/lib/review-normalize.js +413 -0
  80. package/lib/review-prompt.js +163 -0
  81. package/lib/sections.js +325 -0
  82. package/lib/step-prompt.js +21 -1
  83. package/lib/step-validator.js +5 -3
  84. package/lib/stratum-mcp-client.js +172 -7
  85. package/package.json +14 -3
  86. package/pipelines/bug-fix.stratum.yaml +39 -1
  87. package/pipelines/build.stratum.yaml +28 -45
  88. package/pipelines/review-fix.stratum.yaml +1 -1
  89. package/presets/team-review.stratum.yaml +21 -14
  90. package/server/build-stream-bridge.js +28 -0
  91. package/server/cc-session-feature-resolver.js +111 -0
  92. package/server/cc-session-reader.js +327 -0
  93. package/server/cc-session-watcher.js +318 -0
  94. package/server/compose-mcp-tools.js +0 -125
  95. package/server/compose-mcp.js +2 -4
  96. package/server/contract-diff.js +192 -0
  97. package/server/decision-event-emit.js +175 -0
  98. package/server/decision-event-id.js +64 -0
  99. package/server/decision-events-snapshot.js +166 -0
  100. package/server/design-routes.js +92 -49
  101. package/server/drift-axes.js +365 -0
  102. package/server/drift-emit.js +121 -0
  103. package/server/gate-log-store.js +102 -0
  104. package/server/lifecycle-phase-history.js +44 -0
  105. package/server/open-loops-store.js +102 -0
  106. package/server/schema-validator.js +49 -0
  107. package/server/status-emit.js +27 -0
  108. package/server/status-snapshot.js +218 -0
  109. package/server/vision-routes.js +332 -4
  110. package/server/vision-server.js +104 -12
  111. package/server/vision-store.js +21 -0
  112. package/dist/assets/channel-DGElom1e.js +0 -1
  113. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +0 -1
  114. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +0 -1
  115. package/dist/assets/clone-DUJKJXd7.js +0 -1
  116. package/dist/assets/index-CUd6pFGF.css +0 -1
  117. package/dist/assets/index-DReRlzZI.js +0 -1144
  118. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +0 -1
  119. package/server/connectors/agent-connector.js +0 -78
  120. package/server/connectors/claude-sdk-connector.js +0 -198
  121. package/server/connectors/codex-connector.js +0 -240
  122. package/server/connectors/connector-discovery.js +0 -18
  123. package/server/connectors/connector-runtime.js +0 -13
  124. package/server/connectors/opencode-connector.js +0 -200
@@ -0,0 +1,89 @@
1
+ {
2
+ "version": 1,
3
+ "external_skills": [
4
+ {
5
+ "id": "superpowers:systematic-debugging",
6
+ "required_for": ["bug-fix Phase F3", "any unexpected failure"],
7
+ "install": "claude plugin install superpowers",
8
+ "fallback": "general-purpose Agent with debugging prompt",
9
+ "optional": false
10
+ },
11
+ {
12
+ "id": "superpowers:test-driven-development",
13
+ "required_for": ["bug-fix Phase F2", "build Phase 7 step 1"],
14
+ "install": "claude plugin install superpowers",
15
+ "fallback": "inline TDD instructions in SKILL.md",
16
+ "optional": false
17
+ },
18
+ {
19
+ "id": "superpowers:verification-before-completion",
20
+ "required_for": ["all phase exits"],
21
+ "install": "claude plugin install superpowers",
22
+ "fallback": "inline verification checklist in SKILL.md",
23
+ "optional": false
24
+ },
25
+ {
26
+ "id": "superpowers:requesting-code-review",
27
+ "required_for": ["Phase 7 review fallback"],
28
+ "install": "claude plugin install superpowers",
29
+ "fallback": "general-purpose Agent reviewer with canonical ReviewResult prompt",
30
+ "optional": true
31
+ },
32
+ {
33
+ "id": "superpowers:executing-plans",
34
+ "required_for": ["Phase 7 sequential execution"],
35
+ "install": "claude plugin install superpowers",
36
+ "fallback": "inline plan-execution instructions",
37
+ "optional": false
38
+ },
39
+ {
40
+ "id": "superpowers:dispatching-parallel-agents",
41
+ "required_for": ["Phase 7 parallel execution"],
42
+ "install": "claude plugin install superpowers",
43
+ "fallback": "sequential execution",
44
+ "optional": true
45
+ },
46
+ {
47
+ "id": "interface-design:init",
48
+ "required_for": ["Phase 7 — new UI components"],
49
+ "install": "claude plugin install interface-design",
50
+ "fallback": null,
51
+ "optional": true
52
+ },
53
+ {
54
+ "id": "interface-design:critique",
55
+ "required_for": ["Phase 7 — UI critique pass"],
56
+ "install": "claude plugin install interface-design",
57
+ "fallback": null,
58
+ "optional": true
59
+ },
60
+ {
61
+ "id": "interface-design:audit",
62
+ "required_for": ["Phase 7 — UI design-system audit"],
63
+ "install": "claude plugin install interface-design",
64
+ "fallback": null,
65
+ "optional": true
66
+ },
67
+ {
68
+ "id": "codex:review",
69
+ "required_for": ["Phase 7 review gate (human-driven)"],
70
+ "install": "claude plugin install openai-codex",
71
+ "fallback": "mcp__stratum__stratum_agent_run type=codex",
72
+ "optional": true
73
+ },
74
+ {
75
+ "id": "refactor",
76
+ "required_for": ["Phase 7 large-file split"],
77
+ "install": "user-installed skill at ~/.claude/skills/refactor/",
78
+ "fallback": null,
79
+ "optional": true
80
+ },
81
+ {
82
+ "id": "update-docs",
83
+ "required_for": ["Phase 9 docs sync"],
84
+ "install": "user-installed skill at ~/.claude/skills/update-docs/",
85
+ "fallback": "manual TODO surfaced in implementation report",
86
+ "optional": true
87
+ }
88
+ ]
89
+ }
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  AI-powered product lifecycle orchestrator. Compose takes a product idea from intent to shipped code through structured, agent-driven pipelines with human gates at every critical decision point.
4
4
 
5
- Compose coordinates multiple AI agents (Claude, Codex) through YAML-defined workflows powered by [Stratum](https://github.com/regression-io/stratum), enforcing postconditions, retrying on failure, and producing auditable execution traces.
5
+ Compose coordinates multiple AI agents (Claude, Codex) through YAML-defined workflows powered by [Stratum](https://github.com/smartmemory/stratum), enforcing postconditions, retrying on failure, and producing auditable execution traces.
6
6
 
7
7
  ## Table of Contents
8
8
 
@@ -75,7 +75,7 @@ compose build FEAT-1
75
75
  ### Install Compose
76
76
 
77
77
  ```bash
78
- git clone https://github.com/regression-io/compose.git
78
+ git clone https://github.com/smartmemory/compose.git
79
79
  cd compose
80
80
  npm install
81
81
  ```
@@ -224,12 +224,23 @@ compose init --no-lifecycle
224
224
 
225
225
  ### `compose setup`
226
226
 
227
- Global skill and MCP registration. Installs the `/compose` skill and Stratum skill to all detected agents.
227
+ Global skill and MCP registration. Installs the `/compose` skill and Stratum skill to all detected agents. At the end, runs an external-dependency check (see `compose doctor`) and prints actionable install hints for any missing external skills or commands.
228
228
 
229
229
  ```bash
230
230
  compose setup
231
231
  ```
232
232
 
233
+ ### `compose doctor`
234
+
235
+ Verifies that the external skills and commands the lifecycle relies on (e.g. `superpowers:*`, `interface-design:*`, `codex:review`, `refactor`, `update-docs`) are installed locally. The authoritative dep list lives in `.compose-deps.json` at the package root.
236
+
237
+ ```bash
238
+ compose doctor # human-readable report
239
+ compose doctor --json # machine-readable, full dep records (id, required_for, install, fallback, optional)
240
+ compose doctor --strict # exit 1 on any missing required dep (use in CI)
241
+ compose doctor --verbose # also list the filesystem paths scanned
242
+ ```
243
+
233
244
  ### `compose start`
234
245
 
235
246
  Start the Compose app (supervisor with web UI, terminal, and API server).
package/bin/compose.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * compose install — run init + setup (backwards-compat alias)
8
8
  * compose start — start the compose app (supervisor.js)
9
9
  * compose build — headless feature lifecycle runner
10
+ * compose fix — headless bug-fix lifecycle runner (pipelines/bug-fix.stratum.yaml)
10
11
  */
11
12
  import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, rmSync, readdirSync } from 'fs'
12
13
  import { resolve, join, basename, dirname } from 'path'
@@ -22,6 +23,7 @@ const PACKAGE_ROOT = resolve(__dirname, '..')
22
23
  // --team flag (COMP-TEAMS)
23
24
  // ---------------------------------------------------------------------------
24
25
  import { parseTeamFlag } from '../lib/team-flag.js';
26
+ import { loadDeps, checkExternalSkills, printDepReport } from '../lib/deps.js';
25
27
 
26
28
  const [,, cmd, ...args] = process.argv
27
29
 
@@ -37,6 +39,7 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
37
39
  console.log(' import Scan existing project and generate structured analysis')
38
40
  console.log(' feature Add a single feature (folder, design seed, ROADMAP entry)')
39
41
  console.log(' build Run a feature through the headless lifecycle')
42
+ console.log(' fix Run a bug through the headless bug-fix lifecycle')
40
43
  console.log(' pipeline View and edit the build pipeline')
41
44
  console.log(' roadmap Show roadmap status and next buildable features')
42
45
  console.log(' roadmap generate Regenerate ROADMAP.md from feature.json files')
@@ -46,6 +49,7 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
46
49
  console.log(' qa-scope Show affected routes from a feature\'s changed files')
47
50
  console.log(' init Initialize Compose in the current project')
48
51
  console.log(' setup Install global skill + register stratum-mcp')
52
+ console.log(' doctor Check external skill dependencies')
49
53
  process.exit(0)
50
54
  }
51
55
 
@@ -148,6 +152,14 @@ function syncSkills(agents) {
148
152
  console.log(` + ${agent.name}/${name}`)
149
153
  }
150
154
 
155
+ // Copy .compose-deps.json next to the compose SKILL.md so the lifecycle
156
+ // can read it as a fallback when `compose doctor` is unreachable.
157
+ const depsSrc = join(PACKAGE_ROOT, '.compose-deps.json')
158
+ const composeSkillDir = join(agentSkillsRoot, 'compose')
159
+ if (existsSync(depsSrc) && existsSync(composeSkillDir)) {
160
+ copyFileSync(depsSrc, join(composeSkillDir, '.compose-deps.json'))
161
+ }
162
+
151
163
  // Remove skills we previously installed that no longer exist in source
152
164
  const removed = previousSkills.filter(name => !sourceSkills.has(name))
153
165
  for (const name of removed) {
@@ -169,6 +181,34 @@ function syncSkills(agents) {
169
181
  console.log(` - ${name} — not found`)
170
182
  }
171
183
  }
184
+
185
+ // External skill dep check — surface missing plugins / user skills with
186
+ // actionable install hints. Soft check: warnings only, exit code unaffected.
187
+ const deps = loadDeps(PACKAGE_ROOT)
188
+ if (deps) {
189
+ const result = checkExternalSkills(deps)
190
+ printDepReport(result)
191
+ }
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // compose doctor — re-run the external dep check
196
+ // ---------------------------------------------------------------------------
197
+
198
+ function runDoctor(flags = []) {
199
+ const json = flags.includes('--json')
200
+ const strict = flags.includes('--strict')
201
+ const verbose = flags.includes('--verbose') || flags.includes('-v')
202
+
203
+ const deps = loadDeps(PACKAGE_ROOT)
204
+ if (!deps) {
205
+ console.error('Error: .compose-deps.json missing or invalid at package root')
206
+ process.exit(1)
207
+ }
208
+ const result = checkExternalSkills(deps)
209
+ const allRequiredPresent = printDepReport(result, { json, verbose })
210
+
211
+ if (strict && !allRequiredPresent) process.exit(1)
172
212
  }
173
213
 
174
214
  // ---------------------------------------------------------------------------
@@ -408,6 +448,11 @@ if (cmd === 'setup') {
408
448
  process.exit(0)
409
449
  }
410
450
 
451
+ if (cmd === 'doctor') {
452
+ runDoctor(args)
453
+ process.exit(0)
454
+ }
455
+
411
456
  if (cmd === 'install') {
412
457
  // Backwards-compat: run both init + setup
413
458
  await runInit(args)
@@ -1075,6 +1120,118 @@ if (cmd === 'build') {
1075
1120
  })
1076
1121
  })
1077
1122
  }
1123
+ } else if (cmd === 'fix') {
1124
+ // compose fix <bug-code> — runs the bug-fix.stratum.yaml pipeline.
1125
+ // Thin delegation to runBuild() with template='bug-fix'. The pipeline owns
1126
+ // iteration (test step retries=5 + ensure passing==true; retro_check enforces
1127
+ // hard-stop at attempt 2 for visual/CSS bugs and flags fix chains).
1128
+ let agentWorkDir = null
1129
+ const cwdIdx = args.indexOf('--cwd')
1130
+ if (cwdIdx !== -1) {
1131
+ const cwdValue = args[cwdIdx + 1]
1132
+ if (!cwdValue || cwdValue.startsWith('-')) {
1133
+ console.error('Error: --cwd requires a path argument')
1134
+ process.exit(1)
1135
+ }
1136
+ agentWorkDir = resolve(cwdValue)
1137
+ }
1138
+ const filteredArgs = args.filter((a, i) => i !== cwdIdx && (cwdIdx === -1 || i !== cwdIdx + 1))
1139
+ const bugCodes = filteredArgs.filter(a => !a.startsWith('-'))
1140
+ const bugCode = bugCodes[0]
1141
+ const abort = filteredArgs.includes('--abort')
1142
+ const resume = filteredArgs.includes('--resume')
1143
+
1144
+ if (!bugCode && !abort) {
1145
+ console.error('Usage: compose fix <bug-code>')
1146
+ console.error('')
1147
+ console.error('Runs the bug-fix pipeline (reproduce → diagnose → scope → fix → test → verify → retro → ship).')
1148
+ console.error('')
1149
+ console.error('Options:')
1150
+ console.error(' --abort Abort the active fix run')
1151
+ console.error(' --resume Resume the active fix run for <bug-code>')
1152
+ console.error(' --cwd <path> Agent working directory (for cross-repo bugs)')
1153
+ process.exit(1)
1154
+ }
1155
+
1156
+ const fixCwd = process.cwd()
1157
+ if (!existsSync(join(fixCwd, '.compose', 'compose.json')) || !existsSync(join(fixCwd, 'pipelines', 'bug-fix.stratum.yaml'))) {
1158
+ console.log('Running compose init...\n')
1159
+ await runInit(args.filter(a => a.startsWith('--')))
1160
+ console.log('')
1161
+ }
1162
+
1163
+ // COMP-FIX-HARD T4: bug description lives at docs/bugs/<bug-code>/description.md.
1164
+ // If absent, scaffold a stub and exit 1 so the user can fill it before retrying.
1165
+ let bugDescription = null
1166
+ if (!abort && bugCode) {
1167
+ const bugDir = join(fixCwd, 'docs', 'bugs', bugCode)
1168
+ const descPath = join(bugDir, 'description.md')
1169
+ if (!existsSync(descPath)) {
1170
+ mkdirSync(bugDir, { recursive: true })
1171
+ const scaffold = `# ${bugCode}: <symptom in one sentence>
1172
+
1173
+ ## Steps to reproduce
1174
+
1175
+ 1.
1176
+ 2.
1177
+ 3.
1178
+
1179
+ ## Expected behavior
1180
+
1181
+ ## Actual behavior
1182
+
1183
+ ## Environment / Notes
1184
+ `
1185
+ writeFileSync(descPath, scaffold)
1186
+ console.error(`No description found at docs/bugs/${bugCode}/description.md. Scaffold written. Edit it and re-run 'compose fix ${bugCode}'.`)
1187
+ process.exit(1)
1188
+ }
1189
+ try {
1190
+ bugDescription = readFileSync(descPath, 'utf-8').trim()
1191
+ } catch (err) {
1192
+ console.error(`Failed to read ${descPath}: ${err.message}`)
1193
+ process.exit(1)
1194
+ }
1195
+ if (!bugDescription) {
1196
+ console.error(`docs/bugs/${bugCode}/description.md is empty. Edit it and re-run 'compose fix ${bugCode}'.`)
1197
+ process.exit(1)
1198
+ }
1199
+ }
1200
+
1201
+ // COMP-FIX-HARD T8: --resume requires a matching active build for this bug.
1202
+ let resumeFlowId = null
1203
+ if (resume && !abort && bugCode) {
1204
+ const activeBuildPath = join(fixCwd, '.compose', 'data', 'active-build.json')
1205
+ let active = null
1206
+ if (existsSync(activeBuildPath)) {
1207
+ try { active = JSON.parse(readFileSync(activeBuildPath, 'utf-8')) } catch { active = null }
1208
+ }
1209
+ if (!active || active.featureCode !== bugCode || !active.flowId) {
1210
+ console.error(`No active build to resume for ${bugCode}`)
1211
+ process.exit(1)
1212
+ }
1213
+ // Refuse to resume a feature build as a bug build. Mode is best-effort:
1214
+ // legacy active-build.json files that predate the mode field have no
1215
+ // active.mode, in which case we trust the runBuild-side mode check.
1216
+ if (active.mode && active.mode !== 'bug') {
1217
+ console.error(`Cannot --resume: active build for ${bugCode} is in ${active.mode} mode, not bug mode.`)
1218
+ process.exit(1)
1219
+ }
1220
+ resumeFlowId = active.flowId
1221
+ }
1222
+
1223
+ import('../lib/build.js').then(({ runBuild }) => {
1224
+ const opts = { abort, template: 'bug-fix', mode: 'bug' }
1225
+ if (agentWorkDir) opts.workingDirectory = agentWorkDir
1226
+ if (bugDescription) opts.description = bugDescription
1227
+ if (resumeFlowId) opts.resumeFlowId = resumeFlowId
1228
+ runBuild(bugCode, opts).then(() => {
1229
+ process.exit(0)
1230
+ }).catch((err) => {
1231
+ console.error(`Fix failed: ${err.message}`)
1232
+ process.exit(1)
1233
+ })
1234
+ })
1078
1235
  } else if (cmd === 'triage') {
1079
1236
  const triageCode = args.find(a => !a.startsWith('-'))
1080
1237
  if (!triageCode) {
@@ -1509,7 +1666,323 @@ if (cmd === 'build') {
1509
1666
  process.exit(1)
1510
1667
  })
1511
1668
 
1669
+ } else if (cmd === 'gates') {
1670
+ // ---------------------------------------------------------------------------
1671
+ // compose gates report [--since 24h|7d|1h|<ISO>] [--feature <FC>]
1672
+ // [--format text|json] [--rubber-stamp-ms <N>]
1673
+ // COMP-OBS-GATELOG: audit gate log report (Decision 5)
1674
+ // ---------------------------------------------------------------------------
1675
+ const gatesSubcmd = args[0]
1676
+
1677
+ if (gatesSubcmd === 'report') {
1678
+ const { readGateLog } = await import('../server/gate-log-store.js')
1679
+ const flagIdx = (flag) => args.indexOf(flag)
1680
+ const flagVal = (flag) => {
1681
+ const i = flagIdx(flag)
1682
+ return i !== -1 && args[i + 1] ? args[i + 1] : null
1683
+ }
1684
+
1685
+ // Parse --since: shorthand (24h, 7d, 1h) or ISO date string
1686
+ const sinceStr = flagVal('--since')
1687
+ let sinceMs = null
1688
+ if (sinceStr) {
1689
+ const shorthand = sinceStr.match(/^(\d+)(h|d)$/)
1690
+ if (shorthand) {
1691
+ const n = parseInt(shorthand[1], 10)
1692
+ const mult = shorthand[2] === 'h' ? 3600000 : 86400000
1693
+ sinceMs = Date.now() - n * mult
1694
+ } else {
1695
+ const parsed = Date.parse(sinceStr)
1696
+ if (!isNaN(parsed)) sinceMs = parsed
1697
+ else {
1698
+ console.error(`--since: cannot parse "${sinceStr}" (use e.g. 24h, 7d, or ISO date)`)
1699
+ process.exit(1)
1700
+ }
1701
+ }
1702
+ } else {
1703
+ // Default: last 24h
1704
+ sinceMs = Date.now() - 86400000
1705
+ }
1706
+
1707
+ const featureFilter = flagVal('--feature')
1708
+ const format = flagVal('--format') || 'text'
1709
+ const rubberStampMs = parseInt(flagVal('--rubber-stamp-ms') || '3000', 10)
1710
+
1711
+ const entries = readGateLog({ since: sinceMs, featureCode: featureFilter || undefined })
1712
+
1713
+ if (format === 'json') {
1714
+ // Per-gate_id stats as JSON
1715
+ const stats = buildGateStats(entries, rubberStampMs)
1716
+ console.log(JSON.stringify(stats, null, 2))
1717
+ process.exit(0)
1718
+ }
1719
+
1720
+ // Text table
1721
+ const stats = buildGateStats(entries, rubberStampMs)
1722
+ if (stats.length === 0) {
1723
+ console.log('No gate log entries found for the specified window.')
1724
+ process.exit(0)
1725
+ }
1726
+
1727
+ const col = (s, w) => String(s ?? '').padEnd(w)
1728
+ const hdr = col('gate_id', 28) + col('logged', 8) + col('approve%', 10) + col('deny%', 7) + col('interrupt%', 12) + col('median_ms', 11) + 'rubber_stamp%'
1729
+ const sep = '-'.repeat(hdr.length)
1730
+ console.log(hdr)
1731
+ console.log(sep)
1732
+ for (const row of stats) {
1733
+ const flag = row.rubber_stamp_pct > 50 ? ' <- rubber-stamp candidate' : ''
1734
+ console.log(
1735
+ col(row.gate_id, 28) +
1736
+ col(row.logged_decisions, 8) +
1737
+ col(row.approve_pct.toFixed(1), 10) +
1738
+ col(row.deny_pct.toFixed(1), 7) +
1739
+ col(row.interrupt_pct.toFixed(1), 12) +
1740
+ col(row.median_ms !== null ? row.median_ms : 'N/A', 11) +
1741
+ row.rubber_stamp_pct.toFixed(1) + flag
1742
+ )
1743
+ }
1744
+ process.exit(0)
1745
+ }
1746
+
1747
+ console.error(`Unknown gates subcommand: ${gatesSubcmd}`)
1748
+ console.error('Usage: compose gates report [--since 24h] [--feature FC] [--format text|json] [--rubber-stamp-ms N]')
1749
+ process.exit(1)
1750
+
1751
+ } else if (cmd === 'loops') {
1752
+ // ---------------------------------------------------------------------------
1753
+ // compose loops add --feature <FC> --kind <kind> --summary "<text>" [--ttl-days N] [--parent-branch <bid>]
1754
+ // compose loops list --feature <FC> [--include-resolved] [--format json]
1755
+ // compose loops resolve <loopId> --feature <FC> --note "<text>"
1756
+ // COMP-OBS-LOOPS (Decision 4)
1757
+ // ---------------------------------------------------------------------------
1758
+ const loopsSubcmd = args[0]
1759
+
1760
+ const flagVal = (flag) => {
1761
+ const i = args.indexOf(flag)
1762
+ return i !== -1 && args[i + 1] ? args[i + 1] : null
1763
+ }
1764
+ const hasFlag = (flag) => args.includes(flag)
1765
+
1766
+ const featureCode = flagVal('--feature')
1767
+
1768
+ // --feature is required on every subcommand
1769
+ if (!featureCode) {
1770
+ console.error('compose loops: --feature <FC> is required on every subcommand')
1771
+ console.error(' compose loops add --feature <FC> --kind <kind> --summary "<text>"')
1772
+ console.error(' compose loops list --feature <FC>')
1773
+ console.error(' compose loops resolve <loopId> --feature <FC> --note "<text>"')
1774
+ process.exit(1)
1775
+ }
1776
+
1777
+ // Resolve compose server URL (default http://localhost:3000)
1778
+ const baseUrl = process.env.COMPOSE_URL || 'http://localhost:3000'
1779
+
1780
+ async function httpGet(url) {
1781
+ const { default: http } = await import(url.startsWith('https') ? 'https' : 'http')
1782
+ return new Promise((resolve, reject) => {
1783
+ http.get(url, (res) => {
1784
+ let buf = ''
1785
+ res.on('data', c => { buf += c })
1786
+ res.on('end', () => {
1787
+ try { resolve({ status: res.statusCode, body: JSON.parse(buf) }) }
1788
+ catch { resolve({ status: res.statusCode, body: buf }) }
1789
+ })
1790
+ }).on('error', reject)
1791
+ })
1792
+ }
1793
+
1794
+ async function httpPost(urlStr, body) {
1795
+ const { default: http } = await import(urlStr.startsWith('https') ? 'https' : 'http')
1796
+ return new Promise((resolve, reject) => {
1797
+ const data = JSON.stringify(body)
1798
+ const url = new URL(urlStr)
1799
+ const options = {
1800
+ hostname: url.hostname,
1801
+ port: url.port || (urlStr.startsWith('https') ? 443 : 80),
1802
+ path: url.pathname + url.search,
1803
+ method: 'POST',
1804
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
1805
+ }
1806
+ const req = http.request(options, (res) => {
1807
+ let buf = ''
1808
+ res.on('data', c => { buf += c })
1809
+ res.on('end', () => {
1810
+ try { resolve({ status: res.statusCode, body: JSON.parse(buf) }) }
1811
+ catch { resolve({ status: res.statusCode, body: buf }) }
1812
+ })
1813
+ })
1814
+ req.on('error', reject)
1815
+ req.end(data)
1816
+ })
1817
+ }
1818
+
1819
+ // Resolve item id from feature code
1820
+ async function getItemByFeatureCode(fc) {
1821
+ const r = await httpGet(`${baseUrl}/api/vision/items`)
1822
+ if (r.status !== 200) throw new Error(`Failed to list items: ${JSON.stringify(r.body)}`)
1823
+ const items = r.body.items || r.body
1824
+ const item = items.find(i => i.lifecycle?.featureCode === fc)
1825
+ if (!item) throw new Error(`No item found with featureCode=${fc}`)
1826
+ return item
1827
+ }
1828
+
1829
+ if (loopsSubcmd === 'add') {
1830
+ const kind = flagVal('--kind')
1831
+ const summary = flagVal('--summary')
1832
+ const ttlDays = flagVal('--ttl-days') ? parseInt(flagVal('--ttl-days'), 10) : undefined
1833
+ const parentBranch = flagVal('--parent-branch') || undefined
1834
+
1835
+ if (!kind) { console.error('--kind is required'); process.exit(1) }
1836
+ if (!summary) { console.error('--summary is required'); process.exit(1) }
1837
+
1838
+ try {
1839
+ const item = await getItemByFeatureCode(featureCode)
1840
+ const r = await httpPost(`${baseUrl}/api/vision/items/${item.id}/loops`, { kind, summary, ttl_days: ttlDays, parent_branch: parentBranch })
1841
+ if (r.status !== 201) {
1842
+ console.error(`Error: ${JSON.stringify(r.body)}`)
1843
+ process.exit(1)
1844
+ }
1845
+ const format = flagVal('--format')
1846
+ if (format === 'json') {
1847
+ console.log(JSON.stringify(r.body.loop, null, 2))
1848
+ } else {
1849
+ console.log(`Created loop: ${r.body.loop.id}`)
1850
+ console.log(` kind: ${r.body.loop.kind}`)
1851
+ console.log(` summary: ${r.body.loop.summary}`)
1852
+ }
1853
+ process.exit(0)
1854
+ } catch (err) {
1855
+ console.error(`loops add failed: ${err.message}`)
1856
+ process.exit(1)
1857
+ }
1858
+
1859
+ } else if (loopsSubcmd === 'list') {
1860
+ const includeResolved = hasFlag('--include-resolved')
1861
+ const format = flagVal('--format')
1862
+ const nowMs = Date.now()
1863
+
1864
+ try {
1865
+ const item = await getItemByFeatureCode(featureCode)
1866
+ const r = await httpGet(`${baseUrl}/api/vision/items/${item.id}/loops${includeResolved ? '?includeResolved=true' : ''}`)
1867
+ if (r.status !== 200) {
1868
+ console.error(`Error: ${JSON.stringify(r.body)}`)
1869
+ process.exit(1)
1870
+ }
1871
+ const loops = r.body.loops
1872
+
1873
+ if (format === 'json') {
1874
+ console.log(JSON.stringify(loops, null, 2))
1875
+ process.exit(0)
1876
+ }
1877
+
1878
+ if (loops.length === 0) {
1879
+ console.log(`No open loops for ${featureCode}`)
1880
+ process.exit(0)
1881
+ }
1882
+
1883
+ // Sort oldest first
1884
+ const sorted = [...loops].sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at))
1885
+ // ANSI red for stale
1886
+ const RED = '\x1b[31m'
1887
+ const RESET = '\x1b[0m'
1888
+
1889
+ for (const loop of sorted) {
1890
+ const { isStaleLoop } = await import('../server/open-loops-store.js')
1891
+ const stale = isStaleLoop(loop, nowMs)
1892
+ const prefix = stale ? `${RED}>TTL ` : ' '
1893
+ const suffix = stale ? RESET : ''
1894
+ const status = loop.resolution ? '[resolved]' : '[open]'
1895
+ console.log(`${prefix}${loop.id.slice(0, 8)} ${status} [${loop.kind}] ${loop.summary}${suffix}`)
1896
+ }
1897
+ process.exit(0)
1898
+ } catch (err) {
1899
+ console.error(`loops list failed: ${err.message}`)
1900
+ process.exit(1)
1901
+ }
1902
+
1903
+ } else if (loopsSubcmd === 'resolve') {
1904
+ const loopId = args[1]
1905
+ const note = flagVal('--note') || ''
1906
+
1907
+ if (!loopId || loopId.startsWith('-')) {
1908
+ console.error('Usage: compose loops resolve <loopId> --feature <FC> [--note "<text>"]')
1909
+ process.exit(1)
1910
+ }
1911
+
1912
+ try {
1913
+ const item = await getItemByFeatureCode(featureCode)
1914
+ const r = await httpPost(`${baseUrl}/api/vision/items/${item.id}/loops/${loopId}/resolve`, {
1915
+ note,
1916
+ resolved_by: process.env.USER || 'unknown',
1917
+ })
1918
+ if (r.status !== 200) {
1919
+ console.error(`Error: ${JSON.stringify(r.body)}`)
1920
+ process.exit(1)
1921
+ }
1922
+ const format = flagVal('--format')
1923
+ if (format === 'json') {
1924
+ console.log(JSON.stringify(r.body.loop, null, 2))
1925
+ } else {
1926
+ console.log(`Resolved loop: ${r.body.loop.id}`)
1927
+ if (note) console.log(` note: ${note}`)
1928
+ }
1929
+ process.exit(0)
1930
+ } catch (err) {
1931
+ console.error(`loops resolve failed: ${err.message}`)
1932
+ process.exit(1)
1933
+ }
1934
+
1935
+ } else {
1936
+ console.error(`Unknown loops subcommand: ${loopsSubcmd}`)
1937
+ console.error(' compose loops add --feature <FC> --kind <kind> --summary "<text>"')
1938
+ console.error(' compose loops list --feature <FC>')
1939
+ console.error(' compose loops resolve <loopId> --feature <FC> --note "<text>"')
1940
+ process.exit(1)
1941
+ }
1942
+
1512
1943
  } else {
1513
1944
  console.error(`Unknown command: ${cmd}`)
1514
1945
  process.exit(1)
1515
1946
  }
1947
+
1948
+ // ---------------------------------------------------------------------------
1949
+ // Helper: build per-gate stats for `compose gates report`
1950
+ // ---------------------------------------------------------------------------
1951
+ function buildGateStats(entries, rubberStampMs = 3000) {
1952
+ const byGate = new Map()
1953
+
1954
+ for (const e of entries) {
1955
+ const gid = e.gate_id
1956
+ if (!byGate.has(gid)) byGate.set(gid, [])
1957
+ byGate.get(gid).push(e)
1958
+ }
1959
+
1960
+ const rows = []
1961
+ for (const [gate_id, gEntries] of byGate) {
1962
+ const n = gEntries.length
1963
+ const approve = gEntries.filter(e => e.decision === 'approve').length
1964
+ const deny = gEntries.filter(e => e.decision === 'deny').length
1965
+ const interrupt = gEntries.filter(e => e.decision === 'interrupt').length
1966
+ const durations = gEntries
1967
+ .map(e => e.duration_to_decide_ms)
1968
+ .filter(d => typeof d === 'number')
1969
+ .sort((a, b) => a - b)
1970
+ const median_ms = durations.length > 0
1971
+ ? durations[Math.floor(durations.length / 2)]
1972
+ : null
1973
+ const rubber_stamp_count = durations.filter(d => d < rubberStampMs).length
1974
+ const rubber_stamp_pct = n > 0 ? (rubber_stamp_count / n) * 100 : 0
1975
+
1976
+ rows.push({
1977
+ gate_id,
1978
+ logged_decisions: n,
1979
+ approve_pct: n > 0 ? (approve / n) * 100 : 0,
1980
+ deny_pct: n > 0 ? (deny / n) * 100 : 0,
1981
+ interrupt_pct: n > 0 ? (interrupt / n) * 100 : 0,
1982
+ median_ms,
1983
+ rubber_stamp_pct,
1984
+ })
1985
+ }
1986
+
1987
+ return rows.sort((a, b) => a.gate_id.localeCompare(b.gate_id))
1988
+ }