@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.
- package/LICENSE +21 -0
- package/README.md +1014 -0
- package/bin/compose.js +1515 -0
- package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
- package/dist/assets/arc-SxJ2J1sh.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
- package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
- package/dist/assets/channel-DGElom1e.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
- package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
- package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
- package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
- package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
- package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
- package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
- package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
- package/dist/assets/clone-DUJKJXd7.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
- package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
- package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
- package/dist/assets/graph-D0Cfv00Y.js +1 -0
- package/dist/assets/index-CUd6pFGF.css +1 -0
- package/dist/assets/index-DReRlzZI.js +1144 -0
- package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
- package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
- package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-Bj72wOEB.js +1 -0
- package/dist/assets/linear-BRFo114D.js +1 -0
- package/dist/assets/min-GCHnKlJS.js +1 -0
- package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
- package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
- package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
- package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
- package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
- package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
- package/dist/index.html +30 -0
- package/lib/agent-chains.js +65 -0
- package/lib/agent-string.js +86 -0
- package/lib/budget-ledger.js +86 -0
- package/lib/build-all.js +162 -0
- package/lib/build-dag.js +120 -0
- package/lib/build-stream-writer.js +190 -0
- package/lib/build.js +2997 -0
- package/lib/capability-checker.js +53 -0
- package/lib/cert-inject.js +38 -0
- package/lib/cli-progress.js +483 -0
- package/lib/constants.js +69 -0
- package/lib/cross-layer-audit.js +84 -0
- package/lib/debug-discipline.js +173 -0
- package/lib/feature-json.js +106 -0
- package/lib/gate-prompt.js +291 -0
- package/lib/gate-tiers.js +194 -0
- package/lib/health-history.js +119 -0
- package/lib/health-score.js +227 -0
- package/lib/ideabox.js +570 -0
- package/lib/import.js +244 -0
- package/lib/migrate-roadmap.js +94 -0
- package/lib/model-pricing.js +67 -0
- package/lib/new.js +413 -0
- package/lib/pipeline-cli.js +489 -0
- package/lib/plan-parser.js +103 -0
- package/lib/qa-scoping.js +474 -0
- package/lib/questionnaire.js +200 -0
- package/lib/resolve-port.js +7 -0
- package/lib/result-normalizer.js +349 -0
- package/lib/review-lenses.js +166 -0
- package/lib/roadmap-gen.js +210 -0
- package/lib/roadmap-parser.js +176 -0
- package/lib/server-probe.js +23 -0
- package/lib/staleness.js +87 -0
- package/lib/step-prompt.js +260 -0
- package/lib/step-validator.js +49 -0
- package/lib/stratum-mcp-client.js +365 -0
- package/lib/team-flag.js +46 -0
- package/lib/test-bootstrap.js +401 -0
- package/lib/triage.js +274 -0
- package/lib/vision-writer.js +391 -0
- package/package.json +111 -0
- package/pipelines/bug-fix.stratum.yaml +230 -0
- package/pipelines/build.stratum.yaml +498 -0
- package/pipelines/content.stratum.yaml +112 -0
- package/pipelines/coverage-sweep.stratum.yaml +52 -0
- package/pipelines/refactor.stratum.yaml +169 -0
- package/pipelines/research.stratum.yaml +88 -0
- package/pipelines/review-fix.stratum.yaml +109 -0
- package/presets/team-feature.stratum.yaml +105 -0
- package/presets/team-research.stratum.yaml +108 -0
- package/presets/team-review.stratum.yaml +106 -0
- package/scripts/agent-activity-hook.sh +31 -0
- package/scripts/agent-error-hook.sh +28 -0
- package/scripts/analyze-orphans.mjs +50 -0
- package/scripts/find-orphans.mjs +26 -0
- package/scripts/fix-phases.mjs +49 -0
- package/scripts/generate-stratum-spec.mjs +137 -0
- package/scripts/import-roadmap.mjs +116 -0
- package/scripts/phase-audit.mjs +33 -0
- package/scripts/run-pipeline.mjs +314 -0
- package/scripts/session-end-hook.sh +18 -0
- package/scripts/session-start-hook.sh +38 -0
- package/scripts/vision-hook.sh +104 -0
- package/scripts/vision-track.mjs +554 -0
- package/scripts/wire-all-orphans.mjs +108 -0
- package/scripts/wire-orphans.mjs +164 -0
- package/server/activity-routes.js +123 -0
- package/server/agent-health.js +197 -0
- package/server/agent-hooks.js +102 -0
- package/server/agent-mcp.js +10 -0
- package/server/agent-registry.js +95 -0
- package/server/agent-server.js +290 -0
- package/server/agent-spawn.js +251 -0
- package/server/agent-templates.js +77 -0
- package/server/artifact-manager.js +247 -0
- package/server/artifact-templates/architecture.md +28 -0
- package/server/artifact-templates/blueprint.md +21 -0
- package/server/artifact-templates/design.md +36 -0
- package/server/artifact-templates/plan.md +25 -0
- package/server/artifact-templates/prd.md +43 -0
- package/server/artifact-templates/report.md +40 -0
- package/server/block-tracker.js +90 -0
- package/server/build-stream-bridge.js +502 -0
- package/server/coalescing-buffer.js +46 -0
- package/server/compose-mcp-tools.js +479 -0
- package/server/compose-mcp.js +324 -0
- package/server/connectors/agent-connector.js +78 -0
- package/server/connectors/claude-sdk-connector.js +198 -0
- package/server/connectors/codex-connector.js +240 -0
- package/server/connectors/connector-discovery.js +18 -0
- package/server/connectors/connector-runtime.js +13 -0
- package/server/connectors/opencode-connector.js +200 -0
- package/server/design-routes.js +540 -0
- package/server/design-session.js +161 -0
- package/server/feature-scan.js +593 -0
- package/server/file-watcher.js +284 -0
- package/server/find-root.js +29 -0
- package/server/graph-export.js +343 -0
- package/server/ideabox-cache.js +77 -0
- package/server/ideabox-routes.js +294 -0
- package/server/index.js +156 -0
- package/server/model-tiers.js +49 -0
- package/server/pipeline-routes.js +288 -0
- package/server/policy-evaluator.js +36 -0
- package/server/project-root.js +122 -0
- package/server/security.js +23 -0
- package/server/session-manager.js +403 -0
- package/server/session-routes.js +190 -0
- package/server/session-store.js +107 -0
- package/server/settings-routes.js +35 -0
- package/server/settings-store.js +234 -0
- package/server/stratum-api.js +102 -0
- package/server/stratum-client.js +192 -0
- package/server/stratum-sync.js +193 -0
- package/server/summarizer.js +139 -0
- package/server/supervisor.js +196 -0
- package/server/vision-routes.js +668 -0
- package/server/vision-server.js +393 -0
- package/server/vision-store.js +360 -0
- package/server/vision-utils.js +179 -0
- package/server/worktree-gc.js +137 -0
- 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 };
|