@smartmemory/compose 0.2.58-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/dist/assets/{App-s4CulNkO.js → App-DJ5xk_Wx.js} +218 -218
- package/dist/assets/{abnfDiagram-VRR7QNED-CDwzQ5wr.js → abnfDiagram-VRR7QNED-BPWGdCFx.js} +1 -1
- package/dist/assets/{arc-kIU8DDth.js → arc-DX5jqmcO.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-CleorO_0.js → architectureDiagram-ZJ3FMSHR-B6jmNVw7.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-NvEFfoMk.js → blockDiagram-677ZJIJ3-DAx2i-sB.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-BcDQdTVB.js → c4Diagram-LMCZKHZV-zmsZCplj.js} +1 -1
- package/dist/assets/channel-vsDnTvkh.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BseRkpm4.js → chunk-2Q5K7J3B-3QSh6sI7.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-DCHhvgFV.js → chunk-32BRIVSS-BiH5Di_y.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-DcTpMdJ4.js → chunk-5VM5RSS4-C0EfjFLd.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-BdN8fvWs.js → chunk-EX3LRPZG-BGyfmVm-.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DmZJP-8E.js → chunk-JWPE2WC7-Ba0mrNlg.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-V74uYM17.js → chunk-MOJQB5TN-D3QO9EzH.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-CoXUXbn_.js → chunk-RYQCIY6F-D1uvNA6d.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DUscIydR.js → chunk-V7JOEXUC-fihlnodT.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C4UDHq_a.js → chunk-VR4S4FIN-nmRUEo1H.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DIH9i8mZ.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-Dn6Gy6l1.js → cose-bilkent-JH36ORCC-CkacPn8W.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-C2LW5fzB.js → cynefin-VYW2F7L2-BiHaIptG.js} +1 -1
- package/dist/assets/{cynefinDiagram-TSTJHNR4-CGoAq7C9.js → cynefinDiagram-TSTJHNR4-CdKxMmiy.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-BaOKtuBE.js → dagre-VKFMJZFB-BQLY5y_W.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-BdQwqt4x.js → diagram-FQU43EPY-D7uMBHvq.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-Dkcf0o5z.js → diagram-G47NLZAW-B3Z1cuH7.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-C_fDi59r.js → diagram-NH7WQ7WH-BZyRD45e.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-jaTQcOxt.js → diagram-OA4YK3LP-DcThOTt7.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-dEtqVj3s.js → diagram-WEI45ONY-BcgRAkqY.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-s8ImqsSs.js → ebnfDiagram-CCIWWBDH-CGwfO_xH.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-5Zy29-SA.js → erDiagram-Q63AITRT-pV58-Ncc.js} +1 -1
- package/dist/assets/{flowDiagram-23GEKE2U-DV74V9he.js → flowDiagram-23GEKE2U-DJ_SqE8h.js} +1 -1
- package/dist/assets/{ganttDiagram-NO4QXBWP-BND-CiLO.js → ganttDiagram-NO4QXBWP-Dgy0Iyss.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-B_-Mf9Jo.js → gitGraphDiagram-IHSO6WYX-CbiZw9fb.js} +1 -1
- package/dist/assets/{index-CBWbmG7W.js → index-DZTJEk-y.js} +2 -2
- package/dist/assets/{infoDiagram-FWYZ7A6U-Duyz8rZM.js → infoDiagram-FWYZ7A6U-CtsyyEc-.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-D2Qft0e5.js → ishikawaDiagram-FXEZZL3T-BJilNFkK.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-q6rG4Rig.js → journeyDiagram-5HDEW3XC-C2UCMP4t.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-CsPX7gBL.js → kanban-definition-HUTT4EX6-DmLDJBRy.js} +1 -1
- package/dist/assets/{linear-CiccpAAd.js → linear-C1paCqE7.js} +1 -1
- package/dist/assets/{mindmap-definition-LN4V7U3C-DRW2njpA.js → mindmap-definition-LN4V7U3C-CX-RxKVn.js} +1 -1
- package/dist/assets/{pegDiagram-2B236MQR-DoDgm2S_.js → pegDiagram-2B236MQR-CTU32H2W.js} +1 -1
- package/dist/assets/{pieDiagram-ENE6RG2P-CNSYLVAF.js → pieDiagram-ENE6RG2P-D8L1aYoo.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-8-Nd6ony.js → quadrantDiagram-ABIIQ3AL-hQ3bXosy.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-bfdg6Bo4.js → railroadDiagram-RFXS5EU6--_vuYcda.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-slDkxCQy.js → requirementDiagram-TGXJPOKE-C6h_m3Az.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-CR3CboiK.js → sankeyDiagram-HTMAVEWB-Dpc7CfIQ.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BIXw1JS1.js → sequenceDiagram-DBY2YBRQ-CK5ZzbvJ.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-BdKI7JyF.js → sizeCapture-X5ZJPWSS-BAxVvM9G.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-ktVgpBZS.js → stateDiagram-2N3HPSRC-7j33_2PY.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-GviN0FpJ.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-CaXpC3FM.js → swimlanes-5IMT3BWC-CBG2yOob.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-D0wMoxl1.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-CKWSlRUl.js → timeline-definition-FHXFAJF6-BOnZkQvc.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-CwOhxDyv.js → vennDiagram-L72KCM5P-D52NhIfj.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-DGavMpEB.js → wardleyDiagram-EHGQE667-YoPY2ZU-.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CWmnmLZz.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 +1762 -2614
- package/lib/consumer-fanout.js +1317 -0
- package/lib/feature-validator.js +6 -4
- 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/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/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-DiJkE9og.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-DS76VBEL.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-DS76VBEL.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CzPQnlan.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-rIDv6EuQ.js +0 -8
- package/lib/connector-factory-shim.js +0 -167
- package/server/agent-mcp.js +0 -10
|
@@ -1,23 +1,135 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* stratum-mcp-client.js — MCP protocol client for
|
|
2
|
+
* stratum-mcp-client.js — MCP protocol client for the Stratum TS engine.
|
|
3
3
|
*
|
|
4
|
-
* Spawns
|
|
5
|
-
*
|
|
4
|
+
* Spawns the configured TS MCP entrypoint and communicates via the MCP SDK
|
|
5
|
+
* over stdio. This is for the build runner's plan/step_done
|
|
6
6
|
* loop — distinct from server/stratum-client.js which uses CLI subcommands.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
9
|
* const client = new StratumMcpClient();
|
|
10
10
|
* await client.connect();
|
|
11
11
|
* const dispatch = await client.plan(specPath, 'build', { featureCode: 'FEAT-1' });
|
|
12
|
-
* const
|
|
12
|
+
* const ready = dispatch.ready[0];
|
|
13
|
+
* const next = await client.stepDone(dispatch.runId, ready.id, { output: result }, ready.dispatchToken);
|
|
13
14
|
* await client.close();
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
import { execFileSync } from 'node:child_process';
|
|
17
17
|
import { randomUUID } from 'node:crypto';
|
|
18
18
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
19
19
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
20
|
+
import YAML from 'yaml';
|
|
20
21
|
import { validateBuildStreamEvent } from './build-stream-schema.js';
|
|
22
|
+
import { resolveStratumMcpConnection } from './stratum-engine.js';
|
|
23
|
+
|
|
24
|
+
const RUNTIME_INPUT_REF = /^\$\.input\.([A-Za-z_][A-Za-z0-9_]*)$/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve producer-owned authoring values that TS v1 deliberately requires to
|
|
28
|
+
* be literals. Compose knows these values at plan time, so mutate a cloned spec
|
|
29
|
+
* object before it crosses the MCP boundary instead of templating raw YAML.
|
|
30
|
+
*
|
|
31
|
+
* Only schema fields with literal-only v1 types are resolved here. Prompt/data
|
|
32
|
+
* expressions remain engine-owned and are transmitted unchanged.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} spec parsed Stratum specification
|
|
35
|
+
* @param {object} inputs plan input envelope
|
|
36
|
+
* @returns {object} a resolved clone; `spec` is never mutated
|
|
37
|
+
*/
|
|
38
|
+
export function resolvePlanSpecValues(spec, inputs = {}, runtimeProfiles = null) {
|
|
39
|
+
const resolved = structuredClone(spec);
|
|
40
|
+
const literalAgent = (value) => {
|
|
41
|
+
if (typeof value !== 'string') return null;
|
|
42
|
+
const provider = value.split(':', 1)[0];
|
|
43
|
+
return ['claude', 'codex'].includes(provider) ? provider : null;
|
|
44
|
+
};
|
|
45
|
+
const inputValue = (value) => {
|
|
46
|
+
if (typeof value !== 'string') return { matched: false, value };
|
|
47
|
+
const match = RUNTIME_INPUT_REF.exec(value);
|
|
48
|
+
return match ? { matched: true, value: inputs[match[1]] } : { matched: false, value };
|
|
49
|
+
};
|
|
50
|
+
// V4: the engine accepts only the bare provider literal, so a runtime agent
|
|
51
|
+
// that carried a tier/template (e.g. --implementer=claude::critical) loses it
|
|
52
|
+
// at resolution. Record the FULL stripped string, keyed by the enclosing step
|
|
53
|
+
// id, so the invocation can recover the tier/capability profile compose-side.
|
|
54
|
+
const recordProfile = (stepId, fullValue, literal) => {
|
|
55
|
+
if (runtimeProfiles && typeof fullValue === 'string' && fullValue !== literal) {
|
|
56
|
+
runtimeProfiles[stepId] = fullValue;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const [flowName, flow] of Object.entries(resolved?.flows ?? {})) {
|
|
61
|
+
if (flowName === 'entry' || !flow || typeof flow !== 'object') continue;
|
|
62
|
+
for (const step of flow.steps ?? []) {
|
|
63
|
+
const agent = inputValue(step.agent);
|
|
64
|
+
if (agent.matched) {
|
|
65
|
+
const literal = literalAgent(agent.value);
|
|
66
|
+
if (!literal) {
|
|
67
|
+
throw new TypeError(`runtime agent for step ${step.id} must resolve to claude or codex`);
|
|
68
|
+
}
|
|
69
|
+
recordProfile(step.id, agent.value, literal);
|
|
70
|
+
step.agent = literal;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const fanout = step.fanout;
|
|
74
|
+
if (!fanout || typeof fanout !== 'object') continue;
|
|
75
|
+
const preMerge = inputValue(fanout.pre_merge);
|
|
76
|
+
if (preMerge.matched) {
|
|
77
|
+
if (preMerge.value === undefined) delete fanout.pre_merge;
|
|
78
|
+
else if (!Array.isArray(preMerge.value) || preMerge.value.some((item) => typeof item !== 'string')) {
|
|
79
|
+
throw new TypeError(`runtime pre_merge for fanout ${step.id} must resolve to a string array`);
|
|
80
|
+
} else fanout.pre_merge = [...preMerge.value];
|
|
81
|
+
}
|
|
82
|
+
for (const stage of fanout.steps ?? []) {
|
|
83
|
+
const stageAgent = inputValue(stage.agent);
|
|
84
|
+
if (!stageAgent.matched) continue;
|
|
85
|
+
const literal = literalAgent(stageAgent.value);
|
|
86
|
+
if (!literal) {
|
|
87
|
+
throw new TypeError(`runtime agent for fanout ${step.id} must resolve to claude or codex`);
|
|
88
|
+
}
|
|
89
|
+
// Keyed by the FANOUT step id — the consumer invocation looks up by
|
|
90
|
+
// descriptor.step, not the inner stage.
|
|
91
|
+
recordProfile(step.id, stageAgent.value, literal);
|
|
92
|
+
stage.agent = literal;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return resolved;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* V4: resolve a step's agent profile from the merged runtime + static maps,
|
|
101
|
+
* tolerating SCOPED ready ids. Ordinary subflow steps arrive with a scoped id
|
|
102
|
+
* (e.g. `coverage_check/run_tests`) while the sidecar keys are the bare step id
|
|
103
|
+
* (`run_tests`), so fall back to the last path segment. Returns undefined when
|
|
104
|
+
* no profile applies (bare provider literal).
|
|
105
|
+
*/
|
|
106
|
+
export function resolveStepProfile(profiles, stepId) {
|
|
107
|
+
if (!profiles || typeof profiles !== 'object' || !stepId) return undefined;
|
|
108
|
+
if (profiles[stepId]) return profiles[stepId];
|
|
109
|
+
const bare = String(stepId).split('/').pop();
|
|
110
|
+
if (bare && profiles[bare]) return profiles[bare];
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build the TS `stratum_agent_run` request from an agent string + compose-side
|
|
116
|
+
* options. The engine surface (contracts/mcp-surface.json) accepts only
|
|
117
|
+
* {agent, prompt, cwd, model?, sandboxMode?, background?} and rejects any other
|
|
118
|
+
* key, so the python-era knobs (allowed_tools/thinking/effort/correlation_id)
|
|
119
|
+
* are NOT sent — they are compose-side concerns applied at the invocation
|
|
120
|
+
* (resolveAgentConfig). `agent` is normalized to the bare provider literal the
|
|
121
|
+
* engine requires; `cwd` is required and defaults to the process cwd.
|
|
122
|
+
*/
|
|
123
|
+
export function buildAgentRunRequest(agentType, prompt, opts = {}) {
|
|
124
|
+
const provider = String(agentType ?? 'claude').split(':', 1)[0] || 'claude';
|
|
125
|
+
return {
|
|
126
|
+
agent: provider,
|
|
127
|
+
prompt,
|
|
128
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
129
|
+
...(opts.modelID ? { model: opts.modelID } : {}),
|
|
130
|
+
...(opts.sandboxMode ? { sandboxMode: opts.sandboxMode } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
21
133
|
|
|
22
134
|
export class StratumError extends Error {
|
|
23
135
|
constructor(code, message, detail) {
|
|
@@ -39,10 +151,8 @@ export class StratumMcpClient {
|
|
|
39
151
|
* Subscribe to BuildStreamEvent push notifications scoped to a (flowId, stepId).
|
|
40
152
|
* Handler receives a parsed BuildStreamEvent envelope. Returns an unsubscribe fn.
|
|
41
153
|
*
|
|
42
|
-
* Events arrive only while
|
|
43
|
-
*
|
|
44
|
-
* which are tied to an active request. Subscribe BEFORE the poll loop, unsubscribe
|
|
45
|
-
* after `outcome` is observed.
|
|
154
|
+
* Events arrive only while an agent tool call for that scope is in flight —
|
|
155
|
+
* the underlying transport is MCP progress tied to an active request.
|
|
46
156
|
*
|
|
47
157
|
* @param {string} flowId
|
|
48
158
|
* @param {string} stepId
|
|
@@ -88,7 +198,7 @@ export class StratumMcpClient {
|
|
|
88
198
|
* BuildStreamEvent (schema_version + kind). Other progress payloads are
|
|
89
199
|
* ignored.
|
|
90
200
|
*/
|
|
91
|
-
#makeProgressHandler() {
|
|
201
|
+
#makeProgressHandler(callCorrelationId) {
|
|
92
202
|
return (progress) => {
|
|
93
203
|
const msg = progress?.message;
|
|
94
204
|
if (typeof msg !== 'string' || msg.length === 0) return;
|
|
@@ -97,6 +207,25 @@ export class StratumMcpClient {
|
|
|
97
207
|
// Discriminator: BuildStreamEvent has schema_version + kind + flow_id + step_id
|
|
98
208
|
if (!parsed || typeof parsed !== 'object') return;
|
|
99
209
|
if (typeof parsed.kind !== 'string') return;
|
|
210
|
+
// The onprogress handler is bound to ONE tool call, so an envelope belongs
|
|
211
|
+
// to this call iff its flow_id is either (a) ABSENT (undefined) — the TS
|
|
212
|
+
// agent_run wire shape carries no correlation_id (see buildAgentRunRequest),
|
|
213
|
+
// so stamp this call's id — or (b) already EQUAL to this call's correlation
|
|
214
|
+
// id (python echo parity). V6/F7: any OTHER producer-set flow_id is a
|
|
215
|
+
// misroute (one call's stream must never leak into another's subscriber) —
|
|
216
|
+
// drop it. A present-but-falsy flow_id ('' / null / 0) is malformed, NOT
|
|
217
|
+
// absent: it is dropped+warned, never laundered into a stamped valid event.
|
|
218
|
+
if (callCorrelationId) {
|
|
219
|
+
if (parsed.flow_id === undefined) {
|
|
220
|
+
parsed.flow_id = callCorrelationId;
|
|
221
|
+
} else if (parsed.flow_id !== callCorrelationId) {
|
|
222
|
+
console.warn(
|
|
223
|
+
`[stratum-mcp-client] dropping misrouted BuildStreamEvent:` +
|
|
224
|
+
` flow_id=${parsed.flow_id} does not match call ${callCorrelationId}`,
|
|
225
|
+
);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
100
229
|
// STRAT-PAR-STREAM-CONSUMER-VALIDATE: validate envelope before forwarding.
|
|
101
230
|
// On failure: warn and drop — never throw. Consumer must remain robust to
|
|
102
231
|
// producer drift. Non-BuildStreamEvent payloads are silently ignored above.
|
|
@@ -114,7 +243,7 @@ export class StratumMcpClient {
|
|
|
114
243
|
}
|
|
115
244
|
|
|
116
245
|
/**
|
|
117
|
-
* Spawn
|
|
246
|
+
* Spawn the TS Stratum MCP server and establish a connection.
|
|
118
247
|
* @param {object} [opts]
|
|
119
248
|
* @param {string} [opts.command] - Override binary (for testing)
|
|
120
249
|
* @param {string[]} [opts.args] - Override args
|
|
@@ -123,26 +252,16 @@ export class StratumMcpClient {
|
|
|
123
252
|
async connect(opts = {}) {
|
|
124
253
|
if (this.#connected) return;
|
|
125
254
|
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
// Pre-flight: verify binary exists on $PATH (skip for test overrides)
|
|
130
|
-
if (command === 'stratum-mcp') {
|
|
131
|
-
try {
|
|
132
|
-
execFileSync('which', [command], { stdio: 'pipe', timeout: 3000 });
|
|
133
|
-
} catch {
|
|
134
|
-
throw new Error(
|
|
135
|
-
'stratum-mcp not found on $PATH. Install with: pip install stratum-mcp'
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
255
|
+
const defaults = resolveStratumMcpConnection(opts.cwd);
|
|
256
|
+
const command = opts.command ?? defaults.command;
|
|
257
|
+
const args = opts.args ?? defaults.args;
|
|
139
258
|
|
|
140
259
|
const transportOpts = { command, args, stderr: 'pipe' };
|
|
141
|
-
if (opts.cwd) transportOpts.cwd = opts.cwd;
|
|
260
|
+
if (opts.cwd ?? defaults.cwd) transportOpts.cwd = opts.cwd ?? defaults.cwd;
|
|
142
261
|
// Inherit the parent environment. Given no `env`, StdioClientTransport
|
|
143
262
|
// supplies only the MCP SDK's 6-var default allowlist
|
|
144
263
|
// (HOME/LOGNAME/PATH/SHELL/TERM/USER), which strips the auth context the
|
|
145
|
-
// spawned
|
|
264
|
+
// spawned Stratum TS MCP → claude agent needs. Once credentials moved
|
|
146
265
|
// to the macOS keychain, that stripped env stopped authenticating and every
|
|
147
266
|
// agent step failed with `403 forbidden / "Request not allowed"`. Pass the
|
|
148
267
|
// full env through; the stratum connector scrubs SENSITIVE_ENV_VARS
|
|
@@ -193,7 +312,7 @@ export class StratumMcpClient {
|
|
|
193
312
|
if (opts.subscribeProgress) {
|
|
194
313
|
// Long-running tool calls may stream events for many minutes.
|
|
195
314
|
// Use generous timeouts and reset on each progress notification.
|
|
196
|
-
requestOpts.onprogress = this.#makeProgressHandler();
|
|
315
|
+
requestOpts.onprogress = this.#makeProgressHandler(opts.correlationId);
|
|
197
316
|
requestOpts.resetTimeoutOnProgress = true;
|
|
198
317
|
requestOpts.timeout = 600_000; // 10 min per heartbeat
|
|
199
318
|
requestOpts.maxTotalTimeout = 24 * 60 * 60 * 1000; // 24h hard cap
|
|
@@ -201,31 +320,40 @@ export class StratumMcpClient {
|
|
|
201
320
|
|
|
202
321
|
const result = await (client ?? this.#client).callTool(callArgs, undefined, requestOpts);
|
|
203
322
|
|
|
204
|
-
//
|
|
323
|
+
// TS stratum returns the payload as native MCP structured content and also
|
|
324
|
+
// mirrors it as JSON text. Prefer the native object, retaining text parsing
|
|
325
|
+
// for servers that only provide the baseline MCP content array.
|
|
326
|
+
const structuredContent = result.structuredContent;
|
|
327
|
+
const hasStructuredContent = structuredContent
|
|
328
|
+
&& typeof structuredContent === 'object'
|
|
329
|
+
&& !Array.isArray(structuredContent);
|
|
205
330
|
const textContent = result.content?.find(c => c.type === 'text');
|
|
206
|
-
if (!textContent) {
|
|
331
|
+
if (!hasStructuredContent && !textContent) {
|
|
207
332
|
throw new StratumError('EMPTY_RESPONSE', `Tool ${toolName} returned no text content`, '');
|
|
208
333
|
}
|
|
209
334
|
|
|
210
335
|
// MCP isError flag indicates tool-level failure
|
|
211
336
|
if (result.isError) {
|
|
212
|
-
|
|
337
|
+
const message = textContent?.text ?? JSON.stringify(structuredContent);
|
|
338
|
+
throw new StratumError('TOOL_ERROR', message, '');
|
|
213
339
|
}
|
|
214
340
|
|
|
215
|
-
let parsed;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
341
|
+
let parsed = structuredContent;
|
|
342
|
+
if (!hasStructuredContent) {
|
|
343
|
+
try {
|
|
344
|
+
parsed = JSON.parse(textContent.text);
|
|
345
|
+
} catch {
|
|
346
|
+
// Try to extract JSON from text that may have surrounding prose
|
|
347
|
+
const jsonMatch = textContent.text.match(/\{[\s\S]*\}/);
|
|
348
|
+
if (jsonMatch) {
|
|
349
|
+
try {
|
|
350
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
351
|
+
} catch {
|
|
352
|
+
throw new StratumError('PARSE_ERROR', `Tool ${toolName} returned invalid JSON`, textContent.text);
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
225
355
|
throw new StratumError('PARSE_ERROR', `Tool ${toolName} returned invalid JSON`, textContent.text);
|
|
226
356
|
}
|
|
227
|
-
} else {
|
|
228
|
-
throw new StratumError('PARSE_ERROR', `Tool ${toolName} returned invalid JSON`, textContent.text);
|
|
229
357
|
}
|
|
230
358
|
}
|
|
231
359
|
|
|
@@ -244,13 +372,26 @@ export class StratumMcpClient {
|
|
|
244
372
|
|
|
245
373
|
/**
|
|
246
374
|
* Start a flow. Returns the first step dispatch.
|
|
247
|
-
* @param {string} spec -
|
|
375
|
+
* @param {string|object} spec - Parsed spec or inline YAML content
|
|
248
376
|
* @param {string} flow - Flow name within the spec
|
|
249
377
|
* @param {object} inputs - Flow input values
|
|
250
378
|
* @returns {Promise<object>} Step dispatch response
|
|
251
379
|
*/
|
|
252
|
-
async plan(spec, flow, inputs) {
|
|
253
|
-
|
|
380
|
+
async plan(spec, flow, inputs, opts = {}) {
|
|
381
|
+
const parsedSpec = typeof spec === 'string' ? YAML.parse(spec) : spec;
|
|
382
|
+
const resolvedSpec = resolvePlanSpecValues(parsedSpec, inputs);
|
|
383
|
+
// D4: the engine jails file predicates (file_exists / file_contains in
|
|
384
|
+
// ensures) to a workspace root and fails closed ("file function requires a
|
|
385
|
+
// workspace root") without one. Pass the target repo so deterministic
|
|
386
|
+
// artifact ensures on ordinary steps evaluate. Persisted on the run, so
|
|
387
|
+
// resume needs no re-send.
|
|
388
|
+
return this.#callTool('stratum_plan', {
|
|
389
|
+
spec: resolvedSpec,
|
|
390
|
+
input: inputs,
|
|
391
|
+
...(typeof opts.workspaceRoot === 'string' && opts.workspaceRoot.length > 0
|
|
392
|
+
? { workspaceRoot: opts.workspaceRoot }
|
|
393
|
+
: {}),
|
|
394
|
+
});
|
|
254
395
|
}
|
|
255
396
|
|
|
256
397
|
/**
|
|
@@ -259,7 +400,7 @@ export class StratumMcpClient {
|
|
|
259
400
|
* @returns {Promise<object>} Step dispatch response (same format as plan/stepDone)
|
|
260
401
|
*/
|
|
261
402
|
async resume(flowId) {
|
|
262
|
-
return this.#callTool('stratum_resume', {
|
|
403
|
+
return this.#callTool('stratum_resume', { runId: flowId });
|
|
263
404
|
}
|
|
264
405
|
|
|
265
406
|
/**
|
|
@@ -267,13 +408,17 @@ export class StratumMcpClient {
|
|
|
267
408
|
* @param {string} flowId
|
|
268
409
|
* @param {string} stepId
|
|
269
410
|
* @param {object} result - Step result (must match output_contract)
|
|
411
|
+
* @param {string} [dispatchToken] - Engine-issued ready-entry issuance token to echo
|
|
270
412
|
* @returns {Promise<object>}
|
|
271
413
|
*/
|
|
272
|
-
async stepDone(flowId, stepId, result) {
|
|
414
|
+
async stepDone(flowId, stepId, result, dispatchToken) {
|
|
273
415
|
return this.#callTool('stratum_step_done', {
|
|
274
|
-
|
|
275
|
-
|
|
416
|
+
runId: flowId,
|
|
417
|
+
stepId,
|
|
276
418
|
result,
|
|
419
|
+
// Flag-day universal fencing: tokens are opaque and only non-empty strings
|
|
420
|
+
// are transmitted. The strict TS schema rejects the retired epoch field.
|
|
421
|
+
...(typeof dispatchToken === 'string' && dispatchToken.length > 0 ? { dispatchToken } : {}),
|
|
277
422
|
});
|
|
278
423
|
}
|
|
279
424
|
|
|
@@ -284,30 +429,15 @@ export class StratumMcpClient {
|
|
|
284
429
|
* @param {'approve'|'revise'|'kill'} outcome
|
|
285
430
|
* @param {string} rationale
|
|
286
431
|
* @param {'human'|'agent'|'system'} resolvedBy
|
|
432
|
+
* @param {string} [gateToken] - Audit-discovered waiting-gate issuance token to echo
|
|
287
433
|
* @returns {Promise<object>}
|
|
288
434
|
*/
|
|
289
|
-
async gateResolve(flowId, stepId, outcome, rationale, resolvedBy = 'human') {
|
|
435
|
+
async gateResolve(flowId, stepId, outcome, rationale, resolvedBy = 'human', gateToken) {
|
|
290
436
|
return this.#callTool('stratum_gate_resolve', {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
outcome,
|
|
294
|
-
|
|
295
|
-
resolved_by: resolvedBy,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Skip the current step with a recorded reason.
|
|
301
|
-
* @param {string} flowId
|
|
302
|
-
* @param {string} stepId
|
|
303
|
-
* @param {string} reason
|
|
304
|
-
* @returns {Promise<object>}
|
|
305
|
-
*/
|
|
306
|
-
async skipStep(flowId, stepId, reason) {
|
|
307
|
-
return this.#callTool('stratum_skip_step', {
|
|
308
|
-
flow_id: flowId,
|
|
309
|
-
step_id: stepId,
|
|
310
|
-
reason,
|
|
437
|
+
runId: flowId,
|
|
438
|
+
stepId,
|
|
439
|
+
decision: outcome,
|
|
440
|
+
...(typeof gateToken === 'string' && gateToken.length > 0 ? { gateToken } : {}),
|
|
311
441
|
});
|
|
312
442
|
}
|
|
313
443
|
|
|
@@ -317,50 +447,7 @@ export class StratumMcpClient {
|
|
|
317
447
|
* @returns {Promise<object>}
|
|
318
448
|
*/
|
|
319
449
|
async audit(flowId) {
|
|
320
|
-
return this.#callTool('stratum_audit', {
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Start a counted iteration loop on a step.
|
|
325
|
-
* @param {string} flowId
|
|
326
|
-
* @param {string} stepId
|
|
327
|
-
* @returns {Promise<object>}
|
|
328
|
-
*/
|
|
329
|
-
async iterationStart(flowId, stepId) {
|
|
330
|
-
return this.#callTool('stratum_iteration_start', {
|
|
331
|
-
flow_id: flowId,
|
|
332
|
-
step_id: stepId,
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Report one iteration result.
|
|
338
|
-
* @param {string} flowId
|
|
339
|
-
* @param {string} stepId
|
|
340
|
-
* @param {object} result
|
|
341
|
-
* @returns {Promise<object>}
|
|
342
|
-
*/
|
|
343
|
-
async iterationReport(flowId, stepId, result) {
|
|
344
|
-
return this.#callTool('stratum_iteration_report', {
|
|
345
|
-
flow_id: flowId,
|
|
346
|
-
step_id: stepId,
|
|
347
|
-
result,
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Abort an iteration loop early.
|
|
353
|
-
* @param {string} flowId
|
|
354
|
-
* @param {string} stepId
|
|
355
|
-
* @param {string} reason
|
|
356
|
-
* @returns {Promise<object>}
|
|
357
|
-
*/
|
|
358
|
-
async iterationAbort(flowId, stepId, reason) {
|
|
359
|
-
return this.#callTool('stratum_iteration_abort', {
|
|
360
|
-
flow_id: flowId,
|
|
361
|
-
step_id: stepId,
|
|
362
|
-
reason,
|
|
363
|
-
});
|
|
450
|
+
return this.#callTool('stratum_audit', { runId: flowId });
|
|
364
451
|
}
|
|
365
452
|
|
|
366
453
|
/**
|
|
@@ -399,77 +486,7 @@ export class StratumMcpClient {
|
|
|
399
486
|
}
|
|
400
487
|
|
|
401
488
|
/**
|
|
402
|
-
*
|
|
403
|
-
* @param {string} flowId
|
|
404
|
-
* @param {string} stepId
|
|
405
|
-
* @param {Array<{task_id: string, status: string, result?: object, error?: string}>} taskResults
|
|
406
|
-
* @param {'clean'|'conflict'|'fallback'|'manual_required'|{status:string,bounced_tasks?:object[]}} mergeStatus
|
|
407
|
-
* COMP-PAR-MERGE-QUEUE-CONSUMER: either a bare status string (back-compat) or a
|
|
408
|
-
* structured object carrying gate/conflict bounce records. Serializes as-is.
|
|
409
|
-
* @returns {Promise<object>} Next dispatch response
|
|
410
|
-
*/
|
|
411
|
-
async parallelDone(flowId, stepId, taskResults, mergeStatus) {
|
|
412
|
-
return this.#callTool('stratum_parallel_done', {
|
|
413
|
-
flow_id: flowId,
|
|
414
|
-
step_id: stepId,
|
|
415
|
-
task_results: taskResults,
|
|
416
|
-
merge_status: mergeStatus,
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Start server-side execution of a parallel_dispatch step (T2-F5-COMPOSE-MIGRATE).
|
|
422
|
-
* Returns {status: 'started', ...} on success or {error, message} on known error.
|
|
423
|
-
* @param {string} flowId
|
|
424
|
-
* @param {string} stepId
|
|
425
|
-
* @returns {Promise<object>}
|
|
426
|
-
*/
|
|
427
|
-
async parallelStart(flowId, stepId) {
|
|
428
|
-
return this.#callTool('stratum_parallel_start', {
|
|
429
|
-
flow_id: flowId,
|
|
430
|
-
step_id: stepId,
|
|
431
|
-
}, { subscribeProgress: true });
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Poll state of a server-dispatched parallel_dispatch step (T2-F5-COMPOSE-MIGRATE).
|
|
436
|
-
* Returns {summary, tasks, require_satisfied, can_advance, outcome}.
|
|
437
|
-
* Break on `outcome != null`, not `can_advance` — see design doc §3.
|
|
438
|
-
* @param {string} flowId
|
|
439
|
-
* @param {string} stepId
|
|
440
|
-
* @returns {Promise<object>}
|
|
441
|
-
*/
|
|
442
|
-
async parallelPoll(flowId, stepId) {
|
|
443
|
-
return this.#callTool('stratum_parallel_poll', {
|
|
444
|
-
flow_id: flowId,
|
|
445
|
-
step_id: stepId,
|
|
446
|
-
}, { subscribeProgress: true });
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Consumer-driven advance for parallel_dispatch steps with defer_advance:true.
|
|
451
|
-
* Call after observing outcome.status === 'awaiting_consumer_advance' from
|
|
452
|
-
* parallelPoll. Feeds merge_status back to Stratum which runs
|
|
453
|
-
* _evaluate_parallel_results + _advance_after_parallel and returns the real
|
|
454
|
-
* advance outcome.
|
|
455
|
-
*
|
|
456
|
-
* @param {string} flowId
|
|
457
|
-
* @param {string} stepId
|
|
458
|
-
* @param {'clean'|'conflict'|{status:'clean'|'conflict',bounced_tasks?:object[]}} mergeStatus
|
|
459
|
-
* COMP-PAR-MERGE-QUEUE: either a bare status string (back-compat) or a
|
|
460
|
-
* structured object carrying merge_conflict bounce records. Serializes as-is.
|
|
461
|
-
* @returns {Promise<object>}
|
|
462
|
-
*/
|
|
463
|
-
async parallelAdvance(flowId, stepId, mergeStatus) {
|
|
464
|
-
return this.#callTool('stratum_parallel_advance', {
|
|
465
|
-
flow_id: flowId,
|
|
466
|
-
step_id: stepId,
|
|
467
|
-
merge_status: mergeStatus,
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Run an agent (claude/codex) via the Python connector tier and stream
|
|
489
|
+
* Run an agent (claude/codex) via the Stratum connector tier and stream
|
|
473
490
|
* BuildStreamEvent envelopes back via MCP progress notifications.
|
|
474
491
|
* Subscribe via `onEvent(correlationId, '_agent_run', handler)` BEFORE calling.
|
|
475
492
|
*
|
|
@@ -483,7 +500,7 @@ export class StratumMcpClient {
|
|
|
483
500
|
* @param {object} [opts.thinking]
|
|
484
501
|
* @param {string} [opts.effort]
|
|
485
502
|
* @param {string} [opts.cwd]
|
|
486
|
-
* @returns {Promise<{text: string
|
|
503
|
+
* @returns {Promise<{text: string}>}
|
|
487
504
|
*
|
|
488
505
|
* NOTE: Schema injection is the caller's responsibility — `runAndNormalize`
|
|
489
506
|
* runs `injectSchema(prompt, schema)` client-side. Forwarding `schema` to
|
|
@@ -492,21 +509,15 @@ export class StratumMcpClient {
|
|
|
492
509
|
*/
|
|
493
510
|
async agentRun(agentType, prompt, opts = {}) {
|
|
494
511
|
const correlationId = opts.correlationId ?? randomUUID();
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
correlation_id: correlationId,
|
|
505
|
-
}, { subscribeProgress: true });
|
|
506
|
-
if (result && typeof result === 'object' && !result.correlation_id) {
|
|
507
|
-
result.correlation_id = correlationId;
|
|
508
|
-
}
|
|
509
|
-
return result;
|
|
512
|
+
// TS surface (contracts/mcp-surface.json → stratum_agent_run): the request
|
|
513
|
+
// is {agent, prompt, cwd, model?, sandboxMode?, background?} and rejects any
|
|
514
|
+
// undeclared key. The python-era tier accepted {type, allowed_tools, ...};
|
|
515
|
+
// those knobs are resolved compose-side (resolveAgentConfig) and applied at
|
|
516
|
+
// this seam — the engine's connector honors model + sandboxMode. `cwd` is
|
|
517
|
+
// required, so default to the process cwd when the caller omits it.
|
|
518
|
+
return this.#callTool('stratum_agent_run',
|
|
519
|
+
buildAgentRunRequest(agentType, prompt, opts),
|
|
520
|
+
{ subscribeProgress: true, correlationId });
|
|
510
521
|
}
|
|
511
522
|
|
|
512
523
|
/**
|
|
@@ -520,24 +531,24 @@ export class StratumMcpClient {
|
|
|
520
531
|
* @returns {Promise<string>}
|
|
521
532
|
*/
|
|
522
533
|
async runAgentText(agentType, prompt, opts = {}) {
|
|
523
|
-
const result = await this.#callTool('stratum_agent_run',
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
cwd: opts.cwd ?? undefined,
|
|
527
|
-
}, { subscribeProgress: false });
|
|
534
|
+
const result = await this.#callTool('stratum_agent_run',
|
|
535
|
+
buildAgentRunRequest(agentType, prompt, opts),
|
|
536
|
+
{ subscribeProgress: false });
|
|
528
537
|
return result?.text ?? '';
|
|
529
538
|
}
|
|
530
539
|
|
|
531
540
|
/**
|
|
532
|
-
* Cancel an in-flight
|
|
533
|
-
*
|
|
541
|
+
* Cancel an in-flight agent run. V2: the TS surface
|
|
542
|
+
* (contracts/mcp-surface.json → stratum_cancel_agent_run) takes {runId}, not
|
|
543
|
+
* the python-era {correlation_id} the engine now rejects. Only BACKGROUND runs
|
|
544
|
+
* carry a runId to cancel; a synchronous agent_run returns no handle, so this
|
|
545
|
+
* is a no-op (not_found) there — synchronous controlled executions are
|
|
546
|
+
* interrupted via the local connector's AbortController instead.
|
|
534
547
|
*
|
|
535
|
-
* @param {string}
|
|
548
|
+
* @param {string} runId
|
|
536
549
|
* @returns {Promise<object>}
|
|
537
550
|
*/
|
|
538
|
-
async cancelAgentRun(
|
|
539
|
-
return this.#callTool('stratum_cancel_agent_run', {
|
|
540
|
-
correlation_id: correlationId,
|
|
541
|
-
});
|
|
551
|
+
async cancelAgentRun(runId) {
|
|
552
|
+
return this.#callTool('stratum_cancel_agent_run', { runId });
|
|
542
553
|
}
|
|
543
554
|
}
|