@smartmemory/compose 0.2.57-beta → 0.3.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/README.md +2 -2
- package/bin/compose.js +55 -54
- package/contracts/feature-json.schema.json +16 -0
- package/dist/assets/{App-BmhlHOXF.js → App-DJ5xk_Wx.js} +219 -219
- package/dist/assets/{abnfDiagram-VRR7QNED-CMOTYAnt.js → abnfDiagram-VRR7QNED-BPWGdCFx.js} +1 -1
- package/dist/assets/{arc-BJAv_6dL.js → arc-DX5jqmcO.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-xa4DLUcp.js → architectureDiagram-ZJ3FMSHR-B6jmNVw7.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-BdkDqeQF.js → blockDiagram-677ZJIJ3-DAx2i-sB.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-DiNrLIuL.js → c4Diagram-LMCZKHZV-zmsZCplj.js} +1 -1
- package/dist/assets/channel-vsDnTvkh.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BsjtgrSL.js → chunk-2Q5K7J3B-3QSh6sI7.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-DpImULWy.js → chunk-32BRIVSS-BiH5Di_y.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-DTdG5JnR.js → chunk-5VM5RSS4-C0EfjFLd.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-Cj1jNuKY.js → chunk-EX3LRPZG-BGyfmVm-.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DwQ0wwWn.js → chunk-JWPE2WC7-Ba0mrNlg.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-DZZa7unc.js → chunk-MOJQB5TN-D3QO9EzH.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-CgdjqYQj.js → chunk-RYQCIY6F-D1uvNA6d.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DrPIj6XI.js → chunk-V7JOEXUC-fihlnodT.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-BBUOVC5W.js → chunk-VR4S4FIN-nmRUEo1H.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BEIiZ6gS.js → chunk-XXDRQBXY-CM693yEg.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-DO-wdSFZ.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-DO-wdSFZ.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-ClFTQEyD.js → cose-bilkent-JH36ORCC-CkacPn8W.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-5Bp7xFO0.js → cynefin-VYW2F7L2-BiHaIptG.js} +1 -1
- package/dist/assets/{cynefinDiagram-TSTJHNR4-C-0QlSda.js → cynefinDiagram-TSTJHNR4-CdKxMmiy.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-DLkqsfBy.js → dagre-VKFMJZFB-BQLY5y_W.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-Cwh_tJ_r.js → diagram-FQU43EPY-D7uMBHvq.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-CvvAjulf.js → diagram-G47NLZAW-B3Z1cuH7.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-CCoyx8_e.js → diagram-NH7WQ7WH-BZyRD45e.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-DfjhKYCp.js → diagram-OA4YK3LP-DcThOTt7.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-Vjz0ygfB.js → diagram-WEI45ONY-BcgRAkqY.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-GsA8c6g2.js → ebnfDiagram-CCIWWBDH-CGwfO_xH.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-Cc0lJ9sQ.js → erDiagram-Q63AITRT-pV58-Ncc.js} +1 -1
- package/dist/assets/{flowDiagram-23GEKE2U-C_sotK-k.js → flowDiagram-23GEKE2U-DJ_SqE8h.js} +1 -1
- package/dist/assets/{ganttDiagram-NO4QXBWP-BBtu_WUe.js → ganttDiagram-NO4QXBWP-Dgy0Iyss.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-BV_aSB8-.js → gitGraphDiagram-IHSO6WYX-CbiZw9fb.js} +1 -1
- package/dist/assets/{index-CKfOVv2N.js → index-DZTJEk-y.js} +2 -2
- package/dist/assets/{infoDiagram-FWYZ7A6U-DMAvNGtX.js → infoDiagram-FWYZ7A6U-CtsyyEc-.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-DfQB91MZ.js → ishikawaDiagram-FXEZZL3T-BJilNFkK.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bq4Llm9o.js → journeyDiagram-5HDEW3XC-C2UCMP4t.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-DTXHiDiE.js → kanban-definition-HUTT4EX6-DmLDJBRy.js} +1 -1
- package/dist/assets/{linear-BQSYZcVg.js → linear-C1paCqE7.js} +1 -1
- package/dist/assets/{mindmap-definition-LN4V7U3C-KuH2NIj0.js → mindmap-definition-LN4V7U3C-CX-RxKVn.js} +1 -1
- package/dist/assets/{pegDiagram-2B236MQR-BKoNGQFr.js → pegDiagram-2B236MQR-CTU32H2W.js} +1 -1
- package/dist/assets/{pieDiagram-ENE6RG2P-EpYv162Y.js → pieDiagram-ENE6RG2P-D8L1aYoo.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-C9deUjQ2.js → quadrantDiagram-ABIIQ3AL-hQ3bXosy.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-Csqw2hK9.js → railroadDiagram-RFXS5EU6--_vuYcda.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-DCuz3emr.js → requirementDiagram-TGXJPOKE-C6h_m3Az.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-BTeTL24f.js → sankeyDiagram-HTMAVEWB-Dpc7CfIQ.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-Cf1ZkTwO.js → sequenceDiagram-DBY2YBRQ-CK5ZzbvJ.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-D3Lbewzg.js → sizeCapture-X5ZJPWSS-BAxVvM9G.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-O8l37yJI.js → stateDiagram-2N3HPSRC-7j33_2PY.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-GviN0FpJ.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-BikmTh8b.js → swimlanes-5IMT3BWC-CBG2yOob.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-D0wMoxl1.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-BOxSosG0.js → timeline-definition-FHXFAJF6-BOnZkQvc.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-BSClOErZ.js → vennDiagram-L72KCM5P-D52NhIfj.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-DHFRjOK0.js → wardleyDiagram-EHGQE667-YoPY2ZU-.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-Dh1v4x-O.js → xychartDiagram-FW5EYKEG-BanNfgtO.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/build-all.js +0 -5
- package/lib/build-stream-schema.js +1 -1
- package/lib/build.js +1809 -2646
- package/lib/consumer-fanout.js +1317 -0
- package/lib/escalation.js +69 -0
- package/lib/feature-validator.js +6 -4
- package/lib/feature-writer.js +58 -3
- package/lib/flow-state.js +15 -14
- package/lib/gsd-budget.js +48 -10
- package/lib/gsd-prompt.js +3 -4
- package/lib/gsd-stuck.js +1 -1
- package/lib/gsd.js +303 -149
- package/lib/lane-gate.js +200 -0
- package/lib/local-claude-connector.js +149 -0
- package/lib/new.js +162 -307
- package/lib/result-normalizer.js +233 -18
- package/lib/review-lenses.js +1 -1
- package/lib/step-prompt.js +41 -119
- package/lib/stratum-engine.js +297 -0
- package/lib/stratum-mcp-client.js +224 -213
- package/lib/triage.js +144 -0
- package/lib/vocabulary-compliance.js +268 -0
- package/lib/vocabulary-inject.js +1 -36
- package/package.json +2 -1
- package/pipelines/build-quick.stratum.yaml +5 -17
- package/pipelines/build.profiles.json +11 -0
- package/pipelines/build.stratum.yaml +288 -467
- package/pipelines/gsd.stratum.yaml +73 -125
- package/pipelines/new.stratum.yaml +68 -149
- package/server/build-routes.js +4 -1
- package/server/design-routes.js +37 -21
- package/server/index.js +13 -21
- package/server/lifecycle-guard.js +1 -1
- package/server/pipeline-routes.js +113 -31
- package/server/stratum-client.js +33 -47
- package/server/stratum-sync.js +3 -4
- package/server/vision-server.js +1 -1
- package/dist/assets/channel-CYErfopw.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-qCC_hnXu.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-qCC_hnXu.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-BWqk_9py.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-C60ICdks.js +0 -8
- package/lib/connector-factory-shim.js +0 -167
- package/server/agent-mcp.js +0 -10
package/lib/result-normalizer.js
CHANGED
|
@@ -13,6 +13,7 @@ import { calculateCost } from './model-pricing.js';
|
|
|
13
13
|
import { resolveAgentConfig } from './agent-string.js';
|
|
14
14
|
import { normalizeReviewResult } from './review-normalize.js';
|
|
15
15
|
import { KNOWN_VERSIONS } from './build-stream-schema.js';
|
|
16
|
+
import { runLocalClaudeAgent } from './local-claude-connector.js';
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Error classes
|
|
@@ -83,6 +84,65 @@ export function outputFieldsToJsonSchema(outputFields) {
|
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Build a nested JSON Schema from a consumer descriptor's contract CLOSURE
|
|
89
|
+
* (`{ root, contracts }`), resolving the engine's type grammar so the agent-facing
|
|
90
|
+
* schema — and thus the ENGINE's strict validation — see the same shapes:
|
|
91
|
+
* - `X?` optional field (omitted from `required`)
|
|
92
|
+
* - `X[]` typed array (recursively; nests as `X[][]`)
|
|
93
|
+
* - `(a|b)[]` enum array
|
|
94
|
+
* - `a|b` enum
|
|
95
|
+
* - `Name` named record reference, resolved against the closure
|
|
96
|
+
* - primitives / `object` / `array` as before
|
|
97
|
+
*
|
|
98
|
+
* The flat `outputFieldsToJsonSchema` only mapped primitives, so a named record
|
|
99
|
+
* (`Artifact`) or typed array (`Artifact[]`) degraded to `{}`, leaving the agent
|
|
100
|
+
* blind to nested fields the engine still requires. Returns null for a malformed
|
|
101
|
+
* closure so callers can fall back to the flat schema.
|
|
102
|
+
*/
|
|
103
|
+
export function contractClosureToJsonSchema(closure) {
|
|
104
|
+
if (!closure || typeof closure !== 'object') return null;
|
|
105
|
+
const { root, contracts } = closure;
|
|
106
|
+
if (typeof root !== 'string' || !contracts || typeof contracts !== 'object') return null;
|
|
107
|
+
if (!Object.hasOwn(contracts, root)) return null;
|
|
108
|
+
|
|
109
|
+
function typeToSchema(typeStr, seen) {
|
|
110
|
+
let raw = String(typeStr);
|
|
111
|
+
let optional = false;
|
|
112
|
+
if (raw.endsWith('?')) { optional = true; raw = raw.slice(0, -1); }
|
|
113
|
+
|
|
114
|
+
const enumArray = /^\(([^()]+)\)\[\]$/.exec(raw);
|
|
115
|
+
if (enumArray) {
|
|
116
|
+
return { optional, schema: { type: 'array', items: { enum: enumArray[1].split('|') } } };
|
|
117
|
+
}
|
|
118
|
+
if (raw.endsWith('[]')) {
|
|
119
|
+
return { optional, schema: { type: 'array', items: typeToSchema(raw.slice(0, -2), seen).schema } };
|
|
120
|
+
}
|
|
121
|
+
const lower = raw.toLowerCase();
|
|
122
|
+
if (TYPE_MAP[lower]) return { optional, schema: { ...TYPE_MAP[lower] } };
|
|
123
|
+
if (Object.hasOwn(contracts, raw)) return { optional, schema: buildRecord(raw, seen) };
|
|
124
|
+
if (raw.includes('|')) return { optional, schema: { enum: raw.split('|') } };
|
|
125
|
+
return { optional, schema: {} }; // unknown → unconstrained
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildRecord(name, seen) {
|
|
129
|
+
if (seen.has(name)) return { type: 'object' }; // cycle guard (engine forbids recursion)
|
|
130
|
+
const fields = contracts[name];
|
|
131
|
+
if (!fields || typeof fields !== 'object') return { type: 'object' };
|
|
132
|
+
const nextSeen = new Set(seen).add(name);
|
|
133
|
+
const properties = {};
|
|
134
|
+
const required = [];
|
|
135
|
+
for (const [field, typeStr] of Object.entries(fields)) {
|
|
136
|
+
const { schema, optional } = typeToSchema(typeStr, nextSeen);
|
|
137
|
+
properties[field] = schema;
|
|
138
|
+
if (!optional) required.push(field);
|
|
139
|
+
}
|
|
140
|
+
return { type: 'object', required, properties };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return buildRecord(root, new Set());
|
|
144
|
+
}
|
|
145
|
+
|
|
86
146
|
// ---------------------------------------------------------------------------
|
|
87
147
|
// JSON extraction helpers
|
|
88
148
|
// ---------------------------------------------------------------------------
|
|
@@ -150,6 +210,20 @@ export class AgentTimeoutError extends Error {
|
|
|
150
210
|
}
|
|
151
211
|
}
|
|
152
212
|
|
|
213
|
+
/**
|
|
214
|
+
* D3: raised when an `onAgentEvent` observer asks to stop an in-flight agent run
|
|
215
|
+
* (e.g. the GSD stuck detector tripped mid-execution). Carries the observer's
|
|
216
|
+
* reason so the caller can render a diagnostic and halt.
|
|
217
|
+
*/
|
|
218
|
+
export class AgentAbortedError extends Error {
|
|
219
|
+
constructor(stepId, reason) {
|
|
220
|
+
super(`Agent run on step "${stepId}" aborted by observer`);
|
|
221
|
+
this.name = 'AgentAbortedError';
|
|
222
|
+
this.stepId = stepId;
|
|
223
|
+
this.reason = reason;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
153
227
|
/**
|
|
154
228
|
* STRAT-DEDUP-AGENTRUN-V3: `runAndNormalize` is now a thin wrapper around the
|
|
155
229
|
* Python connector tier exposed through `stratum_agent_run`. Events arrive as
|
|
@@ -178,13 +252,35 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
178
252
|
|
|
179
253
|
const stepId = stepDispatch.step_id ?? 'unknown';
|
|
180
254
|
const agentType = stepDispatch.agent ?? 'claude';
|
|
181
|
-
|
|
255
|
+
// D6: the engine ships only the bare provider literal (stepDispatch.agent), so
|
|
256
|
+
// the full profile string (with tool restrictions + model tier) is supplied
|
|
257
|
+
// compose-side via opts.profile, keyed off the compose-owned sidecar. It
|
|
258
|
+
// overrides the bare literal for capability resolution; the provider is
|
|
259
|
+
// unchanged. Absent → bare literal (no restrictions), preserving old behavior.
|
|
260
|
+
const cfg = resolveAgentConfig(opts.profile || agentType);
|
|
261
|
+
// A read-only profile (no Edit/Write/Bash) also maps to a read-only sandbox so
|
|
262
|
+
// the restriction binds at the engine's connector, not just at this invocation.
|
|
263
|
+
const readOnlyProfile = Array.isArray(cfg.disallowedTools)
|
|
264
|
+
&& ['Edit', 'Write'].every((tool) => cfg.disallowedTools.includes(tool));
|
|
265
|
+
const sandboxMode = readOnlyProfile ? 'read-only' : undefined;
|
|
182
266
|
|
|
183
267
|
const outputFields = stepDispatch.output_fields;
|
|
184
268
|
const hasSchema = outputFields && typeof outputFields === 'object' && Object.keys(outputFields).length > 0;
|
|
185
269
|
let actualPrompt = prompt;
|
|
186
270
|
let schema = null;
|
|
187
|
-
|
|
271
|
+
// Consumer-fanout dispatch carries the full contract CLOSURE so nested named
|
|
272
|
+
// records and typed arrays reach the agent (and match the engine's strict
|
|
273
|
+
// validation). Fall back to the flat field→primitive schema otherwise.
|
|
274
|
+
const closureSchema = contractClosureToJsonSchema(stepDispatch.output_contract_closure);
|
|
275
|
+
// A valid but EMPTY root contract ({}) yields zero fields, so `hasSchema` is
|
|
276
|
+
// false while a real (empty-object) closure schema exists. Structured
|
|
277
|
+
// extraction must be gated on "a contract is declared", not "the root has
|
|
278
|
+
// fields" — otherwise the agent's `{}` is discarded and the item wedges.
|
|
279
|
+
const hasStructuredOutput = Boolean(closureSchema) || hasSchema;
|
|
280
|
+
if (closureSchema) {
|
|
281
|
+
schema = closureSchema;
|
|
282
|
+
actualPrompt = injectSchema(prompt, schema);
|
|
283
|
+
} else if (hasSchema) {
|
|
188
284
|
schema = outputFieldsToJsonSchema(outputFields);
|
|
189
285
|
actualPrompt = injectSchema(prompt, schema);
|
|
190
286
|
}
|
|
@@ -206,6 +302,25 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
206
302
|
let timedOut = false;
|
|
207
303
|
let userInterruptAction = null;
|
|
208
304
|
let timeoutHandle = null;
|
|
305
|
+
// D3: an onAgentEvent observer (e.g. the GSD stuck detector) may ask to stop
|
|
306
|
+
// the run mid-stream by returning a truthy reason from a tool event.
|
|
307
|
+
let abortReason = null;
|
|
308
|
+
|
|
309
|
+
// V2/V3: CONTROLLED claude executions (consumer items, review fanout) run via
|
|
310
|
+
// the compose-LOCAL connector — the only seam that can enforce claude tool
|
|
311
|
+
// restrictions AND be interrupted (the sync engine agent_run can do neither;
|
|
312
|
+
// its background mode is codex+read-only-only). Abort replaces cancelAgentRun
|
|
313
|
+
// as the stop mechanism, and the connector streams real tool events so the
|
|
314
|
+
// stuck detector / timeout actually stop a runaway agent.
|
|
315
|
+
const useLocalClaude = opts.localExecution === true && cfg.provider === 'claude';
|
|
316
|
+
const abortController = useLocalClaude ? new AbortController() : null;
|
|
317
|
+
const stopRun = () => {
|
|
318
|
+
if (abortController) {
|
|
319
|
+
try { abortController.abort(); } catch { /* already aborted */ }
|
|
320
|
+
} else {
|
|
321
|
+
stratum.cancelAgentRun(correlationId).catch(() => {});
|
|
322
|
+
}
|
|
323
|
+
};
|
|
209
324
|
|
|
210
325
|
// Subscribe BEFORE calling agentRun — events fire during the call.
|
|
211
326
|
const unsub = stratum.onEvent(correlationId, subStepId, (env) => {
|
|
@@ -271,12 +386,22 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
271
386
|
default:
|
|
272
387
|
break;
|
|
273
388
|
}
|
|
389
|
+
// D3: let an observer inspect every processed envelope and request a stop
|
|
390
|
+
// (returns a truthy reason). Cancel the in-flight run once; the post-run
|
|
391
|
+
// guard rethrows as AgentAbortedError.
|
|
392
|
+
if (opts.onAgentEvent) {
|
|
393
|
+
const stop = opts.onAgentEvent(env);
|
|
394
|
+
if (stop && !abortReason) {
|
|
395
|
+
abortReason = stop;
|
|
396
|
+
stopRun();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
274
399
|
});
|
|
275
400
|
|
|
276
401
|
if (maxDurationMs) {
|
|
277
402
|
timeoutHandle = setTimeout(() => {
|
|
278
403
|
timedOut = true;
|
|
279
|
-
|
|
404
|
+
stopRun();
|
|
280
405
|
}, maxDurationMs);
|
|
281
406
|
}
|
|
282
407
|
|
|
@@ -284,34 +409,118 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
284
409
|
if (progress?.on) {
|
|
285
410
|
onInterrupt = () => {
|
|
286
411
|
userInterruptAction = progress.consumeAction?.() ?? 'skip';
|
|
287
|
-
|
|
412
|
+
stopRun();
|
|
288
413
|
};
|
|
289
414
|
progress.on('interrupt', onInterrupt);
|
|
290
415
|
}
|
|
291
416
|
|
|
417
|
+
// V2/V3: on the local path the connector's tool_use events (not engine
|
|
418
|
+
// progress notifications) drive narration + the stuck detector. This bridge
|
|
419
|
+
// mirrors the engine onEvent handler's tool handling and lets the observer
|
|
420
|
+
// abort a spinning run.
|
|
421
|
+
const localOnToolUse = ({ tool, input }) => {
|
|
422
|
+
if (onToolUse) onToolUse({ tool, input, timestamp: Date.now() });
|
|
423
|
+
if (streamWriter) streamWriter.write({ type: 'tool_use', tool, input: input ?? {} });
|
|
424
|
+
if (progress) progress.toolUse(tool, input?.command ?? input?.pattern ?? input?.file_path ?? '');
|
|
425
|
+
if (opts.onAgentEvent && !abortReason) {
|
|
426
|
+
const env = { schema_version: '0.2.6', kind: 'tool_use_summary', metadata: { tool, input: input ?? {}, summary: '', output: '' } };
|
|
427
|
+
const stop = opts.onAgentEvent(env);
|
|
428
|
+
if (stop) { abortReason = stop; stopRun(); }
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
292
432
|
let runResult;
|
|
293
433
|
try {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
434
|
+
if (useLocalClaude) {
|
|
435
|
+
// Test seam: an installed factory shim exposes an SDK-shaped query adapter
|
|
436
|
+
// so the goldens drive the local path without spawning a real claude.
|
|
437
|
+
// Gated on NODE_ENV=test so production always uses the real SDK.
|
|
438
|
+
const localQuery = opts.localQuery
|
|
439
|
+
?? (process.env.NODE_ENV === 'test' && stratum ? stratum._localQuery : undefined);
|
|
440
|
+
runResult = await runLocalClaudeAgent(actualPrompt, {
|
|
441
|
+
cwd: opts.cwd ?? undefined,
|
|
442
|
+
model: cfg.modelID ?? undefined,
|
|
443
|
+
allowedTools: cfg.allowedTools ?? undefined,
|
|
444
|
+
disallowedTools: cfg.disallowedTools ?? undefined,
|
|
445
|
+
thinking: cfg.thinking ?? undefined,
|
|
446
|
+
abortController,
|
|
447
|
+
onToolUse: localOnToolUse,
|
|
448
|
+
...(localQuery ? { query: localQuery } : {}),
|
|
449
|
+
});
|
|
450
|
+
} else {
|
|
451
|
+
runResult = await stratum.agentRun(agentType, actualPrompt, {
|
|
452
|
+
modelID: cfg.modelID ?? undefined,
|
|
453
|
+
allowedTools: cfg.allowedTools ?? undefined,
|
|
454
|
+
disallowedTools: cfg.disallowedTools ?? undefined,
|
|
455
|
+
thinking: cfg.thinking ?? undefined,
|
|
456
|
+
effort: cfg.effort ?? undefined,
|
|
457
|
+
sandboxMode,
|
|
458
|
+
cwd: opts.cwd ?? undefined,
|
|
459
|
+
correlationId,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
303
462
|
} catch (err) {
|
|
304
|
-
|
|
463
|
+
// F3/G3: preserve any billable usage the failed run reported (the local
|
|
464
|
+
// connector attaches it on a non-success result / usage-bearing rejection) so
|
|
465
|
+
// the consumer failure envelope can still debit the engine/GSD ledgers. G3:
|
|
466
|
+
// the timeout/abort throws happen here too — attach the usage to THOSE errors
|
|
467
|
+
// (not only the generic AgentError) so timeout/stuck attempts are billed.
|
|
468
|
+
const errUsage = (err && typeof err === 'object' && err.usage) ? err.usage : null;
|
|
469
|
+
if (timedOut) {
|
|
470
|
+
const e = new AgentTimeoutError(stepId, Date.now() - startTime);
|
|
471
|
+
if (errUsage) e.usage = errUsage;
|
|
472
|
+
throw e;
|
|
473
|
+
}
|
|
305
474
|
if (userInterruptAction) throw new UserInterruptError(stepId, userInterruptAction);
|
|
306
|
-
|
|
475
|
+
if (abortReason) {
|
|
476
|
+
const e = new AgentAbortedError(stepId, abortReason);
|
|
477
|
+
if (errUsage) e.usage = errUsage;
|
|
478
|
+
throw e;
|
|
479
|
+
}
|
|
480
|
+
const agentError = new AgentError(err?.message ?? 'Agent run failed');
|
|
481
|
+
if (errUsage) agentError.usage = errUsage;
|
|
482
|
+
throw agentError;
|
|
307
483
|
} finally {
|
|
308
484
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
309
485
|
if (onInterrupt && progress?.removeListener) progress.removeListener('interrupt', onInterrupt);
|
|
310
486
|
unsub();
|
|
311
487
|
}
|
|
312
488
|
|
|
313
|
-
|
|
489
|
+
// H2: when the underlying run RESOLVES late (rather than rejecting) after a
|
|
490
|
+
// timeout/abort fired, its billable usage is on runResult.usage. Copy it onto
|
|
491
|
+
// the thrown error — the same single channel the rejection path (G3) uses — so
|
|
492
|
+
// the consumer timeout envelope / stuck-ledger accounting still bill the attempt.
|
|
493
|
+
// Throwing here means usageTotals is never returned, so this is the ONLY channel
|
|
494
|
+
// (no double count).
|
|
495
|
+
const lateUsage = (runResult && typeof runResult === 'object' && runResult.usage && typeof runResult.usage === 'object')
|
|
496
|
+
? runResult.usage
|
|
497
|
+
: null;
|
|
498
|
+
if (timedOut) {
|
|
499
|
+
const e = new AgentTimeoutError(stepId, Date.now() - startTime);
|
|
500
|
+
if (lateUsage) e.usage = lateUsage;
|
|
501
|
+
throw e;
|
|
502
|
+
}
|
|
314
503
|
if (userInterruptAction) throw new UserInterruptError(stepId, userInterruptAction);
|
|
504
|
+
if (abortReason) {
|
|
505
|
+
const e = new AgentAbortedError(stepId, abortReason);
|
|
506
|
+
if (lateUsage) e.usage = lateUsage;
|
|
507
|
+
throw e;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// D2(b): the TS agent_run path returns a synchronous `complete` envelope with
|
|
511
|
+
// aggregate usage ({usd?, tokens, ms}) and streams NO step_usage progress
|
|
512
|
+
// events — without folding it in, budget accounting debits nothing on the TS
|
|
513
|
+
// route. The python / factory-shim path streams step_usage events (usageTotals
|
|
514
|
+
// already populated), so adopt runResult.usage only when the event stream
|
|
515
|
+
// contributed nothing (avoids double counting).
|
|
516
|
+
const runUsage = runResult && typeof runResult === 'object' ? runResult.usage : null;
|
|
517
|
+
const usageFromEvents = usageTotals.input_tokens || usageTotals.output_tokens || usageTotals.cost_usd;
|
|
518
|
+
if (runUsage && typeof runUsage === 'object' && !usageFromEvents) {
|
|
519
|
+
if (typeof runUsage.tokens === 'number') usageTotals.output_tokens += runUsage.tokens;
|
|
520
|
+
if (typeof runUsage.usd === 'number') usageTotals.cost_usd += runUsage.usd;
|
|
521
|
+
if (typeof runUsage.ms === 'number') usageTotals.duration_ms = (usageTotals.duration_ms ?? 0) + runUsage.ms;
|
|
522
|
+
if (!usageTotals.model && runResult.telemetry?.model) usageTotals.model = runResult.telemetry.model;
|
|
523
|
+
}
|
|
315
524
|
|
|
316
525
|
const text = (runResult && typeof runResult.text === 'string' && runResult.text.length > 0)
|
|
317
526
|
? runResult.text
|
|
@@ -350,7 +559,7 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
350
559
|
return { text, result: reviewResult, usage: usageTotals };
|
|
351
560
|
}
|
|
352
561
|
|
|
353
|
-
if (!
|
|
562
|
+
if (!hasStructuredOutput) {
|
|
354
563
|
return { text, result: null, usage: usageTotals };
|
|
355
564
|
}
|
|
356
565
|
|
|
@@ -362,7 +571,13 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
362
571
|
process.stderr.write(' ⚠ Could not extract JSON from agent output, using fallback\n');
|
|
363
572
|
}
|
|
364
573
|
const summary = text.slice(0, 200).replace(/\n/g, ' ').trim();
|
|
365
|
-
|
|
574
|
+
const normalizationFailure = summary || 'Could not extract structured output';
|
|
575
|
+
return {
|
|
576
|
+
text,
|
|
577
|
+
result: { summary: normalizationFailure },
|
|
578
|
+
usage: usageTotals,
|
|
579
|
+
normalizationFailure,
|
|
580
|
+
};
|
|
366
581
|
}
|
|
367
582
|
|
|
368
583
|
return { text, result, usage: usageTotals };
|
package/lib/review-lenses.js
CHANGED
|
@@ -158,7 +158,7 @@ export function shouldRunCrossModel(filesChanged, lineCount = null) {
|
|
|
158
158
|
* Triage: decide which lenses to activate.
|
|
159
159
|
*
|
|
160
160
|
* @param {string[]} fileList - list of changed file paths
|
|
161
|
-
* @returns {Array<object>}
|
|
161
|
+
* @returns {Array<object>} lens items for TS consumer fanout
|
|
162
162
|
*/
|
|
163
163
|
export function triageLenses(fileList) {
|
|
164
164
|
const activeLensIds = [...BASELINE_LENSES];
|
package/lib/step-prompt.js
CHANGED
|
@@ -71,6 +71,36 @@ export function buildStepPrompt(stepDispatch, context) {
|
|
|
71
71
|
|
|
72
72
|
sections.push(`## Intent\n${stepDispatch.intent}`);
|
|
73
73
|
|
|
74
|
+
// D7(a): restore the exact TaskResult filename contract. The engine renders
|
|
75
|
+
// only whole `${item}` (dotted `${item.id}` is REF_INVALID), so the per-item
|
|
76
|
+
// output path is rendered compose-side from the item's id. When the consumer
|
|
77
|
+
// loop supplies it, spell out the exact path so the agent cannot misname the
|
|
78
|
+
// file the blackboard reader expects.
|
|
79
|
+
if (context.taskResultPath) {
|
|
80
|
+
sections.push(
|
|
81
|
+
`## TaskResult Output File\n`
|
|
82
|
+
+ `Write your TaskResult JSON to EXACTLY this path (do not choose a different name):\n`
|
|
83
|
+
+ `\`${context.taskResultPath}\``,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A retried issuance (e.g. a consumer-fanout item that failed its contract,
|
|
88
|
+
// ensure, or connector on a prior attempt) carries the engine's structured
|
|
89
|
+
// previousFailure. Render its reason so the retry can fix the cause rather
|
|
90
|
+
// than repeat it.
|
|
91
|
+
if (stepDispatch.previousFailure) {
|
|
92
|
+
const pf = stepDispatch.previousFailure;
|
|
93
|
+
const reason = typeof pf === 'string'
|
|
94
|
+
? pf
|
|
95
|
+
: (typeof pf.reason === 'string' ? pf.reason : JSON.stringify(pf));
|
|
96
|
+
if (reason) {
|
|
97
|
+
const attemptNote = typeof pf === 'object' && Number.isInteger(pf.attempt)
|
|
98
|
+
? ` (attempt ${pf.attempt})`
|
|
99
|
+
: '';
|
|
100
|
+
sections.push(`## Previous Attempt Failed${attemptNote}\nYour previous attempt was rejected. Fix the cause before finishing:\n${reason}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
74
104
|
sections.push(`## Inputs\n${JSON.stringify(stepDispatch.inputs, null, 2)}`);
|
|
75
105
|
|
|
76
106
|
if (Array.isArray(stepDispatch.output_fields) && stepDispatch.output_fields.length > 0) {
|
|
@@ -133,127 +163,19 @@ export function buildStepPrompt(stepDispatch, context) {
|
|
|
133
163
|
}
|
|
134
164
|
}
|
|
135
165
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const lines = [
|
|
147
|
-
'## File Ownership Conflicts — Resolution Required',
|
|
148
|
-
'',
|
|
149
|
-
'The following task pairs share `files_owned` entries but have no `depends_on`',
|
|
150
|
-
'relationship. Independent tasks may not both claim the same file.',
|
|
151
|
-
'Add a `depends_on` edge from the later task to the earlier task to resolve each conflict:',
|
|
152
|
-
'',
|
|
153
|
-
];
|
|
154
|
-
|
|
155
|
-
for (const { task_a, task_b, files } of conflicts) {
|
|
156
|
-
lines.push(`- **${task_a}** and **${task_b}** both own:`);
|
|
157
|
-
for (const f of files) lines.push(` - \`${f}\``);
|
|
158
|
-
lines.push(` → Add \`depends_on: [${task_a}]\` to \`${task_b}\` (or vice versa).`);
|
|
159
|
-
lines.push('');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return lines.join('\n');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// COMP-PAR-MERGE-QUEUE-CONSUMER-RETRY: pre-merge bounce context (gate_failed /
|
|
166
|
-
// merge_conflict) is injected into a re-dispatched task's prompt on BOTH paths:
|
|
167
|
-
// - server-dispatch: SERVER-SIDE in Stratum's ParallelExecutor._render_prompt
|
|
168
|
-
// (the server re-resolves the task list from flow state on each re-dispatch).
|
|
169
|
-
// - consumer-dispatch: COMPOSE-SIDE via formatBounceForPrompt() below, appended
|
|
170
|
-
// in executeParallelDispatch's retry loop (Compose, not Stratum, builds the
|
|
171
|
-
// per-task prompt on that path). This is the Compose mirror of
|
|
172
|
-
// parallel_exec.py `_format_bounce_for_prompt`.
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Format a ParMergeBounce record (contracts/par-merge-bounce.json) as a prompt
|
|
176
|
-
* section telling a re-dispatched task its prior attempt was rejected before
|
|
177
|
-
* merge. Mirrors Stratum's `_format_bounce_for_prompt` (parallel_exec.py).
|
|
178
|
-
* Degrades to '' on null/non-object input so a malformed bounce never breaks a
|
|
179
|
-
* re-run prompt.
|
|
180
|
-
*
|
|
181
|
-
* @param {{task_id?:string, reason?:string, command?:string|null,
|
|
182
|
-
* exit_code?:number|null, files?:string[], excerpt?:string}} bounce
|
|
183
|
-
* @returns {string}
|
|
184
|
-
*/
|
|
185
|
-
export function formatBounceForPrompt(bounce) {
|
|
186
|
-
if (!bounce || typeof bounce !== 'object') return '';
|
|
187
|
-
const files = Array.isArray(bounce.files) && bounce.files.length
|
|
188
|
-
? bounce.files.join(', ')
|
|
189
|
-
: '(none reported)';
|
|
190
|
-
const lines = ['## Previous attempt was rejected before merge — fix this before finishing'];
|
|
191
|
-
if (bounce.reason === 'gate_failed') {
|
|
192
|
-
const code = bounce.exit_code == null ? '?' : String(bounce.exit_code);
|
|
193
|
-
lines.push(`Your last attempt FAILED the pre-merge gate \`${bounce.command ?? '?'}\` (exit ${code}). It was not merged.`);
|
|
194
|
-
} else if (bounce.reason === 'merge_conflict') {
|
|
195
|
-
lines.push("Your last attempt produced changes that CONFLICTED with another task's changes at merge time. It was not merged.");
|
|
196
|
-
} else {
|
|
197
|
-
lines.push('Your last attempt was rejected before merge.');
|
|
198
|
-
}
|
|
199
|
-
lines.push(`Files involved: ${files}`);
|
|
200
|
-
if (bounce.excerpt) {
|
|
201
|
-
lines.push('Failure output:', '```', String(bounce.excerpt), '```');
|
|
202
|
-
}
|
|
203
|
-
return lines.join('\n');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Build a retry prompt when postconditions failed.
|
|
208
|
-
*
|
|
209
|
-
* @param {object} stepDispatch - Original step dispatch
|
|
210
|
-
* @param {string[]} violations - List of postcondition violations
|
|
211
|
-
* @param {object} context - Execution context
|
|
212
|
-
* @param {Array<{task_a, task_b, files}>} [conflicts] - Structured file conflicts (optional)
|
|
213
|
-
* @returns {string}
|
|
214
|
-
*/
|
|
215
|
-
export function buildRetryPrompt(stepDispatch, violations, context, conflicts) {
|
|
216
|
-
const violationLines = violations.map(v => `- ${v}`).join('\n');
|
|
217
|
-
const header = `RETRY — Previous attempt failed postconditions:\n${violationLines}\n\nFix these issues and try again.`;
|
|
218
|
-
|
|
219
|
-
const sections = [header, buildStepPrompt(stepDispatch, context)];
|
|
220
|
-
|
|
221
|
-
if (conflicts && conflicts.length > 0) {
|
|
222
|
-
sections.push(buildConflictSection(conflicts));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
let prompt = sections.join('\n\n');
|
|
226
|
-
|
|
227
|
-
// COMP-FIX-HARD T6: in bug-mode diagnose retries, prepend a digest of
|
|
228
|
-
// previously rejected hypotheses so the next attempt avoids dead ends.
|
|
229
|
-
// Guard: silent no-op if any precondition fails (regression-safe).
|
|
230
|
-
if (
|
|
231
|
-
context && context.mode === 'bug'
|
|
232
|
-
&& stepDispatch && stepDispatch.step_id === 'diagnose'
|
|
233
|
-
&& context.bug_code && context.cwd
|
|
234
|
-
) {
|
|
235
|
-
try {
|
|
236
|
-
const entries = readHypotheses(context.cwd, context.bug_code);
|
|
237
|
-
const block = formatRejectedHypotheses(entries);
|
|
238
|
-
if (block) prompt = block + '\n' + prompt;
|
|
239
|
-
} catch {
|
|
240
|
-
// best-effort: never let ledger I/O break a retry
|
|
241
|
-
}
|
|
166
|
+
// COMP-FIX-HARD T6: on a bug-mode DIAGNOSE (re)attempt, surface the previously
|
|
167
|
+
// REJECTED hypotheses from the per-bug ledger at the very TOP of the prompt so
|
|
168
|
+
// the agent does not re-propose a diagnosis already ruled out. The TS diagnose
|
|
169
|
+
// retry reissues through buildStepPrompt (there is no separate buildRetryPrompt
|
|
170
|
+
// in v1), so the ledger context — which previousFailure.reason alone dropped —
|
|
171
|
+
// is restored here. Only bug mode + the diagnose step + a populated ledger of
|
|
172
|
+
// rejected entries renders anything; every other step is byte-identical.
|
|
173
|
+
if (context.mode === 'bug' && context.bug_code && stepDispatch.step_id === 'diagnose') {
|
|
174
|
+
const rejectedBlock = formatRejectedHypotheses(readHypotheses(context.cwd, context.bug_code));
|
|
175
|
+
if (rejectedBlock) sections.unshift(rejectedBlock.trimEnd());
|
|
242
176
|
}
|
|
243
177
|
|
|
244
|
-
return
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Build a prompt for a child flow step within a larger workflow.
|
|
249
|
-
*
|
|
250
|
-
* @param {object} flowDispatch - Flow dispatch (child_flow_name, child_step)
|
|
251
|
-
* @param {object} context - Execution context
|
|
252
|
-
* @returns {string}
|
|
253
|
-
*/
|
|
254
|
-
export function buildFlowStepPrompt(flowDispatch, context) {
|
|
255
|
-
const header = `You are executing a sub-workflow "${flowDispatch.child_flow_name}" as part of a larger workflow.`;
|
|
256
|
-
return `${header}\n\n${buildStepPrompt(flowDispatch.child_step, context)}`;
|
|
178
|
+
return sections.join('\n\n');
|
|
257
179
|
}
|
|
258
180
|
|
|
259
181
|
/**
|