@smartmemory/compose 0.1.1-beta → 0.1.2-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/bug-fix/SKILL.md +143 -0
- package/.claude/skills/compose/SKILL.md +604 -0
- package/.compose-deps.json +89 -0
- package/README.md +14 -3
- package/bin/compose.js +473 -0
- package/contracts/comp-obs-contract.schema.json +362 -0
- package/contracts/cross-model-review-result.json +78 -0
- package/contracts/review-result.json +126 -0
- package/dist/assets/{_baseUniq-CQwX6VLz.js → _baseUniq-D-avYfn5.js} +1 -1
- package/dist/assets/{arc-SxJ2J1sh.js → arc-BC4dfQ-X.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BykunY1F.js → architectureDiagram-Q4EWVU46-BZmFXnGI.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-ohAKBOUw.js → blockDiagram-DXYQGD6D-DlfWSuux.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-DBDC3ENB.js → c4Diagram-AHTNJAMY-Y__uJrRx.js} +1 -1
- package/dist/assets/channel-LRG9kHqJ.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-Cv93Z7uM.js → chunk-4BX2VUAB-BfMePfTp.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-DE0WBDkj.js → chunk-4TB4RGXK-BdlMSdEA.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CE1EXenG.js → chunk-55IACEB6-vrQHZTdv.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-DA7Ana6H.js → chunk-EDXVE4YY-B8wioVlW.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CTDIPA3p.js → chunk-FMBD7UC4-Cd6Hrux2.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-uGBaPaTX.js → chunk-OYMX7WX6-CfrhdQXY.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-CYlnXuUO.js → chunk-QZHKN3VN-B9JQerOU.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-ojGkzcZK.js → chunk-YZCP3GAM-DFN9X99H.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +1 -0
- package/dist/assets/clone-dRxgFrBv.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-Bktn9hL-.js → cose-bilkent-S5V4N54A-BAn0ap_E.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-DFaSzuRF.js → dagre-KV5264BT-DyxnVq1g.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DnfmDzEm.js → diagram-5BDNPKRD-XCrzqski.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Bm8W9YnG.js → diagram-G4DWMVQ6-MBCAXft_.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-B5-TSKvp.js → diagram-MMDJMWI5-DbtB2yS6.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-ls4rqlky.js → diagram-TYMM5635-Bb5NzX61.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-giG6WO-r.js → erDiagram-SMLLAGMA-CpIeCOh2.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-XvlUuz-7.js → flowDiagram-DWJPFMVM-CHyoKnhW.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-hLBV57oV.js → ganttDiagram-T4ZO3ILL-DErKteO_.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js → gitGraphDiagram-UUTBAWPF-KFVAtj2F.js} +1 -1
- package/dist/assets/{graph-D0Cfv00Y.js → graph-CRnO_ifT.js} +1 -1
- package/dist/assets/index-DKBsEUJ-.css +1 -0
- package/dist/assets/index-DkRKLuNr.js +1144 -0
- package/dist/assets/{infoDiagram-42DDH7IO-DbqRsOo3.js → infoDiagram-42DDH7IO-BZFnuSp5.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-DnCdx7zb.js → ishikawaDiagram-UXIWVN3A-4Xe2Szde.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-CfD7eNcP.js → journeyDiagram-VCZTEJTY-CZRByfS-.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-BYaO9-mK.js → kanban-definition-6JOO6SKY-B95sk6Fk.js} +1 -1
- package/dist/assets/{layout-Bj72wOEB.js → layout-BqNQzxWT.js} +1 -1
- package/dist/assets/{linear-BRFo114D.js → linear-CUh7qb64.js} +1 -1
- package/dist/assets/{min-GCHnKlJS.js → min-wXgOS3ig.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-n0PMebY4.js → mindmap-definition-QFDTVHPH-DB6iaAbO.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-pN4CljHF.js → pieDiagram-DEJITSTG-CHkZHrTW.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-DNoAy8-D.js → quadrantDiagram-34T5L4WZ-DoTEO8e3.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-BhtY05PT.js → requirementDiagram-MS252O5E-Dn8peXYp.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-B6AD-16A.js → sankeyDiagram-XADWPNL6-DRXs6Ipb.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-DShHM-uk.js → sequenceDiagram-FGHM5R23-wBBYZ0aq.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DMxn7HTo.js → stateDiagram-FHFEXIEX-DPlBNGmf.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-Cdu6uq52.js → timeline-definition-GMOUNBTQ-CbbyTlHk.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-CpK29iRe.js → vennDiagram-DHZGUBPP-Bj4GaFfj.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-BQgSkdcO.js → wardley-RL74JXVD-RtNzq8KU.js} +55 -55
- package/dist/assets/{wardleyDiagram-NUSXRM2D-DJHYev6O.js → wardleyDiagram-NUSXRM2D-CDfE3zSj.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-1d75pbaO.js → xychartDiagram-5P7HB3ND-CZXHHYD5.js} +1 -1
- package/dist/index.html +2 -2
- package/lib/budget-ledger.js +45 -0
- package/lib/bug-bisect.js +292 -0
- package/lib/bug-checkpoint.js +191 -0
- package/lib/bug-escalation.js +306 -0
- package/lib/bug-index-gen.js +136 -0
- package/lib/bug-ledger.js +126 -0
- package/lib/build-stream-schema.js +176 -0
- package/lib/build-stream-writer.js +3 -1
- package/lib/build.js +854 -284
- package/lib/connector-factory-shim.js +167 -0
- package/lib/constants.js +18 -0
- package/lib/debug-discipline.js +176 -27
- package/lib/deps.js +205 -0
- package/lib/health-score.js +4 -4
- package/lib/import.js +26 -13
- package/lib/inject-schema.js +21 -0
- package/lib/new.js +27 -53
- package/lib/result-normalizer.js +160 -144
- package/lib/review-lenses.js +5 -5
- package/lib/review-normalize.js +413 -0
- package/lib/review-prompt.js +163 -0
- package/lib/sections.js +325 -0
- package/lib/step-prompt.js +21 -1
- package/lib/step-validator.js +5 -3
- package/lib/stratum-mcp-client.js +172 -7
- package/package.json +14 -3
- package/pipelines/bug-fix.stratum.yaml +39 -1
- package/pipelines/build.stratum.yaml +28 -45
- package/pipelines/review-fix.stratum.yaml +1 -1
- package/presets/team-review.stratum.yaml +21 -14
- package/server/build-stream-bridge.js +28 -0
- package/server/cc-session-feature-resolver.js +111 -0
- package/server/cc-session-reader.js +327 -0
- package/server/cc-session-watcher.js +318 -0
- package/server/compose-mcp-tools.js +0 -125
- package/server/compose-mcp.js +2 -4
- package/server/contract-diff.js +192 -0
- package/server/decision-event-emit.js +175 -0
- package/server/decision-event-id.js +64 -0
- package/server/decision-events-snapshot.js +166 -0
- package/server/design-routes.js +92 -49
- package/server/drift-axes.js +365 -0
- package/server/drift-emit.js +121 -0
- package/server/gate-log-store.js +102 -0
- package/server/lifecycle-phase-history.js +44 -0
- package/server/open-loops-store.js +102 -0
- package/server/schema-validator.js +49 -0
- package/server/status-emit.js +27 -0
- package/server/status-snapshot.js +218 -0
- package/server/vision-routes.js +332 -4
- package/server/vision-server.js +104 -12
- package/server/vision-store.js +21 -0
- package/dist/assets/channel-DGElom1e.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +0 -1
- package/dist/assets/clone-DUJKJXd7.js +0 -1
- package/dist/assets/index-CUd6pFGF.css +0 -1
- package/dist/assets/index-DReRlzZI.js +0 -1144
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +0 -1
- package/server/connectors/agent-connector.js +0 -78
- package/server/connectors/claude-sdk-connector.js +0 -198
- package/server/connectors/codex-connector.js +0 -240
- package/server/connectors/connector-discovery.js +0 -18
- package/server/connectors/connector-runtime.js +0 -13
- package/server/connectors/opencode-connector.js +0 -200
package/lib/import.js
CHANGED
|
@@ -9,7 +9,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSy
|
|
|
9
9
|
import { join, relative, extname, basename } from 'node:path';
|
|
10
10
|
|
|
11
11
|
import { runAndNormalize } from './result-normalizer.js';
|
|
12
|
-
import {
|
|
12
|
+
import { StratumMcpClient } from './stratum-mcp-client.js';
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// File tree scanner
|
|
@@ -116,12 +116,11 @@ function readKeyFiles(cwd, maxContentSize = 8000) {
|
|
|
116
116
|
* Scan a project and generate a structured analysis.
|
|
117
117
|
*
|
|
118
118
|
* @param {object} opts
|
|
119
|
-
* @param {string}
|
|
120
|
-
* @param {
|
|
119
|
+
* @param {string} [opts.cwd] - Working directory
|
|
120
|
+
* @param {StratumMcpClient} [opts.stratum] - Pre-connected client (testing)
|
|
121
121
|
*/
|
|
122
122
|
export async function runImport(opts = {}) {
|
|
123
123
|
const cwd = opts.cwd ?? process.cwd();
|
|
124
|
-
const getConnector = opts.connectorFactory ?? (() => new ClaudeSDKConnector({ cwd }));
|
|
125
124
|
|
|
126
125
|
console.log('Scanning project...\n');
|
|
127
126
|
|
|
@@ -205,15 +204,29 @@ Write the full analysis to \`docs/discovery/project-analysis.md\`.
|
|
|
205
204
|
|
|
206
205
|
Return JSON: { "summary": string, "features": [{"code": string, "name": string, "status": string}], "artifact": "docs/discovery/project-analysis.md" }`;
|
|
207
206
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
207
|
+
let stratum = opts.stratum;
|
|
208
|
+
let ownsStratum = false;
|
|
209
|
+
if (!stratum) {
|
|
210
|
+
stratum = new StratumMcpClient();
|
|
211
|
+
await stratum.connect({ cwd });
|
|
212
|
+
ownsStratum = true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let result;
|
|
216
|
+
try {
|
|
217
|
+
({ result } = await runAndNormalize(null, prompt, {
|
|
218
|
+
step_id: 'import_analyze',
|
|
219
|
+
agent: 'claude',
|
|
220
|
+
output_contract: 'AnalysisResult',
|
|
221
|
+
output_fields: {
|
|
222
|
+
summary: 'string',
|
|
223
|
+
features: 'array',
|
|
224
|
+
artifact: 'string',
|
|
225
|
+
},
|
|
226
|
+
}, { stratum, cwd }));
|
|
227
|
+
} finally {
|
|
228
|
+
if (ownsStratum) await stratum.close();
|
|
229
|
+
}
|
|
217
230
|
|
|
218
231
|
// 6. Verify the analysis was written
|
|
219
232
|
const analysisPath = join(cwd, 'docs', 'discovery', 'project-analysis.md');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inject-schema.js — Append a JSON-Schema instruction block to an agent prompt
|
|
3
|
+
* so the response includes a structured JSON code-block at the end.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} prompt
|
|
8
|
+
* @param {object} schema JSON Schema object
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function injectSchema(prompt, schema) {
|
|
12
|
+
return (
|
|
13
|
+
`${prompt}\n\n` +
|
|
14
|
+
`IMPORTANT: After completing the task, include a JSON code block at the very end ` +
|
|
15
|
+
`of your response matching this schema:\n` +
|
|
16
|
+
'```json\n' +
|
|
17
|
+
`${JSON.stringify(schema, null, 2)}\n` +
|
|
18
|
+
'```\n' +
|
|
19
|
+
`The JSON block must be the last thing in your response.`
|
|
20
|
+
);
|
|
21
|
+
}
|
package/lib/new.js
CHANGED
|
@@ -12,33 +12,15 @@ import { join, basename } from 'node:path';
|
|
|
12
12
|
|
|
13
13
|
import { StratumMcpClient } from './stratum-mcp-client.js';
|
|
14
14
|
import { runAndNormalize } from './result-normalizer.js';
|
|
15
|
+
import { installFactoryShim } from './connector-factory-shim.js';
|
|
15
16
|
import { buildStepPrompt, buildRetryPrompt, buildGateContext } from './step-prompt.js';
|
|
16
17
|
import { promptGate } from './gate-prompt.js';
|
|
17
18
|
import { VisionWriter } from './vision-writer.js';
|
|
18
19
|
|
|
19
20
|
import { validateStep } from './step-validator.js';
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Agent registry (same as build.js)
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
const DEFAULT_AGENTS = new Map([
|
|
29
|
-
['claude', (opts) => new ClaudeSDKConnector(opts)],
|
|
30
|
-
['codex', (opts) => new CodexConnector(opts)],
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
function defaultConnectorFactory(agentType, opts) {
|
|
34
|
-
const factory = DEFAULT_AGENTS.get(agentType);
|
|
35
|
-
if (!factory) throw new Error(`Unknown agent type: ${agentType}`);
|
|
36
|
-
return factory(opts);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
// Main
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
22
|
+
// STRAT-DEDUP-AGENTRUN-V3: connectors live in stratum-mcp; runAndNormalize
|
|
23
|
+
// dispatches via stratum.agentRun(...) directly.
|
|
42
24
|
|
|
43
25
|
/**
|
|
44
26
|
* Run the product kickoff pipeline.
|
|
@@ -48,12 +30,10 @@ function defaultConnectorFactory(agentType, opts) {
|
|
|
48
30
|
* @param {string} [opts.cwd] - Working directory (default: process.cwd())
|
|
49
31
|
* @param {string} [opts.projectName] - Project name override
|
|
50
32
|
* @param {boolean} [opts.skipResearch] - Skip the research step
|
|
51
|
-
* @param {Function} [opts.connectorFactory] - Override agent connector creation (for testing)
|
|
52
33
|
* @param {object} [opts.gateOpts] - Options for gate prompt (input/output streams)
|
|
53
34
|
*/
|
|
54
35
|
export async function runNew(intent, opts = {}) {
|
|
55
36
|
const cwd = opts.cwd ?? process.cwd();
|
|
56
|
-
const getConnector = opts.connectorFactory ?? defaultConnectorFactory;
|
|
57
37
|
const projectName = opts.projectName ?? basename(cwd);
|
|
58
38
|
const skipResearch = opts.skipResearch ?? false;
|
|
59
39
|
|
|
@@ -116,9 +96,13 @@ export async function runNew(intent, opts = {}) {
|
|
|
116
96
|
const visionWriter = new VisionWriter(dataDir);
|
|
117
97
|
const itemId = await visionWriter.ensureFeatureItem(projectName, projectName);
|
|
118
98
|
|
|
119
|
-
// Stratum MCP client
|
|
120
|
-
const stratum = new StratumMcpClient();
|
|
121
|
-
await stratum.connect({ cwd });
|
|
99
|
+
// Stratum MCP client (test override permitted via opts.stratum)
|
|
100
|
+
const stratum = opts.stratum ?? new StratumMcpClient();
|
|
101
|
+
if (!opts.stratum) await stratum.connect({ cwd });
|
|
102
|
+
|
|
103
|
+
if (opts.connectorFactory && !opts.stratum) {
|
|
104
|
+
installFactoryShim(stratum, opts.connectorFactory, cwd);
|
|
105
|
+
}
|
|
122
106
|
|
|
123
107
|
try {
|
|
124
108
|
console.log(`Starting product kickoff for "${projectName}"...`);
|
|
@@ -144,19 +128,18 @@ export async function runNew(intent, opts = {}) {
|
|
|
144
128
|
|
|
145
129
|
const agentType = response.agent ?? 'claude';
|
|
146
130
|
const prompt = buildStepPrompt(response, context);
|
|
147
|
-
const
|
|
148
|
-
const { result } = await runAndNormalize(connector, prompt, response);
|
|
131
|
+
const { result } = await runAndNormalize(null, prompt, response, { stratum, cwd });
|
|
149
132
|
|
|
150
133
|
// Agent-as-validator: if step has validate config, check the artifact
|
|
151
134
|
const valConfig = validateConfigs.get(stepId);
|
|
152
135
|
if (valConfig) {
|
|
153
136
|
console.log(` ✓ Validating ${stepId}...`);
|
|
154
|
-
const valConnector = getConnector('claude', { cwd });
|
|
155
137
|
const { valid, issues } = await validateStep({
|
|
156
138
|
artifact: valConfig.artifact,
|
|
157
139
|
criteria: valConfig.criteria,
|
|
158
140
|
stepId,
|
|
159
|
-
|
|
141
|
+
stratum,
|
|
142
|
+
cwd,
|
|
160
143
|
});
|
|
161
144
|
if (!valid) {
|
|
162
145
|
console.log(` ✗ Validation failed:`);
|
|
@@ -167,8 +150,8 @@ export async function runNew(intent, opts = {}) {
|
|
|
167
150
|
issues.map(i => `- ${i}`).join('\n') + '\n\n' +
|
|
168
151
|
`Update the file in place. Do not skip any issue.\n\n` +
|
|
169
152
|
`## Context\nWorking directory: ${cwd}\nProject: ${projectName}`;
|
|
170
|
-
const
|
|
171
|
-
await runAndNormalize(
|
|
153
|
+
const fixDispatch = { ...response, agent: 'claude' };
|
|
154
|
+
await runAndNormalize(null, fixPrompt, fixDispatch, { stratum, cwd });
|
|
172
155
|
}
|
|
173
156
|
}
|
|
174
157
|
|
|
@@ -224,7 +207,6 @@ export async function runNew(intent, opts = {}) {
|
|
|
224
207
|
// Agent Q&A callback for interactive gate — with full workflow context
|
|
225
208
|
const gatePreamble = buildGateContext(response, context, null);
|
|
226
209
|
const askAgent = async (question, artifactPath) => {
|
|
227
|
-
const connector = getConnector('claude', { cwd });
|
|
228
210
|
const fileRef = artifactPath
|
|
229
211
|
? `Read the file "${artifactPath}" and answer`
|
|
230
212
|
: `Look at the project files in the working directory and answer`;
|
|
@@ -233,12 +215,8 @@ export async function runNew(intent, opts = {}) {
|
|
|
233
215
|
`${fileRef} this question concisely:\n\n` +
|
|
234
216
|
`${question}\n\n` +
|
|
235
217
|
`Keep your answer brief — 2-3 sentences max.`;
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
if (event.type === 'assistant' && event.content) parts.push(event.content);
|
|
239
|
-
if (event.type === 'result' && event.content && parts.length === 0) parts.push(event.content);
|
|
240
|
-
}
|
|
241
|
-
return parts.join('') || '(no answer)';
|
|
218
|
+
const text = await stratum.runAgentText('claude', qaPrompt, { cwd });
|
|
219
|
+
return text || '(no answer)';
|
|
242
220
|
};
|
|
243
221
|
|
|
244
222
|
const { outcome, rationale } = await promptGate(response, {
|
|
@@ -266,7 +244,7 @@ export async function runNew(intent, opts = {}) {
|
|
|
266
244
|
console.log(`[sub-flow] ${childFlowName}...`);
|
|
267
245
|
|
|
268
246
|
const childResult = await executeChildFlow(
|
|
269
|
-
response, stratum,
|
|
247
|
+
response, stratum, context,
|
|
270
248
|
visionWriter, itemId, opts.gateOpts ?? {}
|
|
271
249
|
);
|
|
272
250
|
|
|
@@ -285,13 +263,12 @@ export async function runNew(intent, opts = {}) {
|
|
|
285
263
|
violations.map(v => `- ${v}`).join('\n') + '\n\n' +
|
|
286
264
|
`Fix every issue. Do not skip any.\n\n` +
|
|
287
265
|
`## Context\nWorking directory: ${cwd}\nProject: ${projectName}`;
|
|
288
|
-
const
|
|
289
|
-
await runAndNormalize(
|
|
266
|
+
const fixDispatch = { ...response, agent: fixAgent };
|
|
267
|
+
await runAndNormalize(null, fixPrompt, fixDispatch, { stratum, cwd });
|
|
290
268
|
|
|
291
269
|
console.log(` ↻ Retrying ${stepId} (${agentType})`);
|
|
292
270
|
const prompt = buildRetryPrompt(response, violations, context);
|
|
293
|
-
const
|
|
294
|
-
const { result } = await runAndNormalize(connector, prompt, response);
|
|
271
|
+
const { result } = await runAndNormalize(null, prompt, response, { stratum, cwd });
|
|
295
272
|
|
|
296
273
|
response = await stratum.stepDone(
|
|
297
274
|
response.flow_id, response.step_id,
|
|
@@ -341,7 +318,7 @@ export async function runNew(intent, opts = {}) {
|
|
|
341
318
|
// ---------------------------------------------------------------------------
|
|
342
319
|
|
|
343
320
|
async function executeChildFlow(
|
|
344
|
-
flowDispatch, stratum,
|
|
321
|
+
flowDispatch, stratum, context,
|
|
345
322
|
visionWriter, itemId, gateOpts
|
|
346
323
|
) {
|
|
347
324
|
let resp = flowDispatch.child_step;
|
|
@@ -353,10 +330,8 @@ async function executeChildFlow(
|
|
|
353
330
|
console.log(` [${childFlowName}] ${resp.step_id}...`);
|
|
354
331
|
await visionWriter.updateItemPhase(itemId, `${childFlowName}:${resp.step_id}`);
|
|
355
332
|
|
|
356
|
-
const agentType = resp.agent ?? 'claude';
|
|
357
333
|
const prompt = buildStepPrompt(resp, context);
|
|
358
|
-
const
|
|
359
|
-
const { result } = await runAndNormalize(connector, prompt, resp);
|
|
334
|
+
const { result } = await runAndNormalize(null, prompt, resp, { stratum, cwd: context.cwd });
|
|
360
335
|
|
|
361
336
|
resp = await stratum.stepDone(
|
|
362
337
|
childFlowId, resp.step_id,
|
|
@@ -381,13 +356,12 @@ async function executeChildFlow(
|
|
|
381
356
|
violations.map(v => `- ${v}`).join('\n') + '\n\n' +
|
|
382
357
|
`Fix every issue.\n\n` +
|
|
383
358
|
`## Context\nWorking directory: ${context.cwd}\nProject: ${context.projectName}`;
|
|
384
|
-
const
|
|
385
|
-
await runAndNormalize(
|
|
359
|
+
const fixDispatch = { ...resp, agent: fixAgent };
|
|
360
|
+
await runAndNormalize(null, fixPrompt, fixDispatch, { stratum, cwd: context.cwd });
|
|
386
361
|
|
|
387
362
|
console.log(` [${childFlowName}] ↻ Retrying ${resp.step_id} (${stepAgent})`);
|
|
388
363
|
const prompt = buildRetryPrompt(resp, violations, context);
|
|
389
|
-
const
|
|
390
|
-
const { result } = await runAndNormalize(connector, prompt, resp);
|
|
364
|
+
const { result } = await runAndNormalize(null, prompt, resp, { stratum, cwd: context.cwd });
|
|
391
365
|
|
|
392
366
|
resp = await stratum.stepDone(
|
|
393
367
|
resp.flow_id ?? childFlowId, resp.step_id,
|
|
@@ -398,7 +372,7 @@ async function executeChildFlow(
|
|
|
398
372
|
const nestedParentFlowId = resp.parent_flow_id;
|
|
399
373
|
const nestedParentStepId = resp.parent_step_id;
|
|
400
374
|
const nestedResult = await executeChildFlow(
|
|
401
|
-
resp, stratum,
|
|
375
|
+
resp, stratum, context,
|
|
402
376
|
visionWriter, itemId, gateOpts
|
|
403
377
|
);
|
|
404
378
|
resp = await stratum.stepDone(nestedParentFlowId, nestedParentStepId, nestedResult);
|
package/lib/result-normalizer.js
CHANGED
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* accumulates text, and extracts structured JSON from the response.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { injectSchema } from './inject-schema.js';
|
|
10
11
|
import { CliProgress } from './cli-progress.js';
|
|
11
12
|
import { calculateCost } from './model-pricing.js';
|
|
13
|
+
import { resolveAgentConfig } from './agent-string.js';
|
|
14
|
+
import { normalizeReviewResult } from './review-normalize.js';
|
|
12
15
|
|
|
13
16
|
// ---------------------------------------------------------------------------
|
|
14
17
|
// Error classes
|
|
@@ -146,26 +149,50 @@ export class AgentTimeoutError extends Error {
|
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
|
|
152
|
+
/**
|
|
153
|
+
* STRAT-DEDUP-AGENTRUN-V3: `runAndNormalize` is now a thin wrapper around the
|
|
154
|
+
* Python connector tier exposed through `stratum_agent_run`. Events arrive as
|
|
155
|
+
* BuildStreamEvent envelopes via MCP progress notifications; we subscribe with
|
|
156
|
+
* `stratum.onEvent(correlationId, '_agent_run', handler)` and translate the
|
|
157
|
+
* envelopes back into the legacy stream-writer shape so downstream consumers
|
|
158
|
+
* (build-stream-writer, cockpit) keep working unchanged.
|
|
159
|
+
*
|
|
160
|
+
* The first `connector` arg is intentionally ignored — kept only so the 18
|
|
161
|
+
* call-sites do not all need to be edited in a single sweep. New required opt:
|
|
162
|
+
* `opts.stratum` — the StratumMcpClient instance.
|
|
163
|
+
*/
|
|
164
|
+
export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, opts = {}) {
|
|
150
165
|
const progress = opts.progress;
|
|
151
166
|
const streamWriter = opts.streamWriter;
|
|
152
|
-
const onToolUse = opts.onToolUse ?? null;
|
|
153
|
-
const maxDurationMs = opts.maxDurationMs ?? null;
|
|
167
|
+
const onToolUse = opts.onToolUse ?? null;
|
|
168
|
+
const maxDurationMs = opts.maxDurationMs ?? null;
|
|
169
|
+
const stratum = opts.stratum;
|
|
170
|
+
|
|
171
|
+
if (!stratum || typeof stratum.agentRun !== 'function') {
|
|
172
|
+
throw new AgentError(
|
|
173
|
+
'runAndNormalize requires opts.stratum (a connected StratumMcpClient). ' +
|
|
174
|
+
'Pass stratum: stratumClient at the call-site.'
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const stepId = stepDispatch.step_id ?? 'unknown';
|
|
179
|
+
const agentType = stepDispatch.agent ?? 'claude';
|
|
180
|
+
const cfg = resolveAgentConfig(agentType);
|
|
181
|
+
|
|
154
182
|
const outputFields = stepDispatch.output_fields;
|
|
155
183
|
const hasSchema = outputFields && typeof outputFields === 'object' && Object.keys(outputFields).length > 0;
|
|
156
|
-
|
|
157
184
|
let actualPrompt = prompt;
|
|
158
185
|
let schema = null;
|
|
159
|
-
|
|
160
186
|
if (hasSchema) {
|
|
161
187
|
schema = outputFieldsToJsonSchema(outputFields);
|
|
162
188
|
actualPrompt = injectSchema(prompt, schema);
|
|
163
189
|
}
|
|
164
190
|
|
|
165
|
-
const
|
|
191
|
+
const correlationId = `${stepDispatch.flow_id ?? 'noflow'}:${stepId}:${randomUUID()}`;
|
|
192
|
+
const subStepId = '_agent_run';
|
|
166
193
|
const startTime = Date.now();
|
|
167
194
|
|
|
168
|
-
|
|
195
|
+
const textParts = [];
|
|
169
196
|
const usageTotals = {
|
|
170
197
|
input_tokens: 0,
|
|
171
198
|
output_tokens: 0,
|
|
@@ -175,157 +202,148 @@ export async function runAndNormalize(connector, prompt, stepDispatch, opts = {}
|
|
|
175
202
|
model: null,
|
|
176
203
|
};
|
|
177
204
|
|
|
178
|
-
// Set up timeout timer if configured
|
|
179
|
-
let timeoutTimer = null;
|
|
180
205
|
let timedOut = false;
|
|
206
|
+
let userInterruptAction = null;
|
|
207
|
+
let timeoutHandle = null;
|
|
208
|
+
|
|
209
|
+
// Subscribe BEFORE calling agentRun — events fire during the call.
|
|
210
|
+
const unsub = stratum.onEvent(correlationId, subStepId, (env) => {
|
|
211
|
+
if (!env || env.schema_version !== '0.2.5') return;
|
|
212
|
+
const m = env.metadata ?? {};
|
|
213
|
+
switch (env.kind) {
|
|
214
|
+
case 'agent_relay':
|
|
215
|
+
if (m.role === 'assistant' && typeof m.text === 'string' && m.text.length > 0) {
|
|
216
|
+
textParts.push(m.text);
|
|
217
|
+
if (streamWriter) streamWriter.write({ type: 'assistant', content: m.text });
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
case 'tool_use_summary': {
|
|
221
|
+
const tool = m.tool;
|
|
222
|
+
if (tool) {
|
|
223
|
+
if (streamWriter) {
|
|
224
|
+
streamWriter.write({ type: 'tool_use', tool, input: m.input ?? {} });
|
|
225
|
+
}
|
|
226
|
+
if (onToolUse) onToolUse({ tool, input: m.input ?? {}, timestamp: Date.now() });
|
|
227
|
+
if (progress) {
|
|
228
|
+
const detail = m.input?.command ?? m.input?.pattern ?? m.input?.query ?? m.input?.file_path ?? '';
|
|
229
|
+
progress.toolUse(tool, detail);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (m.summary) {
|
|
233
|
+
if (streamWriter) {
|
|
234
|
+
streamWriter.write({ type: 'tool_use_summary', summary: m.summary, output: m.output ?? '' });
|
|
235
|
+
}
|
|
236
|
+
if (progress) progress.toolSummary(m.summary);
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case 'step_usage': {
|
|
241
|
+
const inTok = m.input_tokens ?? 0;
|
|
242
|
+
const outTok = m.output_tokens ?? 0;
|
|
243
|
+
const ccit = m.cache_creation_input_tokens ?? 0;
|
|
244
|
+
const crit = m.cache_read_input_tokens ?? 0;
|
|
245
|
+
usageTotals.input_tokens += inTok;
|
|
246
|
+
usageTotals.output_tokens += outTok;
|
|
247
|
+
usageTotals.cache_creation_input_tokens += ccit;
|
|
248
|
+
usageTotals.cache_read_input_tokens += crit;
|
|
249
|
+
if (m.model) usageTotals.model = m.model;
|
|
250
|
+
const stepCost = m.cost_usd != null
|
|
251
|
+
? m.cost_usd
|
|
252
|
+
: calculateCost(m.model, inTok, outTok, ccit, crit);
|
|
253
|
+
usageTotals.cost_usd += stepCost;
|
|
254
|
+
if (streamWriter) {
|
|
255
|
+
streamWriter.write({
|
|
256
|
+
type: 'usage',
|
|
257
|
+
input_tokens: inTok,
|
|
258
|
+
output_tokens: outTok,
|
|
259
|
+
cache_creation_input_tokens: ccit,
|
|
260
|
+
cache_read_input_tokens: crit,
|
|
261
|
+
cost_usd: stepCost,
|
|
262
|
+
model: m.model ?? null,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
default:
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
181
272
|
if (maxDurationMs) {
|
|
182
|
-
|
|
273
|
+
timeoutHandle = setTimeout(() => {
|
|
183
274
|
timedOut = true;
|
|
184
|
-
|
|
275
|
+
stratum.cancelAgentRun(correlationId).catch(() => {});
|
|
185
276
|
}, maxDurationMs);
|
|
186
277
|
}
|
|
187
278
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
279
|
+
let onInterrupt = null;
|
|
280
|
+
if (progress?.on) {
|
|
281
|
+
onInterrupt = () => {
|
|
282
|
+
userInterruptAction = progress.consumeAction?.() ?? 'skip';
|
|
283
|
+
stratum.cancelAgentRun(correlationId).catch(() => {});
|
|
284
|
+
};
|
|
285
|
+
progress.on('interrupt', onInterrupt);
|
|
286
|
+
}
|
|
195
287
|
|
|
288
|
+
let runResult;
|
|
196
289
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
} else if (process.env.COMPOSE_DEBUG) {
|
|
211
|
-
process.stderr.write(` [event] type=${event.type} keys=${Object.keys(event).join(',')}\n`);
|
|
212
|
-
}
|
|
213
|
-
if (event.type === 'error') {
|
|
214
|
-
// build_error is written by build.js catch blocks, not here — avoids duplicate events
|
|
215
|
-
throw new AgentError(event.message);
|
|
216
|
-
}
|
|
217
|
-
if (event.type === 'assistant' && event.content) {
|
|
218
|
-
textParts.push(event.content);
|
|
219
|
-
if (streamWriter) {
|
|
220
|
-
streamWriter.write({ type: 'assistant', content: event.content });
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
if (event.type === 'result' && event.content) {
|
|
224
|
-
// Result contains the final aggregated text — use it if we got nothing from blocks
|
|
225
|
-
if (textParts.length === 0) {
|
|
226
|
-
textParts.push(event.content);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
// Forward tool_use to build stream (before progress logging)
|
|
230
|
-
if (streamWriter && event.type === 'tool_use' && event.tool) {
|
|
231
|
-
streamWriter.write({ type: 'tool_use', tool: event.tool, input: event.input });
|
|
232
|
-
}
|
|
233
|
-
// Passive tap — notify caller of each tool_use without altering event flow (Item 193)
|
|
234
|
-
if (onToolUse && event.type === 'tool_use' && event.tool) {
|
|
235
|
-
onToolUse({ tool: event.tool, input: event.input, timestamp: Date.now() });
|
|
236
|
-
}
|
|
237
|
-
// Progress logging — show tool calls so the user sees activity
|
|
238
|
-
if (event.type === 'tool_use' && event.tool) {
|
|
239
|
-
const detail = event.input?.command
|
|
240
|
-
?? event.input?.pattern
|
|
241
|
-
?? event.input?.query
|
|
242
|
-
?? event.input?.file_path
|
|
243
|
-
?? '';
|
|
244
|
-
if (progress) {
|
|
245
|
-
progress.toolUse(event.tool, detail);
|
|
246
|
-
} else {
|
|
247
|
-
const short = typeof detail === 'string' && detail.length > 60
|
|
248
|
-
? detail.slice(0, 57) + '...'
|
|
249
|
-
: detail;
|
|
250
|
-
process.stderr.write(` ↳ ${event.tool}${short ? ': ' + short : ''}\n`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
// Forward tool_use_summary to build stream (COMP-OBS-STREAM)
|
|
254
|
-
if (streamWriter && event.type === 'tool_use_summary') {
|
|
255
|
-
streamWriter.write({ type: 'tool_use_summary', summary: event.summary, output: event.output });
|
|
256
|
-
}
|
|
257
|
-
if (event.type === 'tool_use_summary' && event.summary) {
|
|
258
|
-
if (progress) {
|
|
259
|
-
progress.toolSummary(event.summary);
|
|
260
|
-
} else {
|
|
261
|
-
const short = event.summary.length > 80
|
|
262
|
-
? event.summary.slice(0, 77) + '...'
|
|
263
|
-
: event.summary;
|
|
264
|
-
process.stderr.write(` ↳ ${short}\n`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
// Forward tool_progress to build stream (COMP-OBS-STREAM)
|
|
268
|
-
if (streamWriter && event.type === 'tool_progress') {
|
|
269
|
-
streamWriter.write({ type: 'tool_progress', tool: event.tool, elapsed: event.elapsed });
|
|
270
|
-
}
|
|
271
|
-
if (event.type === 'tool_progress' && event.tool) {
|
|
272
|
-
if (progress) {
|
|
273
|
-
progress.toolProgress(event.tool, event.elapsed);
|
|
274
|
-
} else {
|
|
275
|
-
process.stderr.write(` ↳ ${event.tool} (${Math.round(event.elapsed)}s)\n`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
// Accumulate usage events (COMP-OBS-COST)
|
|
279
|
-
if (event.type === 'usage') {
|
|
280
|
-
usageTotals.input_tokens += event.input_tokens ?? 0;
|
|
281
|
-
usageTotals.output_tokens += event.output_tokens ?? 0;
|
|
282
|
-
usageTotals.cache_creation_input_tokens += event.cache_creation_input_tokens ?? 0;
|
|
283
|
-
usageTotals.cache_read_input_tokens += event.cache_read_input_tokens ?? 0;
|
|
284
|
-
if (event.model) usageTotals.model = event.model;
|
|
285
|
-
// If the connector already computed cost_usd, use it; otherwise calculate
|
|
286
|
-
const stepCost = event.cost_usd != null
|
|
287
|
-
? event.cost_usd
|
|
288
|
-
: calculateCost(
|
|
289
|
-
event.model,
|
|
290
|
-
event.input_tokens ?? 0,
|
|
291
|
-
event.output_tokens ?? 0,
|
|
292
|
-
event.cache_creation_input_tokens ?? 0,
|
|
293
|
-
event.cache_read_input_tokens ?? 0,
|
|
294
|
-
);
|
|
295
|
-
usageTotals.cost_usd += stepCost;
|
|
296
|
-
// Forward per-step usage to build stream
|
|
297
|
-
if (streamWriter) {
|
|
298
|
-
streamWriter.write({
|
|
299
|
-
type: 'usage',
|
|
300
|
-
input_tokens: event.input_tokens ?? 0,
|
|
301
|
-
output_tokens: event.output_tokens ?? 0,
|
|
302
|
-
cache_creation_input_tokens: event.cache_creation_input_tokens ?? 0,
|
|
303
|
-
cache_read_input_tokens: event.cache_read_input_tokens ?? 0,
|
|
304
|
-
cost_usd: stepCost,
|
|
305
|
-
model: event.model ?? null,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
290
|
+
runResult = await stratum.agentRun(agentType, actualPrompt, {
|
|
291
|
+
modelID: cfg.modelID ?? undefined,
|
|
292
|
+
allowedTools: cfg.allowedTools ?? undefined,
|
|
293
|
+
disallowedTools: cfg.disallowedTools ?? undefined,
|
|
294
|
+
thinking: cfg.thinking ?? undefined,
|
|
295
|
+
effort: cfg.effort ?? undefined,
|
|
296
|
+
cwd: opts.cwd ?? undefined,
|
|
297
|
+
correlationId,
|
|
298
|
+
});
|
|
299
|
+
} catch (err) {
|
|
300
|
+
if (timedOut) throw new AgentTimeoutError(stepId, Date.now() - startTime);
|
|
301
|
+
if (userInterruptAction) throw new UserInterruptError(stepId, userInterruptAction);
|
|
302
|
+
throw new AgentError(err?.message ?? 'Agent run failed');
|
|
310
303
|
} finally {
|
|
311
|
-
if (
|
|
312
|
-
if (progress?.removeListener) progress.removeListener('interrupt', onInterrupt);
|
|
304
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
305
|
+
if (onInterrupt && progress?.removeListener) progress.removeListener('interrupt', onInterrupt);
|
|
306
|
+
unsub();
|
|
313
307
|
}
|
|
314
308
|
|
|
315
|
-
|
|
316
|
-
if (
|
|
317
|
-
const stepId = stepDispatch.step_id ?? 'unknown';
|
|
318
|
-
throw new AgentTimeoutError(stepId, Date.now() - startTime);
|
|
319
|
-
}
|
|
309
|
+
if (timedOut) throw new AgentTimeoutError(stepId, Date.now() - startTime);
|
|
310
|
+
if (userInterruptAction) throw new UserInterruptError(stepId, userInterruptAction);
|
|
320
311
|
|
|
321
|
-
const text =
|
|
312
|
+
const text = (runResult && typeof runResult.text === 'string' && runResult.text.length > 0)
|
|
313
|
+
? runResult.text
|
|
314
|
+
: textParts.join('');
|
|
322
315
|
|
|
323
316
|
if (progress) {
|
|
324
317
|
progress.debug(`normalizer: textParts=${textParts.length}, text length=${text.length}`);
|
|
325
318
|
if (text.length > 0) progress.debug(`text preview: ${text.slice(0, 300)}`);
|
|
326
319
|
} else if (process.env.COMPOSE_DEBUG) {
|
|
327
|
-
process.stderr.write(` [normalizer] textParts
|
|
328
|
-
|
|
320
|
+
process.stderr.write(` [normalizer] textParts=${textParts.length}, text length=${text.length}\n`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// review_mode hook — MUST be before the !hasSchema early return (MF-3 in blueprint).
|
|
324
|
+
// Parallel lens steps often have empty output_fields (hasSchema=false), but review
|
|
325
|
+
// normalization must still run. The Stratum server validates the post-normalize result
|
|
326
|
+
// via `ensure` expressions after stratum_step_done — not against raw text.
|
|
327
|
+
if (opts.reviewMode === true) {
|
|
328
|
+
const reviewAgentType = agentType; // already resolved from stepDispatch.agent at line 178
|
|
329
|
+
const reviewModelId = usageTotals.model ?? cfg.modelID ?? null;
|
|
330
|
+
const repairFn = stratum
|
|
331
|
+
? async (repairPrompt) => {
|
|
332
|
+
const repairResult = await stratum.agentRun(reviewAgentType, repairPrompt, {
|
|
333
|
+
modelID: cfg.modelID ?? undefined,
|
|
334
|
+
cwd: opts.cwd ?? undefined,
|
|
335
|
+
});
|
|
336
|
+
return repairResult?.text ?? '';
|
|
337
|
+
}
|
|
338
|
+
: undefined;
|
|
339
|
+
const reviewResult = await normalizeReviewResult(text, {
|
|
340
|
+
agentType: reviewAgentType,
|
|
341
|
+
modelId: reviewModelId,
|
|
342
|
+
confidenceGate: opts.confidenceGate ?? 7,
|
|
343
|
+
lens: opts.lens ?? 'general',
|
|
344
|
+
repairFn,
|
|
345
|
+
});
|
|
346
|
+
return { text, result: reviewResult, usage: usageTotals };
|
|
329
347
|
}
|
|
330
348
|
|
|
331
349
|
if (!hasSchema) {
|
|
@@ -334,8 +352,6 @@ export async function runAndNormalize(connector, prompt, stepDispatch, opts = {}
|
|
|
334
352
|
|
|
335
353
|
const result = extractJson(text);
|
|
336
354
|
if (result === null) {
|
|
337
|
-
// Agent did its work but didn't return structured JSON.
|
|
338
|
-
// Log a warning and return a fallback — don't crash the pipeline.
|
|
339
355
|
if (progress) {
|
|
340
356
|
progress.warn('Could not extract JSON from agent output, using fallback');
|
|
341
357
|
} else {
|