@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.
Files changed (99) hide show
  1. package/README.md +2 -2
  2. package/bin/compose.js +55 -54
  3. package/dist/assets/{App-s4CulNkO.js → App-DJ5xk_Wx.js} +218 -218
  4. package/dist/assets/{abnfDiagram-VRR7QNED-CDwzQ5wr.js → abnfDiagram-VRR7QNED-BPWGdCFx.js} +1 -1
  5. package/dist/assets/{arc-kIU8DDth.js → arc-DX5jqmcO.js} +1 -1
  6. package/dist/assets/{architectureDiagram-ZJ3FMSHR-CleorO_0.js → architectureDiagram-ZJ3FMSHR-B6jmNVw7.js} +1 -1
  7. package/dist/assets/{blockDiagram-677ZJIJ3-NvEFfoMk.js → blockDiagram-677ZJIJ3-DAx2i-sB.js} +1 -1
  8. package/dist/assets/{c4Diagram-LMCZKHZV-BcDQdTVB.js → c4Diagram-LMCZKHZV-zmsZCplj.js} +1 -1
  9. package/dist/assets/channel-vsDnTvkh.js +1 -0
  10. package/dist/assets/{chunk-2Q5K7J3B-BseRkpm4.js → chunk-2Q5K7J3B-3QSh6sI7.js} +1 -1
  11. package/dist/assets/{chunk-32BRIVSS-DCHhvgFV.js → chunk-32BRIVSS-BiH5Di_y.js} +1 -1
  12. package/dist/assets/{chunk-5VM5RSS4-DcTpMdJ4.js → chunk-5VM5RSS4-C0EfjFLd.js} +1 -1
  13. package/dist/assets/{chunk-EX3LRPZG-BdN8fvWs.js → chunk-EX3LRPZG-BGyfmVm-.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-DmZJP-8E.js → chunk-JWPE2WC7-Ba0mrNlg.js} +1 -1
  15. package/dist/assets/{chunk-MOJQB5TN-V74uYM17.js → chunk-MOJQB5TN-D3QO9EzH.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-CoXUXbn_.js → chunk-RYQCIY6F-D1uvNA6d.js} +1 -1
  17. package/dist/assets/{chunk-V7JOEXUC-DUscIydR.js → chunk-V7JOEXUC-fihlnodT.js} +1 -1
  18. package/dist/assets/{chunk-VR4S4FIN-C4UDHq_a.js → chunk-VR4S4FIN-nmRUEo1H.js} +1 -1
  19. package/dist/assets/{chunk-XXDRQBXY-DIH9i8mZ.js → chunk-XXDRQBXY-CM693yEg.js} +1 -1
  20. package/dist/assets/classDiagram-OUVF2IWQ-DO-wdSFZ.js +1 -0
  21. package/dist/assets/classDiagram-v2-EOCWNBFH-DO-wdSFZ.js +1 -0
  22. package/dist/assets/{cose-bilkent-JH36ORCC-Dn6Gy6l1.js → cose-bilkent-JH36ORCC-CkacPn8W.js} +1 -1
  23. package/dist/assets/{cynefin-VYW2F7L2-C2LW5fzB.js → cynefin-VYW2F7L2-BiHaIptG.js} +1 -1
  24. package/dist/assets/{cynefinDiagram-TSTJHNR4-CGoAq7C9.js → cynefinDiagram-TSTJHNR4-CdKxMmiy.js} +1 -1
  25. package/dist/assets/{dagre-VKFMJZFB-BaOKtuBE.js → dagre-VKFMJZFB-BQLY5y_W.js} +1 -1
  26. package/dist/assets/{diagram-FQU43EPY-BdQwqt4x.js → diagram-FQU43EPY-D7uMBHvq.js} +1 -1
  27. package/dist/assets/{diagram-G47NLZAW-Dkcf0o5z.js → diagram-G47NLZAW-B3Z1cuH7.js} +1 -1
  28. package/dist/assets/{diagram-NH7WQ7WH-C_fDi59r.js → diagram-NH7WQ7WH-BZyRD45e.js} +1 -1
  29. package/dist/assets/{diagram-OA4YK3LP-jaTQcOxt.js → diagram-OA4YK3LP-DcThOTt7.js} +1 -1
  30. package/dist/assets/{diagram-WEI45ONY-dEtqVj3s.js → diagram-WEI45ONY-BcgRAkqY.js} +1 -1
  31. package/dist/assets/{ebnfDiagram-CCIWWBDH-s8ImqsSs.js → ebnfDiagram-CCIWWBDH-CGwfO_xH.js} +1 -1
  32. package/dist/assets/{erDiagram-Q63AITRT-5Zy29-SA.js → erDiagram-Q63AITRT-pV58-Ncc.js} +1 -1
  33. package/dist/assets/{flowDiagram-23GEKE2U-DV74V9he.js → flowDiagram-23GEKE2U-DJ_SqE8h.js} +1 -1
  34. package/dist/assets/{ganttDiagram-NO4QXBWP-BND-CiLO.js → ganttDiagram-NO4QXBWP-Dgy0Iyss.js} +1 -1
  35. package/dist/assets/{gitGraphDiagram-IHSO6WYX-B_-Mf9Jo.js → gitGraphDiagram-IHSO6WYX-CbiZw9fb.js} +1 -1
  36. package/dist/assets/{index-CBWbmG7W.js → index-DZTJEk-y.js} +2 -2
  37. package/dist/assets/{infoDiagram-FWYZ7A6U-Duyz8rZM.js → infoDiagram-FWYZ7A6U-CtsyyEc-.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-FXEZZL3T-D2Qft0e5.js → ishikawaDiagram-FXEZZL3T-BJilNFkK.js} +1 -1
  39. package/dist/assets/{journeyDiagram-5HDEW3XC-q6rG4Rig.js → journeyDiagram-5HDEW3XC-C2UCMP4t.js} +1 -1
  40. package/dist/assets/{kanban-definition-HUTT4EX6-CsPX7gBL.js → kanban-definition-HUTT4EX6-DmLDJBRy.js} +1 -1
  41. package/dist/assets/{linear-CiccpAAd.js → linear-C1paCqE7.js} +1 -1
  42. package/dist/assets/{mindmap-definition-LN4V7U3C-DRW2njpA.js → mindmap-definition-LN4V7U3C-CX-RxKVn.js} +1 -1
  43. package/dist/assets/{pegDiagram-2B236MQR-DoDgm2S_.js → pegDiagram-2B236MQR-CTU32H2W.js} +1 -1
  44. package/dist/assets/{pieDiagram-ENE6RG2P-CNSYLVAF.js → pieDiagram-ENE6RG2P-D8L1aYoo.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-ABIIQ3AL-8-Nd6ony.js → quadrantDiagram-ABIIQ3AL-hQ3bXosy.js} +1 -1
  46. package/dist/assets/{railroadDiagram-RFXS5EU6-bfdg6Bo4.js → railroadDiagram-RFXS5EU6--_vuYcda.js} +1 -1
  47. package/dist/assets/{requirementDiagram-TGXJPOKE-slDkxCQy.js → requirementDiagram-TGXJPOKE-C6h_m3Az.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-HTMAVEWB-CR3CboiK.js → sankeyDiagram-HTMAVEWB-Dpc7CfIQ.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BIXw1JS1.js → sequenceDiagram-DBY2YBRQ-CK5ZzbvJ.js} +1 -1
  50. package/dist/assets/{sizeCapture-X5ZJPWSS-BdKI7JyF.js → sizeCapture-X5ZJPWSS-BAxVvM9G.js} +1 -1
  51. package/dist/assets/{stateDiagram-2N3HPSRC-ktVgpBZS.js → stateDiagram-2N3HPSRC-7j33_2PY.js} +1 -1
  52. package/dist/assets/stateDiagram-v2-6OUMAXLB-GviN0FpJ.js +1 -0
  53. package/dist/assets/{swimlanes-5IMT3BWC-CaXpC3FM.js → swimlanes-5IMT3BWC-CBG2yOob.js} +2 -2
  54. package/dist/assets/swimlanesDiagram-G3AALYLV-D0wMoxl1.js +8 -0
  55. package/dist/assets/{timeline-definition-FHXFAJF6-CKWSlRUl.js → timeline-definition-FHXFAJF6-BOnZkQvc.js} +1 -1
  56. package/dist/assets/{vennDiagram-L72KCM5P-CwOhxDyv.js → vennDiagram-L72KCM5P-D52NhIfj.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-EHGQE667-DGavMpEB.js → wardleyDiagram-EHGQE667-YoPY2ZU-.js} +1 -1
  58. package/dist/assets/{xychartDiagram-FW5EYKEG-CWmnmLZz.js → xychartDiagram-FW5EYKEG-BanNfgtO.js} +1 -1
  59. package/dist/index.html +1 -1
  60. package/lib/build-all.js +0 -5
  61. package/lib/build-stream-schema.js +1 -1
  62. package/lib/build.js +1762 -2614
  63. package/lib/consumer-fanout.js +1317 -0
  64. package/lib/feature-validator.js +6 -4
  65. package/lib/flow-state.js +15 -14
  66. package/lib/gsd-budget.js +48 -10
  67. package/lib/gsd-prompt.js +3 -4
  68. package/lib/gsd-stuck.js +1 -1
  69. package/lib/gsd.js +303 -149
  70. package/lib/local-claude-connector.js +149 -0
  71. package/lib/new.js +162 -307
  72. package/lib/result-normalizer.js +233 -18
  73. package/lib/review-lenses.js +1 -1
  74. package/lib/step-prompt.js +41 -119
  75. package/lib/stratum-engine.js +297 -0
  76. package/lib/stratum-mcp-client.js +224 -213
  77. package/lib/vocabulary-compliance.js +268 -0
  78. package/lib/vocabulary-inject.js +1 -36
  79. package/package.json +2 -1
  80. package/pipelines/build-quick.stratum.yaml +5 -17
  81. package/pipelines/build.profiles.json +11 -0
  82. package/pipelines/build.stratum.yaml +288 -467
  83. package/pipelines/gsd.stratum.yaml +73 -125
  84. package/pipelines/new.stratum.yaml +68 -149
  85. package/server/build-routes.js +4 -1
  86. package/server/design-routes.js +37 -21
  87. package/server/index.js +13 -21
  88. package/server/lifecycle-guard.js +1 -1
  89. package/server/pipeline-routes.js +113 -31
  90. package/server/stratum-client.js +33 -47
  91. package/server/stratum-sync.js +3 -4
  92. package/server/vision-server.js +1 -1
  93. package/dist/assets/channel-DiJkE9og.js +0 -1
  94. package/dist/assets/classDiagram-OUVF2IWQ-DS76VBEL.js +0 -1
  95. package/dist/assets/classDiagram-v2-EOCWNBFH-DS76VBEL.js +0 -1
  96. package/dist/assets/stateDiagram-v2-6OUMAXLB-CzPQnlan.js +0 -1
  97. package/dist/assets/swimlanesDiagram-G3AALYLV-rIDv6EuQ.js +0 -8
  98. package/lib/connector-factory-shim.js +0 -167
  99. package/server/agent-mcp.js +0 -10
@@ -1,23 +1,135 @@
1
1
  /**
2
- * stratum-mcp-client.js — MCP protocol client for stratum-mcp.
2
+ * stratum-mcp-client.js — MCP protocol client for the Stratum TS engine.
3
3
  *
4
- * Spawns `stratum-mcp` (no subcommand) as a child process and communicates
5
- * via the MCP SDK over stdio. This is for the build runner's plan/step_done
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 next = await client.stepDone(dispatch.flow_id, 'step1', { phase: 'design' });
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 a tool call (parallelStart/parallelPoll/...) for that
43
- * scope is in flight — the underlying transport is MCP progress notifications,
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 stratum-mcp and establish MCP connection.
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 command = opts.command ?? 'stratum-mcp';
127
- const args = opts.args ?? [];
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 stratum-mcp → claude agent needs. Once claude credentials moved
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
- // MCP tool results come back as content array; extract text content
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
- throw new StratumError('TOOL_ERROR', textContent.text, '');
337
+ const message = textContent?.text ?? JSON.stringify(structuredContent);
338
+ throw new StratumError('TOOL_ERROR', message, '');
213
339
  }
214
340
 
215
- let parsed;
216
- try {
217
- parsed = JSON.parse(textContent.text);
218
- } catch {
219
- // Try to extract JSON from text that may have surrounding prose
220
- const jsonMatch = textContent.text.match(/\{[\s\S]*\}/);
221
- if (jsonMatch) {
222
- try {
223
- parsed = JSON.parse(jsonMatch[0]);
224
- } catch {
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 - Inline YAML spec content (not a file path)
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
- return this.#callTool('stratum_plan', { spec, flow, inputs });
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', { flow_id: flowId });
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
- flow_id: flowId,
275
- step_id: stepId,
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
- flow_id: flowId,
292
- step_id: stepId,
293
- outcome,
294
- rationale,
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', { flow_id: flowId });
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
- * Report batch task results for a parallel_dispatch step.
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, correlation_id: 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
- const result = await this.#callTool('stratum_agent_run', {
496
- type: agentType,
497
- prompt,
498
- modelID: opts.modelID ?? undefined,
499
- allowed_tools: opts.allowedTools ?? undefined,
500
- disallowed_tools: opts.disallowedTools ?? undefined,
501
- thinking: opts.thinking ?? undefined,
502
- effort: opts.effort ?? undefined,
503
- cwd: opts.cwd ?? undefined,
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
- type: agentType,
525
- prompt,
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 stratum_agent_run by correlation id. Returns
533
- * `{status: 'cancelled' | 'not_found', correlation_id}` from the producer.
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} correlationId
548
+ * @param {string} runId
536
549
  * @returns {Promise<object>}
537
550
  */
538
- async cancelAgentRun(correlationId) {
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
  }