@smartmemory/compose 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1014 -0
  3. package/bin/compose.js +1515 -0
  4. package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
  5. package/dist/assets/arc-SxJ2J1sh.js +1 -0
  6. package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
  7. package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
  8. package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
  9. package/dist/assets/channel-DGElom1e.js +1 -0
  10. package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
  11. package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
  12. package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
  13. package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
  14. package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
  15. package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
  16. package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
  17. package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
  18. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
  19. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
  20. package/dist/assets/clone-DUJKJXd7.js +1 -0
  21. package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
  22. package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
  23. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  24. package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
  25. package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
  26. package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
  27. package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
  28. package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
  29. package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
  30. package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
  31. package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
  32. package/dist/assets/graph-D0Cfv00Y.js +1 -0
  33. package/dist/assets/index-CUd6pFGF.css +1 -0
  34. package/dist/assets/index-DReRlzZI.js +1144 -0
  35. package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
  36. package/dist/assets/init-Gi6I4Gst.js +1 -0
  37. package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
  38. package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
  39. package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
  40. package/dist/assets/katex-DkKDou_j.js +257 -0
  41. package/dist/assets/layout-Bj72wOEB.js +1 -0
  42. package/dist/assets/linear-BRFo114D.js +1 -0
  43. package/dist/assets/min-GCHnKlJS.js +1 -0
  44. package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
  45. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  46. package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
  47. package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
  48. package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
  49. package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
  50. package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
  51. package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
  52. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
  53. package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
  54. package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
  55. package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
  56. package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
  57. package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
  58. package/dist/index.html +30 -0
  59. package/lib/agent-chains.js +65 -0
  60. package/lib/agent-string.js +86 -0
  61. package/lib/budget-ledger.js +86 -0
  62. package/lib/build-all.js +162 -0
  63. package/lib/build-dag.js +120 -0
  64. package/lib/build-stream-writer.js +190 -0
  65. package/lib/build.js +2997 -0
  66. package/lib/capability-checker.js +53 -0
  67. package/lib/cert-inject.js +38 -0
  68. package/lib/cli-progress.js +483 -0
  69. package/lib/constants.js +69 -0
  70. package/lib/cross-layer-audit.js +84 -0
  71. package/lib/debug-discipline.js +173 -0
  72. package/lib/feature-json.js +106 -0
  73. package/lib/gate-prompt.js +291 -0
  74. package/lib/gate-tiers.js +194 -0
  75. package/lib/health-history.js +119 -0
  76. package/lib/health-score.js +227 -0
  77. package/lib/ideabox.js +570 -0
  78. package/lib/import.js +244 -0
  79. package/lib/migrate-roadmap.js +94 -0
  80. package/lib/model-pricing.js +67 -0
  81. package/lib/new.js +413 -0
  82. package/lib/pipeline-cli.js +489 -0
  83. package/lib/plan-parser.js +103 -0
  84. package/lib/qa-scoping.js +474 -0
  85. package/lib/questionnaire.js +200 -0
  86. package/lib/resolve-port.js +7 -0
  87. package/lib/result-normalizer.js +349 -0
  88. package/lib/review-lenses.js +166 -0
  89. package/lib/roadmap-gen.js +210 -0
  90. package/lib/roadmap-parser.js +176 -0
  91. package/lib/server-probe.js +23 -0
  92. package/lib/staleness.js +87 -0
  93. package/lib/step-prompt.js +260 -0
  94. package/lib/step-validator.js +49 -0
  95. package/lib/stratum-mcp-client.js +365 -0
  96. package/lib/team-flag.js +46 -0
  97. package/lib/test-bootstrap.js +401 -0
  98. package/lib/triage.js +274 -0
  99. package/lib/vision-writer.js +391 -0
  100. package/package.json +111 -0
  101. package/pipelines/bug-fix.stratum.yaml +230 -0
  102. package/pipelines/build.stratum.yaml +498 -0
  103. package/pipelines/content.stratum.yaml +112 -0
  104. package/pipelines/coverage-sweep.stratum.yaml +52 -0
  105. package/pipelines/refactor.stratum.yaml +169 -0
  106. package/pipelines/research.stratum.yaml +88 -0
  107. package/pipelines/review-fix.stratum.yaml +109 -0
  108. package/presets/team-feature.stratum.yaml +105 -0
  109. package/presets/team-research.stratum.yaml +108 -0
  110. package/presets/team-review.stratum.yaml +106 -0
  111. package/scripts/agent-activity-hook.sh +31 -0
  112. package/scripts/agent-error-hook.sh +28 -0
  113. package/scripts/analyze-orphans.mjs +50 -0
  114. package/scripts/find-orphans.mjs +26 -0
  115. package/scripts/fix-phases.mjs +49 -0
  116. package/scripts/generate-stratum-spec.mjs +137 -0
  117. package/scripts/import-roadmap.mjs +116 -0
  118. package/scripts/phase-audit.mjs +33 -0
  119. package/scripts/run-pipeline.mjs +314 -0
  120. package/scripts/session-end-hook.sh +18 -0
  121. package/scripts/session-start-hook.sh +38 -0
  122. package/scripts/vision-hook.sh +104 -0
  123. package/scripts/vision-track.mjs +554 -0
  124. package/scripts/wire-all-orphans.mjs +108 -0
  125. package/scripts/wire-orphans.mjs +164 -0
  126. package/server/activity-routes.js +123 -0
  127. package/server/agent-health.js +197 -0
  128. package/server/agent-hooks.js +102 -0
  129. package/server/agent-mcp.js +10 -0
  130. package/server/agent-registry.js +95 -0
  131. package/server/agent-server.js +290 -0
  132. package/server/agent-spawn.js +251 -0
  133. package/server/agent-templates.js +77 -0
  134. package/server/artifact-manager.js +247 -0
  135. package/server/artifact-templates/architecture.md +28 -0
  136. package/server/artifact-templates/blueprint.md +21 -0
  137. package/server/artifact-templates/design.md +36 -0
  138. package/server/artifact-templates/plan.md +25 -0
  139. package/server/artifact-templates/prd.md +43 -0
  140. package/server/artifact-templates/report.md +40 -0
  141. package/server/block-tracker.js +90 -0
  142. package/server/build-stream-bridge.js +502 -0
  143. package/server/coalescing-buffer.js +46 -0
  144. package/server/compose-mcp-tools.js +479 -0
  145. package/server/compose-mcp.js +324 -0
  146. package/server/connectors/agent-connector.js +78 -0
  147. package/server/connectors/claude-sdk-connector.js +198 -0
  148. package/server/connectors/codex-connector.js +240 -0
  149. package/server/connectors/connector-discovery.js +18 -0
  150. package/server/connectors/connector-runtime.js +13 -0
  151. package/server/connectors/opencode-connector.js +200 -0
  152. package/server/design-routes.js +540 -0
  153. package/server/design-session.js +161 -0
  154. package/server/feature-scan.js +593 -0
  155. package/server/file-watcher.js +284 -0
  156. package/server/find-root.js +29 -0
  157. package/server/graph-export.js +343 -0
  158. package/server/ideabox-cache.js +77 -0
  159. package/server/ideabox-routes.js +294 -0
  160. package/server/index.js +156 -0
  161. package/server/model-tiers.js +49 -0
  162. package/server/pipeline-routes.js +288 -0
  163. package/server/policy-evaluator.js +36 -0
  164. package/server/project-root.js +122 -0
  165. package/server/security.js +23 -0
  166. package/server/session-manager.js +403 -0
  167. package/server/session-routes.js +190 -0
  168. package/server/session-store.js +107 -0
  169. package/server/settings-routes.js +35 -0
  170. package/server/settings-store.js +234 -0
  171. package/server/stratum-api.js +102 -0
  172. package/server/stratum-client.js +192 -0
  173. package/server/stratum-sync.js +193 -0
  174. package/server/summarizer.js +139 -0
  175. package/server/supervisor.js +196 -0
  176. package/server/vision-routes.js +668 -0
  177. package/server/vision-server.js +393 -0
  178. package/server/vision-store.js +360 -0
  179. package/server/vision-utils.js +179 -0
  180. package/server/worktree-gc.js +137 -0
  181. package/templates/ROADMAP.md +46 -0
@@ -0,0 +1,489 @@
1
+ /**
2
+ * compose pipeline — view and edit build.stratum.yaml
3
+ *
4
+ * Subcommands:
5
+ * show Print the current pipeline
6
+ * set <step> --agent <agent> Change a step's agent
7
+ * set <step> --mode gate Convert step to a human gate
8
+ * set <step> --mode review Convert step to a codex review sub-flow
9
+ * set <step> --mode agent Convert step back to a regular agent step
10
+ * add --id <id> --after <step> --agent <agent> --intent <intent> Insert a step
11
+ * remove <step> Remove a step
12
+ * enable <steps...> Enable skipped steps (remove skip_if)
13
+ * disable <steps...> Disable steps (set skip_if: "true")
14
+ */
15
+ import { readFileSync, writeFileSync, existsSync } from 'fs'
16
+ import { join } from 'path'
17
+ import { parse, stringify } from 'yaml'
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Helpers
21
+ // ---------------------------------------------------------------------------
22
+
23
+ function loadSpec(cwd) {
24
+ const specPath = join(cwd, 'pipelines', 'build.stratum.yaml')
25
+ if (!existsSync(specPath)) {
26
+ throw new Error(`No pipeline found at ${specPath}. Run 'compose init' first.`)
27
+ }
28
+ return { specPath, spec: parse(readFileSync(specPath, 'utf-8')) }
29
+ }
30
+
31
+ function saveSpec(specPath, spec) {
32
+ writeFileSync(specPath, stringify(spec, { lineWidth: 120 }))
33
+ }
34
+
35
+ function findStep(steps, stepId) {
36
+ const idx = steps.findIndex(s => s.id === stepId)
37
+ if (idx === -1) throw new Error(`Step "${stepId}" not found in pipeline.`)
38
+ return { step: steps[idx], idx }
39
+ }
40
+
41
+ function findFlow(spec, flowName) {
42
+ return spec.flows?.[flowName]
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // show
47
+ // ---------------------------------------------------------------------------
48
+
49
+ const LEVEL_COLORS = {
50
+ gate: '\x1b[33m', // yellow
51
+ skip: '\x1b[90m', // gray
52
+ flow: '\x1b[36m', // cyan
53
+ agent: '\x1b[32m', // green
54
+ }
55
+ const RESET = '\x1b[0m'
56
+
57
+ export function pipelineShow(cwd) {
58
+ const { spec } = loadSpec(cwd)
59
+ const mainFlow = spec.flows?.build
60
+ if (!mainFlow) throw new Error('No "build" flow found in pipeline spec.')
61
+
62
+ console.log(`\n Pipeline: build (${mainFlow.steps.length} steps)\n`)
63
+
64
+ for (const step of mainFlow.steps) {
65
+ const isGate = !!step.function
66
+ const isFlow = !!step.flow
67
+ const isSkipped = step.skip_if === 'true' || step.skip_if === true
68
+ const agent = step.agent ?? (isFlow ? flowAgent(spec, step.flow) : null)
69
+
70
+ let kind, color, detail
71
+ if (isSkipped) {
72
+ kind = 'skip'
73
+ color = LEVEL_COLORS.skip
74
+ detail = step.skip_reason || 'skipped'
75
+ } else if (isGate) {
76
+ kind = 'gate'
77
+ color = LEVEL_COLORS.gate
78
+ detail = `human gate (timeout: ${gateTimeout(spec, step.function)}s)`
79
+ } else if (isFlow) {
80
+ kind = 'flow'
81
+ color = LEVEL_COLORS.flow
82
+ const subFlow = findFlow(spec, step.flow)
83
+ const subSteps = subFlow?.steps?.map(s => s.id).join(' → ') || '?'
84
+ detail = `${step.flow}: ${subSteps} (agent: ${agent})`
85
+ } else {
86
+ kind = 'agent'
87
+ color = LEVEL_COLORS.agent
88
+ const ensures = step.ensure?.length ? ` [${step.ensure.length} ensures]` : ''
89
+ const retries = step.retries ? ` (retries: ${step.retries})` : ''
90
+ const onFail = step.on_fail ? ` → on_fail: ${step.on_fail}` : ''
91
+ detail = `agent: ${agent}${ensures}${retries}${onFail}`
92
+ }
93
+
94
+ const num = String(mainFlow.steps.indexOf(step) + 1).padStart(2)
95
+ console.log(` ${color}${num}. ${step.id.padEnd(18)}${kind.padEnd(6)} ${detail}${RESET}`)
96
+ }
97
+
98
+ // Show sub-flows
99
+ const subFlowNames = mainFlow.steps.filter(s => s.flow).map(s => s.flow)
100
+ if (subFlowNames.length > 0) {
101
+ console.log(`\n Sub-flows:`)
102
+ for (const name of subFlowNames) {
103
+ const flow = findFlow(spec, name)
104
+ if (!flow) continue
105
+ console.log(`\n ${name}:`)
106
+ for (const step of flow.steps) {
107
+ const ensures = step.ensure?.length ? ` [${step.ensure.join(', ')}]` : ''
108
+ const retries = step.retries ? ` (retries: ${step.retries})` : ''
109
+ console.log(` - ${step.id} (${step.agent})${ensures}${retries}`)
110
+ }
111
+ }
112
+ }
113
+
114
+ // Show contracts
115
+ if (spec.contracts) {
116
+ console.log(`\n Contracts: ${Object.keys(spec.contracts).join(', ')}`)
117
+ }
118
+
119
+ console.log('')
120
+ }
121
+
122
+ function flowAgent(spec, flowName) {
123
+ const flow = spec.flows?.[flowName]
124
+ if (!flow?.steps?.length) return '?'
125
+ return flow.steps[0].agent ?? 'claude'
126
+ }
127
+
128
+ function gateTimeout(spec, funcName) {
129
+ return spec.functions?.[funcName]?.timeout ?? '?'
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // set
134
+ // ---------------------------------------------------------------------------
135
+
136
+ export function pipelineSet(cwd, stepId, flags) {
137
+ const { specPath, spec } = loadSpec(cwd)
138
+ const mainFlow = spec.flows?.build
139
+ if (!mainFlow) throw new Error('No "build" flow found.')
140
+
141
+ const { step, idx } = findStep(mainFlow.steps, stepId)
142
+
143
+ // --agent <agent>
144
+ const agentIdx = flags.indexOf('--agent')
145
+ if (agentIdx !== -1) {
146
+ const agent = flags[agentIdx + 1]
147
+ if (!agent) throw new Error('--agent requires a value (claude, codex, gemini)')
148
+ if (step.flow) {
149
+ // Change the agent inside the sub-flow
150
+ const flow = findFlow(spec, step.flow)
151
+ if (flow?.steps?.length) {
152
+ flow.steps[0].agent = agent
153
+ console.log(`Set ${step.flow} → ${flow.steps[0].id} agent to ${agent}`)
154
+ }
155
+ } else if (step.function) {
156
+ throw new Error(`"${stepId}" is a gate — gates don't have agents. Use --mode to change it.`)
157
+ } else {
158
+ step.agent = agent
159
+ console.log(`Set ${stepId} agent to ${agent}`)
160
+ }
161
+ }
162
+
163
+ // --mode gate|review|agent
164
+ const modeIdx = flags.indexOf('--mode')
165
+ if (modeIdx !== -1) {
166
+ const mode = flags[modeIdx + 1]
167
+ if (!mode) throw new Error('--mode requires a value (gate, review, agent)')
168
+
169
+ if (mode === 'gate') {
170
+ convertToGate(spec, mainFlow, step, stepId)
171
+ } else if (mode === 'review') {
172
+ convertToReview(spec, mainFlow, step, stepId)
173
+ } else if (mode === 'agent') {
174
+ convertToAgent(spec, mainFlow, step, stepId)
175
+ } else {
176
+ throw new Error(`Unknown mode "${mode}". Use: gate, review, agent`)
177
+ }
178
+ }
179
+
180
+ // --retries <n>
181
+ const retriesIdx = flags.indexOf('--retries')
182
+ if (retriesIdx !== -1) {
183
+ const n = parseInt(flags[retriesIdx + 1], 10)
184
+ if (isNaN(n)) throw new Error('--retries requires a number')
185
+ if (step.flow) {
186
+ const flow = findFlow(spec, step.flow)
187
+ if (flow?.steps?.length) flow.steps[0].retries = n
188
+ } else {
189
+ step.retries = n
190
+ }
191
+ console.log(`Set ${stepId} retries to ${n}`)
192
+ }
193
+
194
+ saveSpec(specPath, spec)
195
+ }
196
+
197
+ function convertToGate(spec, mainFlow, step, stepId) {
198
+ const funcName = `${stepId}_gate`
199
+ // Add gate function if missing
200
+ spec.functions = spec.functions || {}
201
+ if (!spec.functions[funcName]) {
202
+ spec.functions[funcName] = { mode: 'gate', timeout: 3600 }
203
+ }
204
+
205
+ // Find the next step id for on_approve
206
+ const idx = mainFlow.steps.indexOf(step)
207
+ const nextStep = mainFlow.steps[idx + 1]
208
+
209
+ // Find previous agent step for on_revise
210
+ let prevAgent = null
211
+ for (let i = idx - 1; i >= 0; i--) {
212
+ if (mainFlow.steps[i].agent && !mainFlow.steps[i].skip_if) {
213
+ prevAgent = mainFlow.steps[i].id
214
+ break
215
+ }
216
+ }
217
+
218
+ // Replace step properties
219
+ const deps = step.depends_on
220
+ Object.keys(step).forEach(k => delete step[k])
221
+ step.id = stepId
222
+ step.function = funcName
223
+ step.on_approve = nextStep?.id || null
224
+ step.on_revise = prevAgent
225
+ step.on_kill = null
226
+ if (deps) step.depends_on = deps
227
+
228
+ console.log(`Converted ${stepId} to human gate`)
229
+ }
230
+
231
+ function convertToReview(spec, mainFlow, step, stepId) {
232
+ const flowName = `${stepId}_review`
233
+
234
+ // Create review sub-flow if missing
235
+ if (!spec.flows[flowName]) {
236
+ spec.flows[flowName] = {
237
+ input: { task: { type: 'string' } },
238
+ output: 'ReviewResult',
239
+ steps: [{
240
+ id: 'review',
241
+ agent: 'codex',
242
+ intent: `Review the output of the ${stepId} step. Return { "clean": boolean, "summary": string, "findings": string[] }.`,
243
+ inputs: { task: '$.input.task' },
244
+ output_contract: 'ReviewResult',
245
+ ensure: ['result.clean == True'],
246
+ retries: 5,
247
+ }],
248
+ }
249
+ }
250
+
251
+ // Ensure ReviewResult contract exists
252
+ spec.contracts = spec.contracts || {}
253
+ if (!spec.contracts.ReviewResult) {
254
+ spec.contracts.ReviewResult = {
255
+ clean: { type: 'boolean' },
256
+ summary: { type: 'string' },
257
+ findings: { type: 'array' },
258
+ }
259
+ }
260
+
261
+ // Find the previous step for input reference
262
+ const idx = mainFlow.steps.indexOf(step)
263
+ let prevStepId = null
264
+ for (let i = idx - 1; i >= 0; i--) {
265
+ if (mainFlow.steps[i].agent && !mainFlow.steps[i].skip_if) {
266
+ prevStepId = mainFlow.steps[i].id
267
+ break
268
+ }
269
+ }
270
+
271
+ // Replace step properties
272
+ const deps = step.depends_on
273
+ Object.keys(step).forEach(k => delete step[k])
274
+ step.id = stepId
275
+ step.flow = flowName
276
+ step.inputs = { task: prevStepId ? `$.steps.${prevStepId}.output.summary` : '$.input.description' }
277
+ step.ensure = ['result.clean == True']
278
+ if (deps) step.depends_on = deps
279
+
280
+ console.log(`Converted ${stepId} to codex review loop (flow: ${flowName})`)
281
+ }
282
+
283
+ function convertToAgent(spec, mainFlow, step, stepId) {
284
+ const deps = step.depends_on
285
+ const wasFlow = step.flow
286
+ const wasGate = step.function
287
+
288
+ Object.keys(step).forEach(k => delete step[k])
289
+ step.id = stepId
290
+ step.agent = 'claude'
291
+ step.intent = `Execute the ${stepId} phase.`
292
+ step.inputs = {
293
+ featureCode: '$.input.featureCode',
294
+ description: '$.input.description',
295
+ }
296
+ step.output_contract = 'PhaseResult'
297
+ step.retries = 2
298
+ if (deps) step.depends_on = deps
299
+
300
+ const from = wasFlow ? `flow:${wasFlow}` : wasGate ? `gate:${wasGate}` : 'unknown'
301
+ console.log(`Converted ${stepId} to agent step (from ${from})`)
302
+ }
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // add
306
+ // ---------------------------------------------------------------------------
307
+
308
+ export function pipelineAdd(cwd, flags) {
309
+ const { specPath, spec } = loadSpec(cwd)
310
+ const mainFlow = spec.flows?.build
311
+ if (!mainFlow) throw new Error('No "build" flow found.')
312
+
313
+ const id = flagVal(flags, '--id')
314
+ const after = flagVal(flags, '--after')
315
+ const agent = flagVal(flags, '--agent') || 'claude'
316
+ const intent = flagVal(flags, '--intent') || `Execute the ${id} step.`
317
+
318
+ if (!id) throw new Error('--id is required')
319
+ if (!after) throw new Error('--after is required')
320
+
321
+ // Check id doesn't already exist
322
+ if (mainFlow.steps.some(s => s.id === id)) {
323
+ throw new Error(`Step "${id}" already exists.`)
324
+ }
325
+
326
+ const { idx } = findStep(mainFlow.steps, after)
327
+
328
+ const newStep = {
329
+ id,
330
+ agent,
331
+ intent,
332
+ inputs: {
333
+ featureCode: '$.input.featureCode',
334
+ description: '$.input.description',
335
+ },
336
+ output_contract: 'PhaseResult',
337
+ retries: 2,
338
+ depends_on: [after],
339
+ }
340
+
341
+ // Fix depends_on of the step that previously depended on `after`
342
+ const nextStep = mainFlow.steps[idx + 1]
343
+ if (nextStep?.depends_on?.includes(after)) {
344
+ nextStep.depends_on = nextStep.depends_on.map(d => d === after ? id : d)
345
+ }
346
+
347
+ mainFlow.steps.splice(idx + 1, 0, newStep)
348
+ saveSpec(specPath, spec)
349
+ console.log(`Added step "${id}" after "${after}" (agent: ${agent})`)
350
+ }
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // remove
354
+ // ---------------------------------------------------------------------------
355
+
356
+ export function pipelineRemove(cwd, stepId) {
357
+ const { specPath, spec } = loadSpec(cwd)
358
+ const mainFlow = spec.flows?.build
359
+ if (!mainFlow) throw new Error('No "build" flow found.')
360
+
361
+ const { step, idx } = findStep(mainFlow.steps, stepId)
362
+
363
+ // Rewire depends_on: anything depending on this step now depends on its deps
364
+ const removedDeps = step.depends_on || []
365
+ for (const s of mainFlow.steps) {
366
+ if (s.depends_on?.includes(stepId)) {
367
+ s.depends_on = s.depends_on
368
+ .filter(d => d !== stepId)
369
+ .concat(removedDeps)
370
+ // Deduplicate
371
+ s.depends_on = [...new Set(s.depends_on)]
372
+ }
373
+ }
374
+
375
+ // Rewire gate references
376
+ for (const s of mainFlow.steps) {
377
+ if (s.on_approve === stepId) s.on_approve = mainFlow.steps[idx + 1]?.id || null
378
+ if (s.on_revise === stepId) s.on_revise = null
379
+ if (s.on_fail === stepId) s.on_fail = null
380
+ }
381
+
382
+ mainFlow.steps.splice(idx, 1)
383
+ saveSpec(specPath, spec)
384
+ console.log(`Removed step "${stepId}"`)
385
+ }
386
+
387
+ // ---------------------------------------------------------------------------
388
+ // enable / disable
389
+ // ---------------------------------------------------------------------------
390
+
391
+ export function pipelineEnable(cwd, stepIds) {
392
+ const { specPath, spec } = loadSpec(cwd)
393
+ const mainFlow = spec.flows?.build
394
+ if (!mainFlow) throw new Error('No "build" flow found.')
395
+
396
+ for (const stepId of stepIds) {
397
+ const { step } = findStep(mainFlow.steps, stepId)
398
+ delete step.skip_if
399
+ delete step.skip_reason
400
+ console.log(`Enabled ${stepId}`)
401
+ }
402
+
403
+ saveSpec(specPath, spec)
404
+ }
405
+
406
+ export function pipelineDisable(cwd, stepIds) {
407
+ const { specPath, spec } = loadSpec(cwd)
408
+ const mainFlow = spec.flows?.build
409
+ if (!mainFlow) throw new Error('No "build" flow found.')
410
+
411
+ for (const stepId of stepIds) {
412
+ const { step } = findStep(mainFlow.steps, stepId)
413
+ step.skip_if = 'true'
414
+ step.skip_reason = `Disabled via compose pipeline disable`
415
+ console.log(`Disabled ${stepId} (skip_if: "true")`)
416
+ }
417
+
418
+ saveSpec(specPath, spec)
419
+ }
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // Util
423
+ // ---------------------------------------------------------------------------
424
+
425
+ function flagVal(flags, name) {
426
+ const idx = flags.indexOf(name)
427
+ return idx !== -1 ? flags[idx + 1] : null
428
+ }
429
+
430
+ // ---------------------------------------------------------------------------
431
+ // CLI dispatch
432
+ // ---------------------------------------------------------------------------
433
+
434
+ export function runPipelineCli(cwd, subArgs) {
435
+ const sub = subArgs[0]
436
+ const rest = subArgs.slice(1)
437
+
438
+ if (!sub || sub === '--help') {
439
+ printHelp()
440
+ return
441
+ }
442
+
443
+ switch (sub) {
444
+ case 'show':
445
+ pipelineShow(cwd)
446
+ break
447
+ case 'set':
448
+ if (!rest[0]) throw new Error('Usage: compose pipeline set <step-id> --agent <agent> | --mode <mode>')
449
+ pipelineSet(cwd, rest[0], rest.slice(1))
450
+ break
451
+ case 'add':
452
+ pipelineAdd(cwd, rest)
453
+ break
454
+ case 'remove':
455
+ if (!rest[0]) throw new Error('Usage: compose pipeline remove <step-id>')
456
+ pipelineRemove(cwd, rest[0])
457
+ break
458
+ case 'enable':
459
+ if (!rest.length) throw new Error('Usage: compose pipeline enable <step-id> [step-id...]')
460
+ pipelineEnable(cwd, rest)
461
+ break
462
+ case 'disable':
463
+ if (!rest.length) throw new Error('Usage: compose pipeline disable <step-id> [step-id...]')
464
+ pipelineDisable(cwd, rest)
465
+ break
466
+ default:
467
+ console.error(`Unknown pipeline subcommand: ${sub}`)
468
+ printHelp()
469
+ process.exit(1)
470
+ }
471
+ }
472
+
473
+ function printHelp() {
474
+ console.log(`
475
+ Usage: compose pipeline <command>
476
+
477
+ Commands:
478
+ show Print the current pipeline
479
+ set <step> --agent <agent> Change a step's agent
480
+ set <step> --mode gate Convert to human gate
481
+ set <step> --mode review Convert to codex review loop
482
+ set <step> --mode agent Convert to regular agent step
483
+ set <step> --retries <n> Set retry count
484
+ add --id <id> --after <step> [opts] Insert a new step
485
+ remove <step> Remove a step
486
+ enable <steps...> Enable skipped steps
487
+ disable <steps...> Disable steps (skip)
488
+ `)
489
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * plan-parser.js — agent-side helper for extracting acceptance criteria from plan.md.
3
+ *
4
+ * Used by the ship step agent to populate plan_items before calling step_done.
5
+ * NOT called by the ensure function — the ensure function receives precomputed data.
6
+ */
7
+
8
+ /**
9
+ * Heuristic patterns for marking an item as critical.
10
+ * Items matching any of these are flagged critical: true.
11
+ */
12
+ const CRITICAL_PATTERNS = [
13
+ /\bMUST\b/,
14
+ /\brequired\b/i,
15
+ /\bsecurity\b/i,
16
+ /\bauth\b/i,
17
+ /\bcrypto\b/i,
18
+ /\btest\b/i,
19
+ /\btests\b/i,
20
+ ];
21
+
22
+ /**
23
+ * parsePlanItems(planMarkdown) → Array<{ text, file, critical }>
24
+ *
25
+ * Extracts checkbox items from plan.md markdown.
26
+ * Handles both unchecked `- [ ]` and checked `- [x]` / `- [X]` lines.
27
+ *
28
+ * @param {string} planMarkdown - Raw markdown content of a plan.md file.
29
+ * @returns {Array<{text: string, file: string|null, critical: boolean}>}
30
+ */
31
+ function parsePlanItems(planMarkdown) {
32
+ if (!planMarkdown || typeof planMarkdown !== 'string') {
33
+ return [];
34
+ }
35
+
36
+ const items = [];
37
+ const lines = planMarkdown.split('\n');
38
+
39
+ for (const line of lines) {
40
+ // Match checkbox lines: `- [ ] text` or `- [x] text` or `- [X] text`
41
+ const match = line.match(/^\s*-\s+\[[ xX]\]\s+(.+)$/);
42
+ if (!match) continue;
43
+
44
+ const text = match[1].trim();
45
+
46
+ // Extract the first backtick-quoted file path reference
47
+ const fileMatch = text.match(/`([^`]+\.[a-zA-Z0-9]+[^`]*)`/);
48
+ const file = fileMatch ? fileMatch[1] : null;
49
+
50
+ // Determine criticality
51
+ const critical = CRITICAL_PATTERNS.some(pattern => pattern.test(text));
52
+
53
+ items.push({ text, file, critical });
54
+ }
55
+
56
+ return items;
57
+ }
58
+
59
+ /**
60
+ * matchItemsToDiff(planItems, filesChanged) → { done, missing, extra }
61
+ *
62
+ * Classifies plan items against the set of files that changed in the diff.
63
+ *
64
+ * - done: plan items whose file reference appears in filesChanged
65
+ * - missing: plan items with a file reference NOT in filesChanged
66
+ * - extra: files in filesChanged not mentioned in any plan item (scope creep)
67
+ *
68
+ * Items without a file reference are treated as done (unverifiable, assumed complete).
69
+ *
70
+ * @param {Array<{text: string, file: string|null, critical: boolean}>} planItems
71
+ * @param {string[]} filesChanged - List of file paths touched in the diff.
72
+ * @returns {{ done: Array, missing: Array, extra: string[] }}
73
+ */
74
+ function matchItemsToDiff(planItems, filesChanged) {
75
+ const changedSet = new Set(filesChanged || []);
76
+ const mentionedFiles = new Set();
77
+
78
+ const done = [];
79
+ const missing = [];
80
+
81
+ for (const item of planItems) {
82
+ if (!item.file) {
83
+ // No file reference — treat as done
84
+ done.push(item);
85
+ continue;
86
+ }
87
+
88
+ mentionedFiles.add(item.file);
89
+
90
+ if (changedSet.has(item.file)) {
91
+ done.push(item);
92
+ } else {
93
+ missing.push({ ...item, critical: item.critical ?? false });
94
+ }
95
+ }
96
+
97
+ // Extra: files changed that no plan item mentions (scope creep)
98
+ const extra = [...changedSet].filter(f => !mentionedFiles.has(f));
99
+
100
+ return { done, missing, extra };
101
+ }
102
+
103
+ export { parsePlanItems, matchItemsToDiff };