@smartmemory/compose 0.2.58-beta → 0.3.1-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +2 -2
  2. package/bin/compose.js +55 -54
  3. package/dist/assets/{App-s4CulNkO.js → App-DR9pCiNE.js} +290 -290
  4. package/dist/assets/{abnfDiagram-VRR7QNED-CDwzQ5wr.js → abnfDiagram-VRR7QNED-CZ6p2bRe.js} +1 -1
  5. package/dist/assets/{arc-kIU8DDth.js → arc-6j04LBqn.js} +1 -1
  6. package/dist/assets/{architectureDiagram-ZJ3FMSHR-CleorO_0.js → architectureDiagram-ZJ3FMSHR-00QM3pmc.js} +1 -1
  7. package/dist/assets/{blockDiagram-677ZJIJ3-NvEFfoMk.js → blockDiagram-677ZJIJ3-BUb9Kjfd.js} +1 -1
  8. package/dist/assets/{c4Diagram-LMCZKHZV-BcDQdTVB.js → c4Diagram-LMCZKHZV-D1PkhmSm.js} +1 -1
  9. package/dist/assets/channel-BcWZM2KA.js +1 -0
  10. package/dist/assets/{chunk-2Q5K7J3B-BseRkpm4.js → chunk-2Q5K7J3B-CduDdMf4.js} +1 -1
  11. package/dist/assets/{chunk-32BRIVSS-DCHhvgFV.js → chunk-32BRIVSS-CH8Kj5rh.js} +1 -1
  12. package/dist/assets/{chunk-5VM5RSS4-DcTpMdJ4.js → chunk-5VM5RSS4-Cfts6Lix.js} +1 -1
  13. package/dist/assets/{chunk-EX3LRPZG-BdN8fvWs.js → chunk-EX3LRPZG-Dguk4yKZ.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-DmZJP-8E.js → chunk-JWPE2WC7-DbCJp6t9.js} +1 -1
  15. package/dist/assets/{chunk-MOJQB5TN-V74uYM17.js → chunk-MOJQB5TN-CNjM_9_U.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-CoXUXbn_.js → chunk-RYQCIY6F-D4k-BUbh.js} +1 -1
  17. package/dist/assets/{chunk-V7JOEXUC-DUscIydR.js → chunk-V7JOEXUC-O1sFoHf1.js} +1 -1
  18. package/dist/assets/{chunk-VR4S4FIN-C4UDHq_a.js → chunk-VR4S4FIN-CGbSXVPL.js} +1 -1
  19. package/dist/assets/{chunk-XXDRQBXY-DIH9i8mZ.js → chunk-XXDRQBXY-iIelV7SO.js} +1 -1
  20. package/dist/assets/classDiagram-OUVF2IWQ-Ce7YjlIK.js +1 -0
  21. package/dist/assets/classDiagram-v2-EOCWNBFH-Ce7YjlIK.js +1 -0
  22. package/dist/assets/{cose-bilkent-JH36ORCC-Dn6Gy6l1.js → cose-bilkent-JH36ORCC-BKmPLgw2.js} +1 -1
  23. package/dist/assets/{cynefin-VYW2F7L2-C2LW5fzB.js → cynefin-VYW2F7L2-CmT6ieC5.js} +1 -1
  24. package/dist/assets/{cynefinDiagram-TSTJHNR4-CGoAq7C9.js → cynefinDiagram-TSTJHNR4-Hn2vSVOw.js} +1 -1
  25. package/dist/assets/{dagre-VKFMJZFB-BaOKtuBE.js → dagre-VKFMJZFB-B0ifIwlU.js} +1 -1
  26. package/dist/assets/{diagram-FQU43EPY-BdQwqt4x.js → diagram-FQU43EPY-BuCDB-JG.js} +1 -1
  27. package/dist/assets/{diagram-G47NLZAW-Dkcf0o5z.js → diagram-G47NLZAW-D5avTZAQ.js} +1 -1
  28. package/dist/assets/{diagram-NH7WQ7WH-C_fDi59r.js → diagram-NH7WQ7WH-24QCIgpj.js} +1 -1
  29. package/dist/assets/{diagram-OA4YK3LP-jaTQcOxt.js → diagram-OA4YK3LP-A-bbUTK3.js} +1 -1
  30. package/dist/assets/{diagram-WEI45ONY-dEtqVj3s.js → diagram-WEI45ONY-BIMDm-Kj.js} +1 -1
  31. package/dist/assets/{ebnfDiagram-CCIWWBDH-s8ImqsSs.js → ebnfDiagram-CCIWWBDH-DCXMfG-M.js} +1 -1
  32. package/dist/assets/{erDiagram-Q63AITRT-5Zy29-SA.js → erDiagram-Q63AITRT-BlDrG0Ev.js} +1 -1
  33. package/dist/assets/{flowDiagram-23GEKE2U-DV74V9he.js → flowDiagram-23GEKE2U-aTyazY6P.js} +1 -1
  34. package/dist/assets/{ganttDiagram-NO4QXBWP-BND-CiLO.js → ganttDiagram-NO4QXBWP-Bxo-a80f.js} +1 -1
  35. package/dist/assets/{gitGraphDiagram-IHSO6WYX-B_-Mf9Jo.js → gitGraphDiagram-IHSO6WYX-COhKb9wR.js} +1 -1
  36. package/dist/assets/index-BcmsQZX-.js +119 -0
  37. package/dist/assets/{infoDiagram-FWYZ7A6U-Duyz8rZM.js → infoDiagram-FWYZ7A6U-DLCbpinV.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-FXEZZL3T-D2Qft0e5.js → ishikawaDiagram-FXEZZL3T-eyHF7GIj.js} +1 -1
  39. package/dist/assets/{journeyDiagram-5HDEW3XC-q6rG4Rig.js → journeyDiagram-5HDEW3XC-BsVocmlw.js} +1 -1
  40. package/dist/assets/{kanban-definition-HUTT4EX6-CsPX7gBL.js → kanban-definition-HUTT4EX6-CaFPSZxY.js} +1 -1
  41. package/dist/assets/{linear-CiccpAAd.js → linear-cqnI_wgW.js} +1 -1
  42. package/dist/assets/{mindmap-definition-LN4V7U3C-DRW2njpA.js → mindmap-definition-LN4V7U3C-B66BGqKn.js} +1 -1
  43. package/dist/assets/{pegDiagram-2B236MQR-DoDgm2S_.js → pegDiagram-2B236MQR-F6IN0_L5.js} +1 -1
  44. package/dist/assets/{pieDiagram-ENE6RG2P-CNSYLVAF.js → pieDiagram-ENE6RG2P-D5nOQxpo.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-ABIIQ3AL-8-Nd6ony.js → quadrantDiagram-ABIIQ3AL-DwUAYbMO.js} +1 -1
  46. package/dist/assets/{railroadDiagram-RFXS5EU6-bfdg6Bo4.js → railroadDiagram-RFXS5EU6-Cvmlah5P.js} +1 -1
  47. package/dist/assets/{requirementDiagram-TGXJPOKE-slDkxCQy.js → requirementDiagram-TGXJPOKE-DoUtsFnL.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-HTMAVEWB-CR3CboiK.js → sankeyDiagram-HTMAVEWB-D4XTwiKp.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BIXw1JS1.js → sequenceDiagram-DBY2YBRQ-CiG9yRIR.js} +1 -1
  50. package/dist/assets/{sizeCapture-X5ZJPWSS-BdKI7JyF.js → sizeCapture-X5ZJPWSS-zd7ydQ_Y.js} +1 -1
  51. package/dist/assets/{stateDiagram-2N3HPSRC-ktVgpBZS.js → stateDiagram-2N3HPSRC-BZesBHQ4.js} +1 -1
  52. package/dist/assets/stateDiagram-v2-6OUMAXLB-BnlLOJzB.js +1 -0
  53. package/dist/assets/{swimlanes-5IMT3BWC-CaXpC3FM.js → swimlanes-5IMT3BWC-Bg8exSE7.js} +2 -2
  54. package/dist/assets/swimlanesDiagram-G3AALYLV-DZMVxqbs.js +8 -0
  55. package/dist/assets/{timeline-definition-FHXFAJF6-CKWSlRUl.js → timeline-definition-FHXFAJF6-TKj_HY9k.js} +1 -1
  56. package/dist/assets/{vennDiagram-L72KCM5P-CwOhxDyv.js → vennDiagram-L72KCM5P-CJf-bqn0.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-EHGQE667-DGavMpEB.js → wardleyDiagram-EHGQE667-CqizSfCL.js} +1 -1
  58. package/dist/assets/{xychartDiagram-FW5EYKEG-CWmnmLZz.js → xychartDiagram-FW5EYKEG-CK-OErMM.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 +6 -2
  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/index-CBWbmG7W.js +0 -119
  97. package/dist/assets/stateDiagram-v2-6OUMAXLB-CzPQnlan.js +0 -1
  98. package/dist/assets/swimlanesDiagram-G3AALYLV-rIDv6EuQ.js +0 -8
  99. package/lib/connector-factory-shim.js +0 -167
  100. package/server/agent-mcp.js +0 -10
@@ -312,7 +312,7 @@ export async function ensureGuard(featureCode, currentPhase, workspaceRoot, mode
312
312
  workspaceRoot,
313
313
  });
314
314
  } catch (e) {
315
- // A thrown spawn failure (e.g. stratum-mcp not installed) must NOT escape as
315
+ // A thrown Stratum CLI failure must NOT escape as
316
316
  // a generic 500/400 — normalise to a fail-closed error result.
317
317
  return { error: { code: 'GUARD_UNREACHABLE', message: e.message } };
318
318
  }
@@ -184,14 +184,23 @@ function flowNamesOf(parsed) {
184
184
  * src/lib/pipeline-model.js `denormalizeStep` (kept in sync; server cannot import
185
185
  * the frontend module). `id` leads; surfaced fields override `_extra` on clash.
186
186
  */
187
- function denormalizeModelStep(step) {
187
+ // D2/D3: the physical instruction key is VERSION-derived — `do` for TS v1,
188
+ // `intent` for v0.3 — never presence-derived. Mirrors src/lib/pipeline-model.js.
189
+ function intentKeyForVersion(version) {
190
+ return version === 1 ? 'do' : 'intent';
191
+ }
192
+
193
+ function denormalizeModelStep(step, version) {
188
194
  const out = { id: step.id };
189
195
  if (step._extra && typeof step._extra === 'object') {
190
196
  for (const key of Object.keys(step._extra)) out[key] = step._extra[key];
191
197
  }
192
198
  if (step.agent !== undefined) out.agent = step.agent;
193
199
  if (step.function !== undefined) out.function = step.function;
194
- if (step.intent !== undefined) out.intent = step.intent;
200
+ // C3/D2/D3: write the instruction to its physical field so a serialized v1 step
201
+ // never carries an undeclared `intent`. A NEW step (no `_intentKey`) defaults to
202
+ // the spec version's key, not `intent`.
203
+ if (step.intent !== undefined) out[step._intentKey || intentKeyForVersion(version)] = step.intent;
195
204
  if (step.inputs && Object.keys(step.inputs).length > 0) out.inputs = { ...step.inputs };
196
205
  if (step.output_contract !== undefined) out.output_contract = step.output_contract;
197
206
  if (Array.isArray(step.ensure) && step.ensure.length > 0) out.ensure = [...step.ensure];
@@ -210,6 +219,33 @@ function modelFlow(model, flowName) {
210
219
  return model.flows.find(f => f && f.name === flowName) || null;
211
220
  }
212
221
 
222
+ /**
223
+ * F2: is this a GENUINE full-document (spec-wide) save? An omitted `flowName` is
224
+ * NOT sufficient — a partial model (e.g. `{flows:[oneFlow]}`) with no flowName would
225
+ * otherwise perform a one-flow write through the hash-OPTIONAL spec-wide branch,
226
+ * bypassing E1's staleness guard. A true spec-wide save carries the authoritative
227
+ * full document (`model._doc`) AND covers EVERY flow present on disk (flows with a
228
+ * steps[] array, plus a workflow.steps single flow). Anything missing a disk flow
229
+ * is a flow-scoped write in disguise → classified flow-scoped (baseHash required).
230
+ */
231
+ function coversFullDocument(model, diskParsed) {
232
+ if (!model || !model._doc || typeof model._doc !== 'object') return false;
233
+ const diskFlowNames = new Set();
234
+ if (diskParsed && diskParsed.flows && typeof diskParsed.flows === 'object') {
235
+ for (const n of Object.keys(diskParsed.flows)) {
236
+ if (Array.isArray(diskParsed.flows[n]?.steps)) diskFlowNames.add(n);
237
+ }
238
+ }
239
+ if (Array.isArray(diskParsed?.workflow?.steps)) {
240
+ diskFlowNames.add(diskParsed.workflow.name || 'workflow');
241
+ }
242
+ const modelFlowNames = new Set((model.flows || []).map(f => f && f.name).filter(Boolean));
243
+ for (const n of diskFlowNames) {
244
+ if (!modelFlowNames.has(n)) return false;
245
+ }
246
+ return true;
247
+ }
248
+
213
249
  // The reserved built-in contract — never written as a user contract (mirrors
214
250
  // src/lib/pipeline-model.js serializeContracts; server can't import the FE module).
215
251
  const RESERVED_CONTRACT = 'TaskGraph';
@@ -470,14 +506,32 @@ function surfacedValue(step, key) {
470
506
  */
471
507
  function mergeStepNode(doc, mapNode, step, specWide = false) {
472
508
  const extra = (step._extra && typeof step._extra === 'object') ? step._extra : {};
509
+ // C3/D2: the surfaced `intent` is written to its PHYSICAL field — `do` for TS v1,
510
+ // `intent` for v0.3. Prefer the model's remembered key; fall back to the spec
511
+ // VERSION (never the disk node's presence — a v1 step carrying both fields must
512
+ // resolve to `do`, and the stray `intent` is cleaned up below).
513
+ const intentKey = step._intentKey || (intentKeyForVersion(doc.get('version')));
473
514
  if (!specWide) {
474
- // Additive: add only missing keys.
515
+ // D4: DIFF-DRIVEN, and NEVER delete. A key absent from an incomplete client
516
+ // _extra is preserved (finding 2); a key PRESENT and CHANGED is written back —
517
+ // whatever it is. This persists every field a renameStep rewrote (C4: after[],
518
+ // nested gate routes, AND `when`/`set`/fanout.over/with templates) without an
519
+ // allowlist, so new template-bearing fields are future-proofed. Unchanged keys
520
+ // are left untouched, preserving their comments.
475
521
  for (const k of Object.keys(extra)) {
476
- if (!mapNode.has(k)) mapNode.set(k, doc.createNode(extra[k]));
522
+ if (!mapNode.has(k)) {
523
+ mapNode.set(k, doc.createNode(extra[k]));
524
+ } else {
525
+ const diskNode = mapNode.get(k, true);
526
+ const diskJs = diskNode && typeof diskNode.toJSON === 'function' ? diskNode.toJSON() : mapNode.get(k);
527
+ if (!deepEqual(diskJs, extra[k])) mapNode.set(k, doc.createNode(extra[k]));
528
+ }
477
529
  }
478
530
  } else {
479
- // Full reconcile of unsurfaced fields.
480
- const surfacedSet = new Set(['id', ...SURFACED_STEP_KEYS]);
531
+ // Full reconcile of unsurfaced fields. The physical intent key is surfaced
532
+ // (below), so it must be treated as surfaced here too — never deleted as an
533
+ // unknown extra.
534
+ const surfacedSet = new Set(['id', ...SURFACED_STEP_KEYS, intentKey]);
481
535
  // Add / update changed extra keys (skip unchanged to preserve their comments).
482
536
  for (const k of Object.keys(extra)) {
483
537
  if (!mapNode.has(k)) {
@@ -500,8 +554,15 @@ function mergeStepNode(doc, mapNode, step, specWide = false) {
500
554
  if (mapNode.get('id') !== step.id) mapNode.set('id', step.id);
501
555
  for (const key of SURFACED_STEP_KEYS) {
502
556
  const { present, value } = surfacedValue(step, key);
503
- if (present) mapNode.set(key, doc.createNode(value));
504
- else if (mapNode.has(key)) mapNode.delete(key);
557
+ // C3: the `intent` surfaced key maps to its physical field (`do`/`intent`).
558
+ const diskKey = key === 'intent' ? intentKey : key;
559
+ if (present) mapNode.set(diskKey, doc.createNode(value));
560
+ else if (mapNode.has(diskKey)) mapNode.delete(diskKey);
561
+ // Never leave the stale sibling — e.g. a python-era `intent` on a v1 step.
562
+ if (key === 'intent') {
563
+ const stray = intentKey === 'do' ? 'intent' : 'do';
564
+ if (mapNode.has(stray)) mapNode.delete(stray);
565
+ }
505
566
  }
506
567
  return mapNode;
507
568
  }
@@ -804,18 +865,6 @@ export function attachPipelineRoutes(app, { broadcastMessage, scheduleBroadcast,
804
865
  return res.status(400).json({ error: 'model with flows[] is required' });
805
866
  }
806
867
 
807
- // Determine which flows to write back. If flowName is given, write only that
808
- // flow (the editor edits one flow at a time); otherwise write every flow the
809
- // model carries that exists in the document.
810
- const flowsToWrite = flowName
811
- ? [flowName]
812
- : model.flows.map(f => f && f.name).filter(Boolean);
813
-
814
- // A SPEC-WIDE save (flowName omitted) treats model._doc as the authoritative
815
- // full document: reconcilers update AND delete, not just add. The flow-scoped
816
- // path (flowName given) stays additive and never touches other flows/blocks.
817
- const specWide = !flowName;
818
-
819
868
  let doc;
820
869
  let diskParsed;
821
870
  let diskText;
@@ -825,10 +874,24 @@ export function attachPipelineRoutes(app, { broadcastMessage, scheduleBroadcast,
825
874
  return res.status(400).json({ error: `Cannot read current spec: ${e.message}` });
826
875
  }
827
876
 
828
- // COMP-PIPE-EDIT-6 conflict detection: if the client sent a baseHash and the
829
- // freshly-read disk text no longer matches it, the file diverged since load
830
- // (another writer / external edit). Refuse to write (409) unless force:true.
831
- if (baseHash && !force) {
877
+ // Conflict detection (COMP-PIPE-EDIT-6 + E1/G1). ANY save of an existing file
878
+ // resubmits a model derived from a snapshot of disk; a stale editor (loaded old,
879
+ // another writer saved new, this client resubmits its stale value) would SILENTLY
880
+ // resurrect the old value on merge. G1: proving "full-document-ness" from request
881
+ // shape is spoofable (an empty `_doc` + stub flow bodies covering every disk flow
882
+ // NAME would slip through a hash-optional spec-wide branch and destructively
883
+ // delete real steps), so the guard NO LONGER keys on classification: a baseHash
884
+ // is REQUIRED on EVERY save of an existing file — spec-wide and flow-scoped alike.
885
+ // Missing → 400, stale → 409. F1: ONLY strict `force === true` bypasses — a
886
+ // truthy-but-non-boolean `force` ("false", 1, {}) must NOT authorize the bypass.
887
+ // G3: this check runs on the RAW disk bytes BEFORE parsing, so a stale baseHash
888
+ // is a 409 even when the on-disk YAML is malformed (parse-independent semantics).
889
+ if (force !== true) {
890
+ if (typeof baseHash !== 'string' || baseHash.length === 0) {
891
+ return res.status(400).json({
892
+ error: 'baseHash is required to save an existing spec (reload the spec, or pass force:true to override)',
893
+ });
894
+ }
832
895
  const currentHash = sha256(diskText);
833
896
  if (currentHash !== baseHash) {
834
897
  return res.status(409).json({
@@ -846,6 +909,22 @@ export function attachPipelineRoutes(app, { broadcastMessage, scheduleBroadcast,
846
909
  return res.status(400).json({ error: `Failed to parse current spec: ${e.message}` });
847
910
  }
848
911
 
912
+ // A SPEC-WIDE save treats model._doc as the authoritative full document:
913
+ // reconcilers update AND delete, not just add. The flow-scoped path stays
914
+ // additive and never touches other flows/blocks. This classification now drives
915
+ // MERGE BEHAVIOR ONLY (full replace vs additive) — it never relaxes the baseHash
916
+ // guard above (G1). An omitted `flowName` alone does NOT make a save spec-wide:
917
+ // the model must genuinely cover the full document (model._doc + every disk flow),
918
+ // else a partial model writes additively as a flow-scoped save.
919
+ const specWide = !flowName && coversFullDocument(model, diskParsed);
920
+
921
+ // Determine which flows to write back. If flowName is given, write only that
922
+ // flow (the editor edits one flow at a time); otherwise write every flow the
923
+ // model carries that exists in the document.
924
+ const flowsToWrite = flowName
925
+ ? [flowName]
926
+ : model.flows.map(f => f && f.name).filter(Boolean);
927
+
849
928
  for (const name of flowsToWrite) {
850
929
  const flow = modelFlow(model, name);
851
930
  if (!flow) {
@@ -868,10 +947,13 @@ export function attachPipelineRoutes(app, { broadcastMessage, scheduleBroadcast,
868
947
  // COMP-PIPE-EDIT-5: the flow is not on disk yet (e.g. a just-collapsed
869
948
  // sub-flow that exists only in the model). CREATE the flows.<name> node —
870
949
  // steps + input + output from model._doc.flows — rather than erroring.
871
- // ONLY on a SPEC-WIDE save (flowName omitted): an explicit flowName that
872
- // doesn't map to a steps location is still a 400 (never auto-create a
873
- // flow the caller named by mistake / clobber a workflow.steps spec).
874
- const created = !flowName && createNewFlowNode(doc, model, name);
950
+ // ONLY on a genuine SPEC-WIDE save: G2 gate on the computed `specWide`
951
+ // classification, not a bare `!flowName`. A flow-scoped-classified request
952
+ // (no flowName but a partial model that does NOT cover the full document)
953
+ // must NOT perform this documented spec-wide-only flow creation; an explicit
954
+ // flowName that doesn't map to a steps location is likewise a 400 (never
955
+ // auto-create a flow the caller named by mistake / clobber a workflow.steps).
956
+ const created = specWide && createNewFlowNode(doc, model, name);
875
957
  if (!created) {
876
958
  return res.status(400).json({ error: `Flow "${name}" has no steps location in the spec` });
877
959
  }
@@ -902,7 +984,7 @@ export function attachPipelineRoutes(app, { broadcastMessage, scheduleBroadcast,
902
984
  }
903
985
  return existing
904
986
  ? mergeStepNode(doc, existing, step, specWide)
905
- : doc.createNode(denormalizeModelStep(step));
987
+ : doc.createNode(denormalizeModelStep(step, doc.get('version')));
906
988
  });
907
989
  }
908
990
 
@@ -1041,7 +1123,7 @@ function buildSpecObjectFromModel(model) {
1041
1123
  if (Array.isArray(doc?.workflow?.steps)) {
1042
1124
  const wfName = doc.workflow.name || 'workflow';
1043
1125
  const flow = byName.get(wfName);
1044
- if (flow) out.workflow = { ...doc.workflow, steps: (flow.steps || []).map(denormalizeModelStep) };
1126
+ if (flow) out.workflow = { ...doc.workflow, steps: (flow.steps || []).map(s => denormalizeModelStep(s, doc.version)) };
1045
1127
  }
1046
1128
 
1047
1129
  if (doc?.flows && typeof doc.flows === 'object') {
@@ -1049,7 +1131,7 @@ function buildSpecObjectFromModel(model) {
1049
1131
  for (const name of Object.keys(doc.flows)) {
1050
1132
  const flow = byName.get(name);
1051
1133
  out.flows[name] = flow
1052
- ? { ...doc.flows[name], steps: (flow.steps || []).map(denormalizeModelStep) }
1134
+ ? { ...doc.flows[name], steps: (flow.steps || []).map(s => denormalizeModelStep(s, doc.version)) }
1053
1135
  : doc.flows[name];
1054
1136
  }
1055
1137
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
- * stratum-client.js — Single adapter for all stratum-mcp subprocess calls.
2
+ * stratum-client.js — Single adapter for all Stratum TS CLI subprocess calls.
3
3
  *
4
- * This is the ONLY module in compose that spawns stratum-mcp processes.
4
+ * This is the ONLY module in compose that spawns Stratum CLI processes.
5
5
  * All query and mutation calls go through the exported functions below.
6
- * No other file may call execFile/spawn with 'stratum-mcp' as the command.
6
+ * No other file may spawn the Stratum CLI directly.
7
7
  *
8
8
  * Contract:
9
9
  * - Query calls: 5s timeout, 1 retry on timeout, no retry on error
@@ -15,9 +15,8 @@
15
15
  */
16
16
 
17
17
  import { execFile as _execFileDefault } from 'node:child_process';
18
- import { loadProjectConfig } from './project-root.js';
19
-
20
- const STRATUM_BIN = 'stratum-mcp';
18
+ import { getTargetRoot } from './project-root.js';
19
+ import { resolveStratumBin, resolveStratumEngine as resolveEngine } from '../lib/stratum-engine.js';
21
20
 
22
21
  // Injected executor — replaced by tests only. Production code never calls this setter.
23
22
  let _execFile = _execFileDefault;
@@ -28,31 +27,21 @@ const MUTATION_TIMEOUT_MS = 10_000;
28
27
  // ---------------------------------------------------------------------------
29
28
  // Engine selection (COMP-STRATUM-TS)
30
29
  //
31
- // The flow/gate seam is engine-selectable: Python `stratum-mcp` (default) or
32
- // the TS engine's `stratum` CLI, which emits the same JSON projections and
33
- // exit codes. Guard calls are PINNED to Python — STRAT-GUARD is not part of
34
- // the TS port. Resolution order: COMPOSE_STRATUM_ENGINE env override, then
35
- // capabilities.stratumEngine in .compose/compose.json, then "python".
36
- // Unknown values fail loudly; never a silent fallback.
30
+ // All flow, gate, and guard calls use the TS CLI. The shared resolver still
31
+ // reads the environment/project capability so a retired Python selection fails
32
+ // loudly before any subprocess is spawned.
37
33
  // ---------------------------------------------------------------------------
38
34
 
39
- /** @returns {'python'|'ts'} */
35
+ /** @returns {'ts'} */
40
36
  export function resolveStratumEngine() {
41
- let value = process.env.COMPOSE_STRATUM_ENGINE;
42
- if (value === undefined || value === '') {
43
- try { value = loadProjectConfig()?.capabilities?.stratumEngine; } catch { value = undefined; }
44
- }
45
- if (value === undefined || value === '') return 'python';
46
- if (value !== 'python' && value !== 'ts') {
47
- throw new Error(`stratumEngine must be "python" or "ts", got ${JSON.stringify(value)}`);
48
- }
49
- return value;
37
+ return resolveEngine(getTargetRoot());
50
38
  }
51
39
 
52
40
  /** Binary for flow/gate query+mutation calls under the selected engine. */
53
41
  function flowGateBin() {
54
- if (resolveStratumEngine() === 'ts') return process.env.COMPOSE_STRATUM_TS_BIN || 'stratum';
55
- return STRATUM_BIN;
42
+ const cwd = getTargetRoot();
43
+ resolveEngine(cwd);
44
+ return resolveStratumBin('cli', cwd);
56
45
  }
57
46
 
58
47
  // ---------------------------------------------------------------------------
@@ -65,10 +54,10 @@ function flowGateBin() {
65
54
  *
66
55
  * @param {string[]} args
67
56
  * @param {number} timeoutMs
68
- * @param {string} [bin] — defaults to the Python stratum-mcp binary
57
+ * @param {string} [bin] — explicit binary selected by the calling seam
69
58
  * @returns {Promise<{ stdout: string, stderr: string, code: number }>}
70
59
  */
71
- function spawnStratum(args, timeoutMs, bin = STRATUM_BIN) {
60
+ function spawnStratum(args, timeoutMs, bin) {
72
61
  return new Promise((resolve) => {
73
62
  const proc = _execFile(bin, args, { timeout: timeoutMs }, (err, out, err2) => {
74
63
  resolve(_spawnResult(bin, err, out, err2));
@@ -110,9 +99,7 @@ function _spawnResult(bin, err, out, err2) {
110
99
 
111
100
  /** Binary-specific spawn-failure message with the install/path remedy. */
112
101
  function _spawnRemedy(bin, code) {
113
- return bin === STRATUM_BIN
114
- ? `stratum-mcp failed to spawn (${code}). Install with: pip install stratum-mcp`
115
- : `${bin} (TS stratum engine) failed to spawn (${code}). Install @smartmemory/stratum or set COMPOSE_STRATUM_TS_BIN`;
102
+ return `${bin} (TS stratum engine) failed to spawn (${code}). Install @smartmemory/stratum or set COMPOSE_STRATUM_TS_CLI_BIN`;
116
103
  }
117
104
 
118
105
  /**
@@ -128,7 +115,7 @@ async function runQuery(args) {
128
115
  // Retry once on timeout
129
116
  result = await spawnStratum(args, QUERY_TIMEOUT_MS, bin);
130
117
  if (result.code === -1) {
131
- return { error: { code: 'TIMEOUT', message: 'stratum-mcp query timed out', detail: '' } };
118
+ return { error: { code: 'TIMEOUT', message: 'Stratum query timed out', detail: '' } };
132
119
  }
133
120
  }
134
121
 
@@ -142,14 +129,14 @@ async function runQuery(args) {
142
129
  try {
143
130
  return JSON.parse(result.stdout);
144
131
  } catch {
145
- return { error: { code: 'UNKNOWN', message: 'stratum-mcp query failed', detail: '' } };
132
+ return { error: { code: 'UNKNOWN', message: 'Stratum query failed', detail: '' } };
146
133
  }
147
134
  }
148
135
 
149
136
  try {
150
137
  return JSON.parse(result.stdout);
151
138
  } catch {
152
- return { error: { code: 'PARSE_ERROR', message: 'stratum-mcp returned invalid JSON', detail: '' } };
139
+ return { error: { code: 'PARSE_ERROR', message: 'Stratum returned invalid JSON', detail: '' } };
153
140
  }
154
141
  }
155
142
 
@@ -162,7 +149,7 @@ async function runMutation(args) {
162
149
  const result = await spawnStratum(args, MUTATION_TIMEOUT_MS, flowGateBin());
163
150
 
164
151
  if (result.code === -1) {
165
- return { error: { code: 'TIMEOUT', message: 'stratum-mcp gate timed out', detail: '' } };
152
+ return { error: { code: 'TIMEOUT', message: 'Stratum gate timed out', detail: '' } };
166
153
  }
167
154
 
168
155
  if (result.code === -2) {
@@ -183,36 +170,35 @@ async function runMutation(args) {
183
170
  try {
184
171
  return JSON.parse(result.stdout);
185
172
  } catch {
186
- return { error: { code: 'UNKNOWN', message: 'stratum-mcp gate failed', detail: '' } };
173
+ return { error: { code: 'UNKNOWN', message: 'Stratum gate failed', detail: '' } };
187
174
  }
188
175
  }
189
176
 
190
177
  try {
191
178
  return JSON.parse(result.stdout);
192
179
  } catch {
193
- return { error: { code: 'PARSE_ERROR', message: 'stratum-mcp returned invalid JSON', detail: '' } };
180
+ return { error: { code: 'PARSE_ERROR', message: 'Stratum returned invalid JSON', detail: '' } };
194
181
  }
195
182
  }
196
183
 
197
184
  /**
198
- * Spawn stratum-mcp with args and pipe `inputJson` (a string) on stdin.
185
+ * Spawn the TS Stratum CLI with args and pipe `inputJson` on stdin.
199
186
  * Used by the STRAT-GUARD adapter, whose CLI reads one JSON kwargs object from
200
- * stdin. Same resolve contract as spawnStratum. PINNED to the Python binary —
201
- * STRAT-GUARD is not part of the TS port and ignores the engine flag.
187
+ * stdin. Same resolve contract as spawnStratum.
202
188
  *
203
189
  * @param {string[]} args
204
190
  * @param {string} inputJson
205
191
  * @param {number} timeoutMs
206
192
  * @returns {Promise<{ stdout: string, stderr: string, code: number }>}
207
193
  */
208
- function spawnStratumStdin(args, inputJson, timeoutMs) {
194
+ function spawnStratumStdin(args, inputJson, timeoutMs, bin) {
209
195
  return new Promise((resolve) => {
210
- const proc = _execFile(STRATUM_BIN, args, { timeout: timeoutMs }, (err, out, err2) => {
211
- resolve(_spawnResult(STRATUM_BIN, err, out, err2));
196
+ const proc = _execFile(bin, args, { timeout: timeoutMs }, (err, out, err2) => {
197
+ resolve(_spawnResult(bin, err, out, err2));
212
198
  });
213
199
  // Same both-paths-settle-identically contract as spawnStratum.
214
200
  proc.on('error', (err) => {
215
- resolve(_spawnResult(STRATUM_BIN, err ?? new Error('child process error'), '', ''));
201
+ resolve(_spawnResult(bin, err ?? new Error('child process error'), '', ''));
216
202
  });
217
203
 
218
204
  // Feed the JSON kwargs on stdin. The test mock supplies a fake stdin; a
@@ -234,10 +220,10 @@ function spawnStratumStdin(args, inputJson, timeoutMs) {
234
220
  * @returns {Promise<any>} parsed JSON result or { error }
235
221
  */
236
222
  async function runGuard(action, kwargs, timeoutMs = MUTATION_TIMEOUT_MS) {
237
- const result = await spawnStratumStdin(['guard', action], JSON.stringify(kwargs), timeoutMs);
223
+ const result = await spawnStratumStdin(['guard', action], JSON.stringify(kwargs), timeoutMs, flowGateBin());
238
224
 
239
225
  if (result.code === -1) {
240
- return { error: { code: 'TIMEOUT', message: 'stratum-mcp guard timed out', detail: '' } };
226
+ return { error: { code: 'TIMEOUT', message: 'Stratum guard timed out', detail: '' } };
241
227
  }
242
228
  if (result.code === -2) {
243
229
  console.error('[stratum-client] guard spawn failure:', result.stderr);
@@ -248,13 +234,13 @@ async function runGuard(action, kwargs, timeoutMs = MUTATION_TIMEOUT_MS) {
248
234
  try {
249
235
  return JSON.parse(result.stdout); // canonical { status:"error", ... }
250
236
  } catch {
251
- return { error: { code: 'UNKNOWN', message: 'stratum-mcp guard failed', detail: '' } };
237
+ return { error: { code: 'UNKNOWN', message: 'Stratum guard failed', detail: '' } };
252
238
  }
253
239
  }
254
240
  try {
255
241
  return JSON.parse(result.stdout);
256
242
  } catch {
257
- return { error: { code: 'PARSE_ERROR', message: 'stratum-mcp returned invalid JSON', detail: '' } };
243
+ return { error: { code: 'PARSE_ERROR', message: 'Stratum returned invalid JSON', detail: '' } };
258
244
  }
259
245
  }
260
246
 
@@ -334,7 +320,7 @@ export async function gateRevise(flowId, stepId, note = '', resolvedBy = 'human'
334
320
  //
335
321
  // Reaches stratum's guarded-transition primitive over the same CLI-subprocess
336
322
  // seam. Each function translates camelCase params into the snake_case JSON
337
- // kwargs the `stratum-mcp guard <action>` CLI forwards verbatim to the guard
323
+ // kwargs the `stratum guard <action>` CLI forwards verbatim to the guard
338
324
  // library, and pipes them on stdin.
339
325
  // ---------------------------------------------------------------------------
340
326
 
@@ -4,8 +4,7 @@
4
4
  * Polls stratum flow state via stratum-client (not direct file reads) and syncs
5
5
  * into the vision store on a self-scheduling, sleep-aware loop.
6
6
  *
7
- * Sleep behavior: each poll cold-starts a `stratum-mcp` subprocess (Python
8
- * interpreter spin-up — the expensive part, not the query itself). Doing that
7
+ * Sleep behavior: each poll cold-starts the Stratum TS CLI. Doing that
9
8
  * every few seconds generated enough periodic activity to keep the host from
10
9
  * sleeping. Two mitigations:
11
10
  * 1. Cadence widened to 60s (override with COMPOSE_STRATUM_POLL_MS).
@@ -37,7 +36,7 @@ const WAKE_GAP_FACTOR = 1.5;
37
36
  /**
38
37
  * Sleep-aware poll gate: true only when some bound vision item could still
39
38
  * change state (i.e. is bound to a flow and not settled-`complete`). When this
40
- * is false the poller skips the stratum-mcp spawn so an idle host can sleep.
39
+ * is false the poller skips the CLI spawn so an idle host can sleep.
41
40
  *
42
41
  * A bound `complete` item is steady-state (status only advances to `complete`
43
42
  * via the audit route, never back), so polling it changes nothing. Anything
@@ -97,7 +96,7 @@ export class StratumSync {
97
96
  }
98
97
 
99
98
  /**
100
- * One poll tick. Skips the stratum-mcp spawn when nothing is live (lets the
99
+ * One poll tick. Skips the Stratum CLI spawn when nothing is live (lets the
101
100
  * host sleep); on a tick that fired far later than scheduled, notes the
102
101
  * sleep gap before resyncing.
103
102
  */
@@ -275,7 +275,7 @@ export class VisionServer {
275
275
  console.log('[vision] Stratum sync enabled');
276
276
  } else {
277
277
  app.use('/api/stratum', (_req, res) => {
278
- res.status(503).json({ error: 'Stratum not enabled', hint: 'pip install stratum && compose init' });
278
+ res.status(503).json({ error: 'Stratum not enabled', hint: 'compose init (TS engine; python retired — see python-legacy branch)' });
279
279
  });
280
280
  }
281
281
 
@@ -1 +0,0 @@
1
- import{ag as o,ah as n}from"./App-s4CulNkO.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-DUscIydR.js";import{_ as i}from"./App-s4CulNkO.js";import"./chunk-5VM5RSS4-DcTpMdJ4.js";import"./chunk-XXDRQBXY-DIH9i8mZ.js";import"./chunk-VR4S4FIN-C4UDHq_a.js";import"./chunk-32BRIVSS-DCHhvgFV.js";import"./mobile-PV1zQVwv.js";import"./index-CBWbmG7W.js";import"./graph--wGAvskB.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-DUscIydR.js";import{_ as i}from"./App-s4CulNkO.js";import"./chunk-5VM5RSS4-DcTpMdJ4.js";import"./chunk-XXDRQBXY-DIH9i8mZ.js";import"./chunk-VR4S4FIN-C4UDHq_a.js";import"./chunk-32BRIVSS-DCHhvgFV.js";import"./mobile-PV1zQVwv.js";import"./index-CBWbmG7W.js";import"./graph--wGAvskB.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};