@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,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step Prompt Builder — constructs agent prompts from Stratum step dispatch responses.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { checkStaleness } from './staleness.js';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Ambient context cache — loaded once per build, keyed by contextDir path.
|
|
11
|
+
// Cleared between builds by passing context.contextDir on first call.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const _contextCache = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load and concatenate all .md files from docs/context/ (or the configured
|
|
18
|
+
* contextDir). Returns the combined text or null if the directory is absent.
|
|
19
|
+
* Results are cached so disk reads happen once per build context dir.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} contextDir - Absolute path to the context directory
|
|
22
|
+
* @returns {string|null}
|
|
23
|
+
*/
|
|
24
|
+
export function loadAmbientContext(contextDir) {
|
|
25
|
+
if (!contextDir || !existsSync(contextDir)) return null;
|
|
26
|
+
if (_contextCache.has(contextDir)) return _contextCache.get(contextDir);
|
|
27
|
+
|
|
28
|
+
let files;
|
|
29
|
+
try {
|
|
30
|
+
files = readdirSync(contextDir).filter(f => f.endsWith('.md')).sort();
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const parts = [];
|
|
36
|
+
for (const filename of files) {
|
|
37
|
+
try {
|
|
38
|
+
const content = readFileSync(join(contextDir, filename), 'utf-8').trimEnd();
|
|
39
|
+
if (content) parts.push(content);
|
|
40
|
+
} catch {
|
|
41
|
+
// skip unreadable files
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const combined = parts.length > 0 ? parts.join('\n\n') : null;
|
|
46
|
+
_contextCache.set(contextDir, combined);
|
|
47
|
+
return combined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clear the ambient context cache for a given contextDir (call at build start).
|
|
52
|
+
*
|
|
53
|
+
* @param {string} contextDir
|
|
54
|
+
*/
|
|
55
|
+
export function clearAmbientContextCache(contextDir) {
|
|
56
|
+
if (contextDir) _contextCache.delete(contextDir);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build an agent prompt from a step dispatch and execution context.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} stepDispatch - Stratum step dispatch (step_id, intent, inputs, output_fields, ensure)
|
|
63
|
+
* @param {object} context - Execution context (cwd, featureCode, contextDir?)
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function buildStepPrompt(stepDispatch, context) {
|
|
67
|
+
const sections = [];
|
|
68
|
+
|
|
69
|
+
sections.push(`You are executing step "${stepDispatch.step_id}" in a Stratum workflow.`);
|
|
70
|
+
|
|
71
|
+
sections.push(`## Intent\n${stepDispatch.intent}`);
|
|
72
|
+
|
|
73
|
+
sections.push(`## Inputs\n${JSON.stringify(stepDispatch.inputs, null, 2)}`);
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(stepDispatch.output_fields) && stepDispatch.output_fields.length > 0) {
|
|
76
|
+
const fieldLines = stepDispatch.output_fields
|
|
77
|
+
.map(f => `- ${f.name} (${f.type})`)
|
|
78
|
+
.join('\n');
|
|
79
|
+
sections.push(`## Expected Output\nReturn a JSON object with these fields:\n${fieldLines}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (Array.isArray(stepDispatch.ensure) && stepDispatch.ensure.length > 0) {
|
|
83
|
+
const ensureLines = stepDispatch.ensure.map(e => `- ${e}`).join('\n');
|
|
84
|
+
sections.push(`## Postconditions\nYour result must satisfy:\n${ensureLines}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Inject ambient project context (docs/context/*.md) — cached per build
|
|
88
|
+
if (context.contextDir) {
|
|
89
|
+
const ambient = loadAmbientContext(context.contextDir);
|
|
90
|
+
if (ambient) {
|
|
91
|
+
sections.push(`## Project Context\n${ambient}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const ctxLines = [
|
|
96
|
+
`Working directory: ${context.cwd}`,
|
|
97
|
+
`Feature: ${context.featureCode}`,
|
|
98
|
+
];
|
|
99
|
+
if (context.featureDir) {
|
|
100
|
+
ctxLines.push(`Feature docs: ${context.featureDir}`);
|
|
101
|
+
}
|
|
102
|
+
sections.push(`## Context\n${ctxLines.join('\n')}`);
|
|
103
|
+
|
|
104
|
+
// Inject prior step results so the agent doesn't re-explore from scratch
|
|
105
|
+
if (Array.isArray(context.stepHistory) && context.stepHistory.length > 0) {
|
|
106
|
+
const historyLines = context.stepHistory.map(h => {
|
|
107
|
+
let line = `- **${h.stepId}**: ${h.summary}`;
|
|
108
|
+
if (h.artifact) line += ` → \`${h.artifact}\``;
|
|
109
|
+
return line;
|
|
110
|
+
});
|
|
111
|
+
sections.push(`## Prior Steps\n${historyLines.join('\n')}`);
|
|
112
|
+
|
|
113
|
+
// If any prior step captured a file manifest, include it for downstream steps
|
|
114
|
+
// (context.filesChanged is maintained as a pre-deduplicated array in build.js)
|
|
115
|
+
if (context.filesChanged?.length > 0) {
|
|
116
|
+
sections.push(`## Files Changed by This Feature\n${context.filesChanged.map(f => '- ' + f).join('\n')}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return sections.join('\n\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build a "File Ownership Conflicts" section for decompose-step retry prompts.
|
|
125
|
+
*
|
|
126
|
+
* @param {Array<{task_a: string, task_b: string, files: string[]}>} conflicts
|
|
127
|
+
* @returns {string}
|
|
128
|
+
*/
|
|
129
|
+
function buildConflictSection(conflicts) {
|
|
130
|
+
const lines = [
|
|
131
|
+
'## File Ownership Conflicts — Resolution Required',
|
|
132
|
+
'',
|
|
133
|
+
'The following task pairs share `files_owned` entries but have no `depends_on`',
|
|
134
|
+
'relationship. Independent tasks may not both claim the same file.',
|
|
135
|
+
'Add a `depends_on` edge from the later task to the earlier task to resolve each conflict:',
|
|
136
|
+
'',
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
for (const { task_a, task_b, files } of conflicts) {
|
|
140
|
+
lines.push(`- **${task_a}** and **${task_b}** both own:`);
|
|
141
|
+
for (const f of files) lines.push(` - \`${f}\``);
|
|
142
|
+
lines.push(` → Add \`depends_on: [${task_a}]\` to \`${task_b}\` (or vice versa).`);
|
|
143
|
+
lines.push('');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return lines.join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build a retry prompt when postconditions failed.
|
|
151
|
+
*
|
|
152
|
+
* @param {object} stepDispatch - Original step dispatch
|
|
153
|
+
* @param {string[]} violations - List of postcondition violations
|
|
154
|
+
* @param {object} context - Execution context
|
|
155
|
+
* @param {Array<{task_a, task_b, files}>} [conflicts] - Structured file conflicts (optional)
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
export function buildRetryPrompt(stepDispatch, violations, context, conflicts) {
|
|
159
|
+
const violationLines = violations.map(v => `- ${v}`).join('\n');
|
|
160
|
+
const header = `RETRY — Previous attempt failed postconditions:\n${violationLines}\n\nFix these issues and try again.`;
|
|
161
|
+
|
|
162
|
+
const sections = [header, buildStepPrompt(stepDispatch, context)];
|
|
163
|
+
|
|
164
|
+
if (conflicts && conflicts.length > 0) {
|
|
165
|
+
sections.push(buildConflictSection(conflicts));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return sections.join('\n\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Build a prompt for a child flow step within a larger workflow.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} flowDispatch - Flow dispatch (child_flow_name, child_step)
|
|
175
|
+
* @param {object} context - Execution context
|
|
176
|
+
* @returns {string}
|
|
177
|
+
*/
|
|
178
|
+
export function buildFlowStepPrompt(flowDispatch, context) {
|
|
179
|
+
const header = `You are executing a sub-workflow "${flowDispatch.child_flow_name}" as part of a larger workflow.`;
|
|
180
|
+
return `${header}\n\n${buildStepPrompt(flowDispatch.child_step, context)}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Build context preamble for a gate Q&A agent.
|
|
185
|
+
*
|
|
186
|
+
* Assembles the same execution context that regular steps get so the agent
|
|
187
|
+
* answering gate questions knows what feature is being built, what just
|
|
188
|
+
* completed, what files were touched, and what the gate controls.
|
|
189
|
+
*
|
|
190
|
+
* @param {object} gateDispatch - Stratum gate dispatch (step_id, on_approve, on_revise, on_kill)
|
|
191
|
+
* @param {object} context - Execution context (cwd, featureCode, featureDir, stepHistory, filesChanged)
|
|
192
|
+
* @param {object} [gateExtras] - Optional enrichment (fromPhase, toPhase, summary)
|
|
193
|
+
* @returns {string}
|
|
194
|
+
*/
|
|
195
|
+
export function buildGateContext(gateDispatch, context, gateExtras) {
|
|
196
|
+
const sections = [];
|
|
197
|
+
|
|
198
|
+
sections.push(
|
|
199
|
+
`You are answering questions about a gate review in a Compose build workflow.\n` +
|
|
200
|
+
`Gate: "${gateDispatch.step_id}"`,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Feature identity
|
|
204
|
+
const ctxLines = [
|
|
205
|
+
`Working directory: ${context.cwd}`,
|
|
206
|
+
`Feature: ${context.featureCode}`,
|
|
207
|
+
];
|
|
208
|
+
if (context.featureDir) {
|
|
209
|
+
ctxLines.push(`Feature docs: ${context.featureDir}`);
|
|
210
|
+
}
|
|
211
|
+
sections.push(`## Feature\n${ctxLines.join('\n')}`);
|
|
212
|
+
|
|
213
|
+
// Phase transition
|
|
214
|
+
if (gateExtras?.fromPhase || gateExtras?.toPhase) {
|
|
215
|
+
const from = gateExtras.fromPhase ?? '(unknown)';
|
|
216
|
+
const to = gateExtras.toPhase ?? '(unknown)';
|
|
217
|
+
sections.push(`## Phase Transition\n${from} → ${to}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Gate summary (from stratum dispatch enrichment)
|
|
221
|
+
if (gateExtras?.summary) {
|
|
222
|
+
sections.push(`## Gate Summary\n${gateExtras.summary}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Routing — what happens on each decision
|
|
226
|
+
const routing = [];
|
|
227
|
+
routing.push(`- **Approve** → ${gateDispatch.on_approve ?? '(complete flow)'}`);
|
|
228
|
+
routing.push(`- **Revise** → re-run from \`${gateDispatch.on_revise ?? '(kill)'}\``);
|
|
229
|
+
routing.push(`- **Kill** → ${gateDispatch.on_kill ?? '(terminate flow)'}`);
|
|
230
|
+
sections.push(`## Gate Routing\n${routing.join('\n')}`);
|
|
231
|
+
|
|
232
|
+
// Prior step history
|
|
233
|
+
if (Array.isArray(context.stepHistory) && context.stepHistory.length > 0) {
|
|
234
|
+
const historyLines = context.stepHistory.map(h => {
|
|
235
|
+
let line = `- **${h.stepId}**: ${h.summary}`;
|
|
236
|
+
if (h.artifact) line += ` → \`${h.artifact}\``;
|
|
237
|
+
return line;
|
|
238
|
+
});
|
|
239
|
+
sections.push(`## Prior Steps\n${historyLines.join('\n')}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Files changed
|
|
243
|
+
if (context.filesChanged?.length > 0) {
|
|
244
|
+
sections.push(`## Files Changed by This Feature\n${context.filesChanged.map(f => '- ' + f).join('\n')}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Staleness warnings — flag artifacts that belong to an earlier phase
|
|
248
|
+
if (context.featureDir && gateExtras?.toPhase) {
|
|
249
|
+
const staleArtifacts = checkStaleness(context.featureDir, gateExtras.toPhase);
|
|
250
|
+
const stale = staleArtifacts.filter(a => a.stale);
|
|
251
|
+
if (stale.length > 0) {
|
|
252
|
+
const lines = stale.map(a =>
|
|
253
|
+
`- **${a.file}** was written in phase \`${a.writtenPhase}\` but feature is now in \`${a.currentPhase}\``
|
|
254
|
+
);
|
|
255
|
+
sections.push(`## Stale Artifacts\nThe following artifacts may be outdated:\n${lines.join('\n')}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return sections.join('\n\n');
|
|
260
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* step-validator.js — Agent-as-validator for pipeline step outputs.
|
|
3
|
+
*
|
|
4
|
+
* After a step writes its artifact, dispatches a lightweight agent call
|
|
5
|
+
* to read the artifact and validate it against criteria. Returns
|
|
6
|
+
* { valid, issues } so the dispatch loop can retry if needed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { runAndNormalize } from './result-normalizer.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Run a validation agent call for a completed step.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} opts
|
|
15
|
+
* @param {string} opts.artifact - File path to validate (relative to cwd)
|
|
16
|
+
* @param {string[]} opts.criteria - List of things to check
|
|
17
|
+
* @param {string} opts.stepId - Step ID (for logging)
|
|
18
|
+
* @param {object} opts.connector - Agent connector to use
|
|
19
|
+
* @returns {Promise<{ valid: boolean, issues: string[] }>}
|
|
20
|
+
*/
|
|
21
|
+
export async function validateStep({ artifact, criteria, stepId, connector }) {
|
|
22
|
+
const prompt =
|
|
23
|
+
`You are a validator. Read the file "${artifact}" and check the following criteria:\n\n` +
|
|
24
|
+
criteria.map((c, i) => `${i + 1}. ${c}`).join('\n') + '\n\n' +
|
|
25
|
+
`Return ONLY a JSON code block — no other text:\n` +
|
|
26
|
+
'```json\n' +
|
|
27
|
+
'{ "valid": true, "issues": [] }\n' +
|
|
28
|
+
'```\n' +
|
|
29
|
+
`Set valid to false and list issues if any criterion is not met.`;
|
|
30
|
+
|
|
31
|
+
// Minimal dispatch descriptor — only output_fields needed for JSON extraction
|
|
32
|
+
const dispatch = {
|
|
33
|
+
step_id: `validate_${stepId}`,
|
|
34
|
+
output_fields: {
|
|
35
|
+
valid: 'boolean',
|
|
36
|
+
issues: 'array',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const { result } = await runAndNormalize(connector, prompt, dispatch);
|
|
41
|
+
|
|
42
|
+
if (!result || typeof result.valid !== 'boolean') {
|
|
43
|
+
// Extraction failed — assume valid (optimistic fallback)
|
|
44
|
+
process.stderr.write(` ⚠ Validator returned no structured result for ${stepId}, assuming valid\n`);
|
|
45
|
+
return { valid: true, issues: [] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { valid: result.valid, issues: result.issues ?? [] };
|
|
49
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stratum-mcp-client.js — MCP protocol client for stratum-mcp.
|
|
3
|
+
*
|
|
4
|
+
* Spawns `stratum-mcp` (no subcommand) as a child process and communicates
|
|
5
|
+
* via the MCP SDK over stdio. This is for the build runner's plan/step_done
|
|
6
|
+
* loop — distinct from server/stratum-client.js which uses CLI subcommands.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const client = new StratumMcpClient();
|
|
10
|
+
* await client.connect();
|
|
11
|
+
* const dispatch = await client.plan(specPath, 'build', { featureCode: 'FEAT-1' });
|
|
12
|
+
* const next = await client.stepDone(dispatch.flow_id, 'step1', { phase: 'design' });
|
|
13
|
+
* await client.close();
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
18
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
19
|
+
|
|
20
|
+
export class StratumError extends Error {
|
|
21
|
+
constructor(code, message, detail) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'StratumError';
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.detail = detail;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class StratumMcpClient {
|
|
30
|
+
#client = null;
|
|
31
|
+
#transport = null;
|
|
32
|
+
#connected = false;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Spawn stratum-mcp and establish MCP connection.
|
|
36
|
+
* @param {object} [opts]
|
|
37
|
+
* @param {string} [opts.command] - Override binary (for testing)
|
|
38
|
+
* @param {string[]} [opts.args] - Override args
|
|
39
|
+
* @param {string} [opts.cwd] - Working directory for the subprocess
|
|
40
|
+
*/
|
|
41
|
+
async connect(opts = {}) {
|
|
42
|
+
if (this.#connected) return;
|
|
43
|
+
|
|
44
|
+
const command = opts.command ?? 'stratum-mcp';
|
|
45
|
+
const args = opts.args ?? [];
|
|
46
|
+
|
|
47
|
+
// Pre-flight: verify binary exists on $PATH (skip for test overrides)
|
|
48
|
+
if (command === 'stratum-mcp') {
|
|
49
|
+
try {
|
|
50
|
+
execFileSync('which', [command], { stdio: 'pipe', timeout: 3000 });
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'stratum-mcp not found on $PATH. Install with: pip install stratum-mcp'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const transportOpts = { command, args, stderr: 'pipe' };
|
|
59
|
+
if (opts.cwd) transportOpts.cwd = opts.cwd;
|
|
60
|
+
this.#transport = new StdioClientTransport(transportOpts);
|
|
61
|
+
|
|
62
|
+
this.#client = new Client(
|
|
63
|
+
{ name: 'compose-build', version: '1.0.0' },
|
|
64
|
+
{ capabilities: {} }
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
await this.#client.connect(this.#transport);
|
|
68
|
+
this.#connected = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Kill subprocess and clean up. */
|
|
72
|
+
async close() {
|
|
73
|
+
if (!this.#connected) return;
|
|
74
|
+
try {
|
|
75
|
+
await this.#client.close();
|
|
76
|
+
} catch {
|
|
77
|
+
// Ignore close errors — process may already be dead
|
|
78
|
+
}
|
|
79
|
+
this.#client = null;
|
|
80
|
+
this.#transport = null;
|
|
81
|
+
this.#connected = false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Call an MCP tool and return the parsed JSON result.
|
|
86
|
+
* @param {string} toolName
|
|
87
|
+
* @param {object} args
|
|
88
|
+
* @returns {Promise<any>}
|
|
89
|
+
*/
|
|
90
|
+
async #callTool(toolName, args) {
|
|
91
|
+
// Allow test-injected client to bypass real connection requirement.
|
|
92
|
+
// Gated on NODE_ENV=test so production code cannot accidentally redirect calls.
|
|
93
|
+
const client = (process.env.NODE_ENV === 'test' && this._testClient) || null;
|
|
94
|
+
if (!client && !this.#connected) {
|
|
95
|
+
throw new Error('StratumMcpClient not connected. Call connect() first.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const result = await (client ?? this.#client).callTool({
|
|
99
|
+
name: toolName,
|
|
100
|
+
arguments: args,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// MCP tool results come back as content array; extract text content
|
|
104
|
+
const textContent = result.content?.find(c => c.type === 'text');
|
|
105
|
+
if (!textContent) {
|
|
106
|
+
throw new StratumError('EMPTY_RESPONSE', `Tool ${toolName} returned no text content`, '');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// MCP isError flag indicates tool-level failure
|
|
110
|
+
if (result.isError) {
|
|
111
|
+
throw new StratumError('TOOL_ERROR', textContent.text, '');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(textContent.text);
|
|
117
|
+
} catch {
|
|
118
|
+
// Try to extract JSON from text that may have surrounding prose
|
|
119
|
+
const jsonMatch = textContent.text.match(/\{[\s\S]*\}/);
|
|
120
|
+
if (jsonMatch) {
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
123
|
+
} catch {
|
|
124
|
+
throw new StratumError('PARSE_ERROR', `Tool ${toolName} returned invalid JSON`, textContent.text);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
throw new StratumError('PARSE_ERROR', `Tool ${toolName} returned invalid JSON`, textContent.text);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check for Stratum error envelope
|
|
132
|
+
if (parsed.status === 'error' || parsed.error) {
|
|
133
|
+
const err = parsed.error ?? parsed;
|
|
134
|
+
throw new StratumError(
|
|
135
|
+
err.code ?? 'STRATUM_ERROR',
|
|
136
|
+
err.message ?? 'Stratum tool call failed',
|
|
137
|
+
err.detail ?? ''
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return parsed;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Start a flow. Returns the first step dispatch.
|
|
146
|
+
* @param {string} spec - Inline YAML spec content (not a file path)
|
|
147
|
+
* @param {string} flow - Flow name within the spec
|
|
148
|
+
* @param {object} inputs - Flow input values
|
|
149
|
+
* @returns {Promise<object>} Step dispatch response
|
|
150
|
+
*/
|
|
151
|
+
async plan(spec, flow, inputs) {
|
|
152
|
+
return this.#callTool('stratum_plan', { spec, flow, inputs });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resume an in-progress flow. Returns the current step dispatch.
|
|
157
|
+
* @param {string} flowId
|
|
158
|
+
* @returns {Promise<object>} Step dispatch response (same format as plan/stepDone)
|
|
159
|
+
*/
|
|
160
|
+
async resume(flowId) {
|
|
161
|
+
return this.#callTool('stratum_resume', { flow_id: flowId });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Report step completion. Returns next step dispatch or completion.
|
|
166
|
+
* @param {string} flowId
|
|
167
|
+
* @param {string} stepId
|
|
168
|
+
* @param {object} result - Step result (must match output_contract)
|
|
169
|
+
* @returns {Promise<object>}
|
|
170
|
+
*/
|
|
171
|
+
async stepDone(flowId, stepId, result) {
|
|
172
|
+
return this.#callTool('stratum_step_done', {
|
|
173
|
+
flow_id: flowId,
|
|
174
|
+
step_id: stepId,
|
|
175
|
+
result,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Resolve a gate step.
|
|
181
|
+
* @param {string} flowId
|
|
182
|
+
* @param {string} stepId
|
|
183
|
+
* @param {'approve'|'revise'|'kill'} outcome
|
|
184
|
+
* @param {string} rationale
|
|
185
|
+
* @param {'human'|'agent'|'system'} resolvedBy
|
|
186
|
+
* @returns {Promise<object>}
|
|
187
|
+
*/
|
|
188
|
+
async gateResolve(flowId, stepId, outcome, rationale, resolvedBy = 'human') {
|
|
189
|
+
return this.#callTool('stratum_gate_resolve', {
|
|
190
|
+
flow_id: flowId,
|
|
191
|
+
step_id: stepId,
|
|
192
|
+
outcome,
|
|
193
|
+
rationale,
|
|
194
|
+
resolved_by: resolvedBy,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Skip the current step with a recorded reason.
|
|
200
|
+
* @param {string} flowId
|
|
201
|
+
* @param {string} stepId
|
|
202
|
+
* @param {string} reason
|
|
203
|
+
* @returns {Promise<object>}
|
|
204
|
+
*/
|
|
205
|
+
async skipStep(flowId, stepId, reason) {
|
|
206
|
+
return this.#callTool('stratum_skip_step', {
|
|
207
|
+
flow_id: flowId,
|
|
208
|
+
step_id: stepId,
|
|
209
|
+
reason,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get the full execution trace.
|
|
215
|
+
* @param {string} flowId
|
|
216
|
+
* @returns {Promise<object>}
|
|
217
|
+
*/
|
|
218
|
+
async audit(flowId) {
|
|
219
|
+
return this.#callTool('stratum_audit', { flow_id: flowId });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Start a counted iteration loop on a step.
|
|
224
|
+
* @param {string} flowId
|
|
225
|
+
* @param {string} stepId
|
|
226
|
+
* @returns {Promise<object>}
|
|
227
|
+
*/
|
|
228
|
+
async iterationStart(flowId, stepId) {
|
|
229
|
+
return this.#callTool('stratum_iteration_start', {
|
|
230
|
+
flow_id: flowId,
|
|
231
|
+
step_id: stepId,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Report one iteration result.
|
|
237
|
+
* @param {string} flowId
|
|
238
|
+
* @param {string} stepId
|
|
239
|
+
* @param {object} result
|
|
240
|
+
* @returns {Promise<object>}
|
|
241
|
+
*/
|
|
242
|
+
async iterationReport(flowId, stepId, result) {
|
|
243
|
+
return this.#callTool('stratum_iteration_report', {
|
|
244
|
+
flow_id: flowId,
|
|
245
|
+
step_id: stepId,
|
|
246
|
+
result,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Abort an iteration loop early.
|
|
252
|
+
* @param {string} flowId
|
|
253
|
+
* @param {string} stepId
|
|
254
|
+
* @param {string} reason
|
|
255
|
+
* @returns {Promise<object>}
|
|
256
|
+
*/
|
|
257
|
+
async iterationAbort(flowId, stepId, reason) {
|
|
258
|
+
return this.#callTool('stratum_iteration_abort', {
|
|
259
|
+
flow_id: flowId,
|
|
260
|
+
step_id: stepId,
|
|
261
|
+
reason,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Validate a spec without executing.
|
|
267
|
+
* @param {string} spec - Inline YAML spec content
|
|
268
|
+
* @returns {Promise<{valid: boolean, errors?: string[]}>}
|
|
269
|
+
*/
|
|
270
|
+
async validate(spec) {
|
|
271
|
+
return this.#callTool('stratum_validate', { spec });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Create a named checkpoint.
|
|
276
|
+
* @param {string} flowId
|
|
277
|
+
* @param {string} label
|
|
278
|
+
* @returns {Promise<object>}
|
|
279
|
+
*/
|
|
280
|
+
async commit(flowId, label) {
|
|
281
|
+
return this.#callTool('stratum_commit', {
|
|
282
|
+
flow_id: flowId,
|
|
283
|
+
label,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Roll back to a checkpoint.
|
|
289
|
+
* @param {string} flowId
|
|
290
|
+
* @param {string} label
|
|
291
|
+
* @returns {Promise<object>}
|
|
292
|
+
*/
|
|
293
|
+
async revert(flowId, label) {
|
|
294
|
+
return this.#callTool('stratum_revert', {
|
|
295
|
+
flow_id: flowId,
|
|
296
|
+
label,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Report batch task results for a parallel_dispatch step.
|
|
302
|
+
* @param {string} flowId
|
|
303
|
+
* @param {string} stepId
|
|
304
|
+
* @param {Array<{task_id: string, status: string, result?: object, error?: string}>} taskResults
|
|
305
|
+
* @param {'clean'|'conflict'|'fallback'|'manual_required'} mergeStatus
|
|
306
|
+
* @returns {Promise<object>} Next dispatch response
|
|
307
|
+
*/
|
|
308
|
+
async parallelDone(flowId, stepId, taskResults, mergeStatus) {
|
|
309
|
+
return this.#callTool('stratum_parallel_done', {
|
|
310
|
+
flow_id: flowId,
|
|
311
|
+
step_id: stepId,
|
|
312
|
+
task_results: taskResults,
|
|
313
|
+
merge_status: mergeStatus,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Start server-side execution of a parallel_dispatch step (T2-F5-COMPOSE-MIGRATE).
|
|
319
|
+
* Returns {status: 'started', ...} on success or {error, message} on known error.
|
|
320
|
+
* @param {string} flowId
|
|
321
|
+
* @param {string} stepId
|
|
322
|
+
* @returns {Promise<object>}
|
|
323
|
+
*/
|
|
324
|
+
async parallelStart(flowId, stepId) {
|
|
325
|
+
return this.#callTool('stratum_parallel_start', {
|
|
326
|
+
flow_id: flowId,
|
|
327
|
+
step_id: stepId,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Poll state of a server-dispatched parallel_dispatch step (T2-F5-COMPOSE-MIGRATE).
|
|
333
|
+
* Returns {summary, tasks, require_satisfied, can_advance, outcome}.
|
|
334
|
+
* Break on `outcome != null`, not `can_advance` — see design doc §3.
|
|
335
|
+
* @param {string} flowId
|
|
336
|
+
* @param {string} stepId
|
|
337
|
+
* @returns {Promise<object>}
|
|
338
|
+
*/
|
|
339
|
+
async parallelPoll(flowId, stepId) {
|
|
340
|
+
return this.#callTool('stratum_parallel_poll', {
|
|
341
|
+
flow_id: flowId,
|
|
342
|
+
step_id: stepId,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Consumer-driven advance for parallel_dispatch steps with defer_advance:true.
|
|
348
|
+
* Call after observing outcome.status === 'awaiting_consumer_advance' from
|
|
349
|
+
* parallelPoll. Feeds merge_status back to Stratum which runs
|
|
350
|
+
* _evaluate_parallel_results + _advance_after_parallel and returns the real
|
|
351
|
+
* advance outcome.
|
|
352
|
+
*
|
|
353
|
+
* @param {string} flowId
|
|
354
|
+
* @param {string} stepId
|
|
355
|
+
* @param {'clean'|'conflict'} mergeStatus
|
|
356
|
+
* @returns {Promise<object>}
|
|
357
|
+
*/
|
|
358
|
+
async parallelAdvance(flowId, stepId, mergeStatus) {
|
|
359
|
+
return this.#callTool('stratum_parallel_advance', {
|
|
360
|
+
flow_id: flowId,
|
|
361
|
+
step_id: stepId,
|
|
362
|
+
merge_status: mergeStatus,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|