@smartmemory/compose 0.2.57-beta → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +2 -2
  2. package/bin/compose.js +55 -54
  3. package/contracts/feature-json.schema.json +16 -0
  4. package/dist/assets/{App-BmhlHOXF.js → App-DJ5xk_Wx.js} +219 -219
  5. package/dist/assets/{abnfDiagram-VRR7QNED-CMOTYAnt.js → abnfDiagram-VRR7QNED-BPWGdCFx.js} +1 -1
  6. package/dist/assets/{arc-BJAv_6dL.js → arc-DX5jqmcO.js} +1 -1
  7. package/dist/assets/{architectureDiagram-ZJ3FMSHR-xa4DLUcp.js → architectureDiagram-ZJ3FMSHR-B6jmNVw7.js} +1 -1
  8. package/dist/assets/{blockDiagram-677ZJIJ3-BdkDqeQF.js → blockDiagram-677ZJIJ3-DAx2i-sB.js} +1 -1
  9. package/dist/assets/{c4Diagram-LMCZKHZV-DiNrLIuL.js → c4Diagram-LMCZKHZV-zmsZCplj.js} +1 -1
  10. package/dist/assets/channel-vsDnTvkh.js +1 -0
  11. package/dist/assets/{chunk-2Q5K7J3B-BsjtgrSL.js → chunk-2Q5K7J3B-3QSh6sI7.js} +1 -1
  12. package/dist/assets/{chunk-32BRIVSS-DpImULWy.js → chunk-32BRIVSS-BiH5Di_y.js} +1 -1
  13. package/dist/assets/{chunk-5VM5RSS4-DTdG5JnR.js → chunk-5VM5RSS4-C0EfjFLd.js} +1 -1
  14. package/dist/assets/{chunk-EX3LRPZG-Cj1jNuKY.js → chunk-EX3LRPZG-BGyfmVm-.js} +1 -1
  15. package/dist/assets/{chunk-JWPE2WC7-DwQ0wwWn.js → chunk-JWPE2WC7-Ba0mrNlg.js} +1 -1
  16. package/dist/assets/{chunk-MOJQB5TN-DZZa7unc.js → chunk-MOJQB5TN-D3QO9EzH.js} +1 -1
  17. package/dist/assets/{chunk-RYQCIY6F-CgdjqYQj.js → chunk-RYQCIY6F-D1uvNA6d.js} +1 -1
  18. package/dist/assets/{chunk-V7JOEXUC-DrPIj6XI.js → chunk-V7JOEXUC-fihlnodT.js} +1 -1
  19. package/dist/assets/{chunk-VR4S4FIN-BBUOVC5W.js → chunk-VR4S4FIN-nmRUEo1H.js} +1 -1
  20. package/dist/assets/{chunk-XXDRQBXY-BEIiZ6gS.js → chunk-XXDRQBXY-CM693yEg.js} +1 -1
  21. package/dist/assets/classDiagram-OUVF2IWQ-DO-wdSFZ.js +1 -0
  22. package/dist/assets/classDiagram-v2-EOCWNBFH-DO-wdSFZ.js +1 -0
  23. package/dist/assets/{cose-bilkent-JH36ORCC-ClFTQEyD.js → cose-bilkent-JH36ORCC-CkacPn8W.js} +1 -1
  24. package/dist/assets/{cynefin-VYW2F7L2-5Bp7xFO0.js → cynefin-VYW2F7L2-BiHaIptG.js} +1 -1
  25. package/dist/assets/{cynefinDiagram-TSTJHNR4-C-0QlSda.js → cynefinDiagram-TSTJHNR4-CdKxMmiy.js} +1 -1
  26. package/dist/assets/{dagre-VKFMJZFB-DLkqsfBy.js → dagre-VKFMJZFB-BQLY5y_W.js} +1 -1
  27. package/dist/assets/{diagram-FQU43EPY-Cwh_tJ_r.js → diagram-FQU43EPY-D7uMBHvq.js} +1 -1
  28. package/dist/assets/{diagram-G47NLZAW-CvvAjulf.js → diagram-G47NLZAW-B3Z1cuH7.js} +1 -1
  29. package/dist/assets/{diagram-NH7WQ7WH-CCoyx8_e.js → diagram-NH7WQ7WH-BZyRD45e.js} +1 -1
  30. package/dist/assets/{diagram-OA4YK3LP-DfjhKYCp.js → diagram-OA4YK3LP-DcThOTt7.js} +1 -1
  31. package/dist/assets/{diagram-WEI45ONY-Vjz0ygfB.js → diagram-WEI45ONY-BcgRAkqY.js} +1 -1
  32. package/dist/assets/{ebnfDiagram-CCIWWBDH-GsA8c6g2.js → ebnfDiagram-CCIWWBDH-CGwfO_xH.js} +1 -1
  33. package/dist/assets/{erDiagram-Q63AITRT-Cc0lJ9sQ.js → erDiagram-Q63AITRT-pV58-Ncc.js} +1 -1
  34. package/dist/assets/{flowDiagram-23GEKE2U-C_sotK-k.js → flowDiagram-23GEKE2U-DJ_SqE8h.js} +1 -1
  35. package/dist/assets/{ganttDiagram-NO4QXBWP-BBtu_WUe.js → ganttDiagram-NO4QXBWP-Dgy0Iyss.js} +1 -1
  36. package/dist/assets/{gitGraphDiagram-IHSO6WYX-BV_aSB8-.js → gitGraphDiagram-IHSO6WYX-CbiZw9fb.js} +1 -1
  37. package/dist/assets/{index-CKfOVv2N.js → index-DZTJEk-y.js} +2 -2
  38. package/dist/assets/{infoDiagram-FWYZ7A6U-DMAvNGtX.js → infoDiagram-FWYZ7A6U-CtsyyEc-.js} +1 -1
  39. package/dist/assets/{ishikawaDiagram-FXEZZL3T-DfQB91MZ.js → ishikawaDiagram-FXEZZL3T-BJilNFkK.js} +1 -1
  40. package/dist/assets/{journeyDiagram-5HDEW3XC-Bq4Llm9o.js → journeyDiagram-5HDEW3XC-C2UCMP4t.js} +1 -1
  41. package/dist/assets/{kanban-definition-HUTT4EX6-DTXHiDiE.js → kanban-definition-HUTT4EX6-DmLDJBRy.js} +1 -1
  42. package/dist/assets/{linear-BQSYZcVg.js → linear-C1paCqE7.js} +1 -1
  43. package/dist/assets/{mindmap-definition-LN4V7U3C-KuH2NIj0.js → mindmap-definition-LN4V7U3C-CX-RxKVn.js} +1 -1
  44. package/dist/assets/{pegDiagram-2B236MQR-BKoNGQFr.js → pegDiagram-2B236MQR-CTU32H2W.js} +1 -1
  45. package/dist/assets/{pieDiagram-ENE6RG2P-EpYv162Y.js → pieDiagram-ENE6RG2P-D8L1aYoo.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-ABIIQ3AL-C9deUjQ2.js → quadrantDiagram-ABIIQ3AL-hQ3bXosy.js} +1 -1
  47. package/dist/assets/{railroadDiagram-RFXS5EU6-Csqw2hK9.js → railroadDiagram-RFXS5EU6--_vuYcda.js} +1 -1
  48. package/dist/assets/{requirementDiagram-TGXJPOKE-DCuz3emr.js → requirementDiagram-TGXJPOKE-C6h_m3Az.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-HTMAVEWB-BTeTL24f.js → sankeyDiagram-HTMAVEWB-Dpc7CfIQ.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-DBY2YBRQ-Cf1ZkTwO.js → sequenceDiagram-DBY2YBRQ-CK5ZzbvJ.js} +1 -1
  51. package/dist/assets/{sizeCapture-X5ZJPWSS-D3Lbewzg.js → sizeCapture-X5ZJPWSS-BAxVvM9G.js} +1 -1
  52. package/dist/assets/{stateDiagram-2N3HPSRC-O8l37yJI.js → stateDiagram-2N3HPSRC-7j33_2PY.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-6OUMAXLB-GviN0FpJ.js +1 -0
  54. package/dist/assets/{swimlanes-5IMT3BWC-BikmTh8b.js → swimlanes-5IMT3BWC-CBG2yOob.js} +2 -2
  55. package/dist/assets/swimlanesDiagram-G3AALYLV-D0wMoxl1.js +8 -0
  56. package/dist/assets/{timeline-definition-FHXFAJF6-BOxSosG0.js → timeline-definition-FHXFAJF6-BOnZkQvc.js} +1 -1
  57. package/dist/assets/{vennDiagram-L72KCM5P-BSClOErZ.js → vennDiagram-L72KCM5P-D52NhIfj.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-EHGQE667-DHFRjOK0.js → wardleyDiagram-EHGQE667-YoPY2ZU-.js} +1 -1
  59. package/dist/assets/{xychartDiagram-FW5EYKEG-Dh1v4x-O.js → xychartDiagram-FW5EYKEG-BanNfgtO.js} +1 -1
  60. package/dist/index.html +1 -1
  61. package/lib/build-all.js +0 -5
  62. package/lib/build-stream-schema.js +1 -1
  63. package/lib/build.js +1809 -2646
  64. package/lib/consumer-fanout.js +1317 -0
  65. package/lib/escalation.js +69 -0
  66. package/lib/feature-validator.js +6 -4
  67. package/lib/feature-writer.js +58 -3
  68. package/lib/flow-state.js +15 -14
  69. package/lib/gsd-budget.js +48 -10
  70. package/lib/gsd-prompt.js +3 -4
  71. package/lib/gsd-stuck.js +1 -1
  72. package/lib/gsd.js +303 -149
  73. package/lib/lane-gate.js +200 -0
  74. package/lib/local-claude-connector.js +149 -0
  75. package/lib/new.js +162 -307
  76. package/lib/result-normalizer.js +233 -18
  77. package/lib/review-lenses.js +1 -1
  78. package/lib/step-prompt.js +41 -119
  79. package/lib/stratum-engine.js +297 -0
  80. package/lib/stratum-mcp-client.js +224 -213
  81. package/lib/triage.js +144 -0
  82. package/lib/vocabulary-compliance.js +268 -0
  83. package/lib/vocabulary-inject.js +1 -36
  84. package/package.json +2 -1
  85. package/pipelines/build-quick.stratum.yaml +5 -17
  86. package/pipelines/build.profiles.json +11 -0
  87. package/pipelines/build.stratum.yaml +288 -467
  88. package/pipelines/gsd.stratum.yaml +73 -125
  89. package/pipelines/new.stratum.yaml +68 -149
  90. package/server/build-routes.js +4 -1
  91. package/server/design-routes.js +37 -21
  92. package/server/index.js +13 -21
  93. package/server/lifecycle-guard.js +1 -1
  94. package/server/pipeline-routes.js +113 -31
  95. package/server/stratum-client.js +33 -47
  96. package/server/stratum-sync.js +3 -4
  97. package/server/vision-server.js +1 -1
  98. package/dist/assets/channel-CYErfopw.js +0 -1
  99. package/dist/assets/classDiagram-OUVF2IWQ-qCC_hnXu.js +0 -1
  100. package/dist/assets/classDiagram-v2-EOCWNBFH-qCC_hnXu.js +0 -1
  101. package/dist/assets/stateDiagram-v2-6OUMAXLB-BWqk_9py.js +0 -1
  102. package/dist/assets/swimlanesDiagram-G3AALYLV-C60ICdks.js +0 -8
  103. package/lib/connector-factory-shim.js +0 -167
  104. package/server/agent-mcp.js +0 -10
@@ -0,0 +1,69 @@
1
+ /**
2
+ * escalation.js — Pure escalation decision for the triage lane ladder.
3
+ *
4
+ * COMP-TRIAGE-5 / S03 (see docs/features/COMP-TRIAGE-5/plan.md, blueprint.md C3).
5
+ *
6
+ * `escalate` is a PURE decision function: no file I/O, no imports of build.js
7
+ * or the vision store. The caller (lib/build.js's escalation observer) reads
8
+ * currentPhase from the vision store itself and performs all side effects
9
+ * (un-skipping phases, re-entering, writing checkpoints, human handoff).
10
+ */
11
+
12
+ /** Lane ladder, low → high. Exported so callers/tests (and S04) share one order. */
13
+ export const LANE_ORDER = ['trivial', 'standard', 'complex'];
14
+
15
+ /** Bound on escalation attempts before forcing a human handoff (STOP). */
16
+ const MAX_ESCALATIONS = 2;
17
+
18
+ /** Earliest phase to re-enter for each escalated-to lane. */
19
+ const RE_ENTRY_PHASE_BY_LANE = {
20
+ standard: 'blueprint',
21
+ complex: 'design',
22
+ };
23
+
24
+ /**
25
+ * Decide whether/how to escalate the current build lane in response to a
26
+ * normalized gate result.
27
+ *
28
+ * @param {{gate: 'review'|'test', passed: boolean}} normalizedGate - normalized gate signal.
29
+ * @param {'trivial'|'standard'|'complex'} currentLane - current lane on the ladder.
30
+ * @param {number} escalationCount - number of escalations already performed for this feature.
31
+ * @param {string} [currentPhase] - caller-supplied current phase (from the vision store).
32
+ * Not read or fetched here — the caller owns all I/O. Reserved for future use to avoid
33
+ * re-entering a phase later than necessary than the lane's default reEntryPhase; ignored
34
+ * in this implementation (see Deviation note below).
35
+ * @returns {null|'STOP'|{nextLane: string, reEntryPhase: string}}
36
+ * - `null` — gate passed, no escalation needed.
37
+ * - `'STOP'` — bounded out (escalationCount >= 2) or already at the top lane ('complex')
38
+ * and the gate failed; caller performs human handoff.
39
+ * - `{nextLane, reEntryPhase}` — escalate one rung up the ladder and re-enter at the
40
+ * earliest phase the new lane requires.
41
+ */
42
+ export function escalate(normalizedGate, currentLane, escalationCount, currentPhase) {
43
+ // currentPhase is intentionally unused: the design instructs "if unsure, ignore it and
44
+ // document that" (docs/features/COMP-TRIAGE-5/plan.md S03). Accepted as a parameter so
45
+ // the caller's signature is stable for S04, but the reEntryPhase returned here is always
46
+ // the ladder default for the target lane, not narrowed by currentPhase.
47
+ void currentPhase;
48
+
49
+ if (normalizedGate.passed === true) {
50
+ return null;
51
+ }
52
+
53
+ if (escalationCount >= MAX_ESCALATIONS) {
54
+ return 'STOP';
55
+ }
56
+
57
+ const currentIndex = LANE_ORDER.indexOf(currentLane);
58
+ const nextIndex = currentIndex + 1;
59
+
60
+ if (currentIndex === -1 || nextIndex >= LANE_ORDER.length) {
61
+ // Unknown lane or already at the top rung ('complex') — cannot escalate further.
62
+ return 'STOP';
63
+ }
64
+
65
+ const nextLane = LANE_ORDER[nextIndex];
66
+ const reEntryPhase = RE_ENTRY_PHASE_BY_LANE[nextLane];
67
+
68
+ return { nextLane, reEntryPhase };
69
+ }
@@ -572,14 +572,16 @@ function runArtifactLinkChecks(fctx, ctx, findings) {
572
572
  }
573
573
  // PLANNED / SUPERSEDED / KILLED / PARKED — no finding (expected baseline)
574
574
  }
575
- // MISSING_COMPLETION_REPORT (warning, COMPLETE)
575
+ // MISSING_COMPLETION_REPORT (info, COMPLETE)
576
+ // The report phase is OPTIONAL/skippable (small features legitimately never
577
+ // write one), so a missing report.md is informational, not warning-level debt.
576
578
  // COMP-BUILD-QUICK-1: the quick lifecycle (compose build --quick) omits the
577
579
  // report phase by design and stamps built_via:'build-quick' on feature.json at
578
- // ship. Exempt those features a missing report.md is expected, not debt.
580
+ // ship those stay fully exempt (no finding at all).
579
581
  if (status === 'COMPLETE' && !folder.files.has('report.md') &&
580
582
  featureJson?.built_via !== 'build-quick') {
581
- findings.push(finding('warning', 'MISSING_COMPLETION_REPORT', code,
582
- `Feature is COMPLETE but report.md is missing (current writers do not enforce this; warning only)`));
583
+ findings.push(finding('info', 'MISSING_COMPLETION_REPORT', code,
584
+ `Feature is COMPLETE without report.md — the report phase is optional (skippable for small features); informational only`));
583
585
  }
584
586
 
585
587
  // DANGLING_ARTIFACT_LINK + ARTIFACT_OUTSIDE_FEATURE_FOLDER
@@ -63,6 +63,52 @@ const COMPLEXITIES = new Set(['S', 'M', 'L', 'XL']);
63
63
  // produced feature.json. Same vocab the ideabox uses (low|medium|high).
64
64
  const IMPACTS = new Set(['low', 'medium', 'high']);
65
65
 
66
+ // COMP-TRIAGE-5: front-of-pipeline scope-estimation vocab. `lane` drives which
67
+ // phases get skipped; `estimateSource` records whether the lane came from the
68
+ // front estimator, the doc-reading refinement, or an escalation bump.
69
+ const LANES = new Set(['trivial', 'standard', 'complex']);
70
+ const ESTIMATE_SOURCES = new Set(['front', 'refined', 'escalated']);
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // validateFeatureFields — shared field-shape guard
74
+ // ---------------------------------------------------------------------------
75
+
76
+ /**
77
+ * Validate the subset of feature.json fields that carry a fixed vocabulary or
78
+ * range. Called from both addRoadmapEntry (below) and the build write path
79
+ * (lib/build.js) so neither can bypass the guard the way the old inline
80
+ * `COMPLEXITIES.has` check — scoped only to addRoadmapEntry — allowed
81
+ * `complexity: String(triageResult.tier)` (e.g. "0".."4") to reach disk via a
82
+ * raw provider write.
83
+ *
84
+ * Every field is optional; only fields present on `fields` are checked.
85
+ * Throws a plain Error (matching addRoadmapEntry's existing signaling) on the
86
+ * first violation found.
87
+ *
88
+ * @param {object} fields
89
+ * @param {string} [fields.complexity] one of S|M|L|XL
90
+ * @param {number} [fields.triageTier] integer 0-4
91
+ * @param {string} [fields.lane] one of trivial|standard|complex
92
+ * @param {string} [fields.estimateSource] one of front|refined|escalated
93
+ */
94
+ export function validateFeatureFields(fields) {
95
+ if (fields.complexity !== undefined && !COMPLEXITIES.has(fields.complexity)) {
96
+ throw new Error(`feature-writer: invalid complexity "${fields.complexity}"`);
97
+ }
98
+ if (fields.triageTier !== undefined &&
99
+ (!Number.isInteger(fields.triageTier) || fields.triageTier < 0 || fields.triageTier > 4)) {
100
+ throw new Error(`feature-writer: invalid triageTier "${fields.triageTier}" — must be an integer 0-4`);
101
+ }
102
+ if (fields.lane !== undefined && !LANES.has(fields.lane)) {
103
+ throw new Error(`feature-writer: invalid lane "${fields.lane}" — must be one of ${[...LANES].join(', ')}`);
104
+ }
105
+ if (fields.estimateSource !== undefined && !ESTIMATE_SOURCES.has(fields.estimateSource)) {
106
+ throw new Error(
107
+ `feature-writer: invalid estimateSource "${fields.estimateSource}" — must be one of ${[...ESTIMATE_SOURCES].join(', ')}`
108
+ );
109
+ }
110
+ }
111
+
66
112
  // ---------------------------------------------------------------------------
67
113
  // Helpers
68
114
  // ---------------------------------------------------------------------------
@@ -115,6 +161,9 @@ function maybeIdempotent(args, fn) {
115
161
  * @param {string} [args.triageTimestamp] COMP-ROADMAP-PLAN: triage cache stamp
116
162
  * @param {string} [args.plannedBy] COMP-ROADMAP-PLAN: originating plan session
117
163
  * @param {string} [args.impact] COMP-ROADMAP-PLAN: low | medium | high
164
+ * @param {string} [args.lane] COMP-TRIAGE-5: trivial | standard | complex
165
+ * @param {number} [args.triageTier] COMP-TRIAGE-5: integer 0-4
166
+ * @param {string} [args.estimateSource] COMP-TRIAGE-5: front | refined | escalated
118
167
  * @param {boolean} [args.force]
119
168
  * @param {string} [args.idempotency_key]
120
169
  */
@@ -127,9 +176,11 @@ export async function addRoadmapEntry(cwd, args) {
127
176
  validateCode(args.code);
128
177
  if (!args.description) throw new Error('feature-writer: description is required');
129
178
  if (!args.phase) throw new Error('feature-writer: phase is required');
130
- if (args.complexity && !COMPLEXITIES.has(args.complexity)) {
131
- throw new Error(`feature-writer: invalid complexity "${args.complexity}"`);
132
- }
179
+ // COMP-TRIAGE-5: complexity/triageTier/lane/estimateSource shape validation
180
+ // lives in the shared validateFeatureFields guard so the build write path
181
+ // (lib/build.js) can call the same check and can't bypass it the way the
182
+ // old inline `COMPLEXITIES.has` (scoped only to this function) allowed.
183
+ validateFeatureFields(args);
133
184
  const status = args.status ?? 'PLANNED';
134
185
  if (!STATUSES.has(status)) {
135
186
  throw new Error(`feature-writer: invalid status "${status}"`);
@@ -180,6 +231,10 @@ export async function addRoadmapEntry(cwd, args) {
180
231
  if (args.triageTimestamp !== undefined) feature.triageTimestamp = args.triageTimestamp;
181
232
  if (args.plannedBy !== undefined) feature.plannedBy = args.plannedBy;
182
233
  if (args.impact !== undefined) feature.impact = args.impact;
234
+ // COMP-TRIAGE-5: front-of-pipeline scope-estimation fields.
235
+ if (args.lane !== undefined) feature.lane = args.lane;
236
+ if (args.triageTier !== undefined) feature.triageTier = args.triageTier;
237
+ if (args.estimateSource !== undefined) feature.estimateSource = args.estimateSource;
183
238
 
184
239
  let roundtrip = null;
185
240
  if (isLocalProvider(provider)) {
package/lib/flow-state.js CHANGED
@@ -1,6 +1,8 @@
1
1
  /**
2
- * flow-state.js — small read-only helpers over the persisted Stratum flow state
3
- * (`~/.stratum/flows/<flowId>.json`).
2
+ * flow-state.js — small read-only helpers over persisted Stratum flow state.
3
+ *
4
+ * Reads the TS store (`STRATUM_STATE_ROOT/<flowId>.json` when configured,
5
+ * otherwise `~/.stratum/ts/flows/<flowId>.json`).
4
6
  *
5
7
  * Shared by the gate handlers in build.js and new.js so the gate id can be made
6
8
  * round-aware (COMP-PLAN-GATE-LOOP).
@@ -10,27 +12,26 @@ import { join } from 'node:path';
10
12
  import { homedir } from 'node:os';
11
13
 
12
14
  /**
13
- * Read Stratum's current round for a flow from its persisted state file.
15
+ * Read Stratum's current round for a flow from its persisted state files.
14
16
  *
15
- * The await_gate dispatch Stratum returns does not carry the round, but the
16
- * persisted flow file does. Threading the round into the gate id
17
+ * The TS running response does not carry the round, but the persisted flow
18
+ * file does. Threading the round into the gate id
17
19
  * (`<flowId>:<stepId>:<round>`) makes each gate re-entry after a `revise` a
18
20
  * fresh, pending gate rather than colliding with the prior resolved gate and
19
21
  * replaying its stale outcome.
20
22
  *
21
- * Fail-open: returns 1 (the legacy default) if the file is missing, unreadable,
22
- * or lacks an integer round — a read failure must never block a gate.
23
+ * Fresh TS runs have no `rounds` field and use round 0. A read failure must
24
+ * never block a gate.
23
25
  *
24
26
  * @param {string} flowId
25
27
  * @returns {number}
26
28
  */
27
29
  export function readFlowRound(flowId) {
30
+ // TS store — STRATUM_STATE_ROOT-aware; flows are stored flat at the root.
28
31
  try {
29
- const flowFile = join(homedir(), '.stratum', 'flows', `${flowId}.json`);
30
- const state = JSON.parse(readFileSync(flowFile, 'utf-8'));
31
- const r = state?.round;
32
- return Number.isInteger(r) && r >= 0 ? r : 1;
33
- } catch {
34
- return 1;
35
- }
32
+ const tsRoot = process.env.STRATUM_STATE_ROOT || join(homedir(), '.stratum', 'ts', 'flows');
33
+ const state = JSON.parse(readFileSync(join(tsRoot, `${flowId}.json`), 'utf-8'));
34
+ const r = state?.rounds;
35
+ return Number.isInteger(r) && r >= 0 ? r : 0;
36
+ } catch { return 0; }
36
37
  }
package/lib/gsd-budget.js CHANGED
@@ -62,8 +62,8 @@ export function buildBudgetBlock(cfg = {}) {
62
62
  if (Object.keys(budget).length > 0) out.budget = budget;
63
63
 
64
64
  if (cfg.per_task_ms != null) {
65
- // stratum parallel_dispatch per-task timeout is `task_timeout` in SECONDS
66
- // (spec.py:145, schema minimum 1). Convert from ms, floor at 1s.
65
+ // Compose's per-consumer timeout is carried in seconds. Convert from ms and
66
+ // floor at 1s.
67
67
  out.taskTimeoutSec = Math.max(1, Math.ceil(cfg.per_task_ms / 1000));
68
68
  }
69
69
 
@@ -91,8 +91,13 @@ export function buildBudgetBlock(cfg = {}) {
91
91
  */
92
92
  export function injectBudget(specYaml, cfg = {}) {
93
93
  const built = buildBudgetBlock(cfg);
94
- if (!built.budget && built.taskTimeoutSec == null) {
95
- return specYaml; // identitynothing to inject
94
+ // D2(a): per_task_ms is a PER-ITEM wall-clock ceiling enforced compose-side
95
+ // (runConsumerIssuance runs each agent), NOT an engine step budget a step
96
+ // budget on the fanout cannot be per-item (the engine can't know N). So only
97
+ // the honest flow-wide budget is injected here; per_task_ms is threaded to the
98
+ // consumer loop via gsd.budget.per_task_ms.
99
+ if (!built.budget) {
100
+ return specYaml; // identity — nothing to inject into the spec
96
101
  }
97
102
 
98
103
  const parsed = YAML.parse(specYaml);
@@ -102,16 +107,49 @@ export function injectBudget(specYaml, cfg = {}) {
102
107
  throw new Error('injectBudget: spec has no flows.gsd to attach a budget to');
103
108
  }
104
109
 
105
- if (built.budget) flow.budget = built.budget;
106
-
107
- if (built.taskTimeoutSec != null && Array.isArray(flow.steps)) {
108
- const execute = flow.steps.find((s) => s && s.id === 'execute');
109
- if (execute) execute.task_timeout = built.taskTimeoutSec;
110
- }
110
+ flow.budget = parsed.version === 1
111
+ ? {
112
+ ...(built.budget.max_tokens != null ? { tokens: built.budget.max_tokens } : {}),
113
+ ...(built.budget.max_agent_dispatches != null ? { dispatches: built.budget.max_agent_dispatches } : {}),
114
+ ...(built.budget.usd != null ? { usd: built.budget.usd } : {}),
115
+ ...(built.budget.ms != null ? { ms: built.budget.ms } : {}),
116
+ }
117
+ : built.budget;
111
118
 
112
119
  return YAML.stringify(parsed);
113
120
  }
114
121
 
122
+ /**
123
+ * D2(c): adapt the TS engine's terminal `ledger` ({spent, budget}) into the
124
+ * legacy `budget_state` ({caps, consumed}) shape the GSD diagnostics/recording
125
+ * helpers consume. The python terminal envelope carried `budget_state`
126
+ * directly; the TS engine returns a `ledger` with the Budget key names
127
+ * (usd/tokens/dispatches/ms). Returns null for a missing/empty ledger so
128
+ * callers can `response.budget_state ?? budgetStateFromLedger(response.ledger)`.
129
+ *
130
+ * @param {{ spent?: object, budget?: object, limits?: object }} ledger
131
+ * @returns {{ caps: object, consumed: object } | null}
132
+ */
133
+ export function budgetStateFromLedger(ledger) {
134
+ if (!ledger || typeof ledger !== 'object') return null;
135
+ const spent = ledger.spent ?? {};
136
+ const caps = ledger.budget ?? ledger.limits ?? {};
137
+ return {
138
+ caps: {
139
+ ...(caps.tokens != null ? { max_tokens: caps.tokens } : {}),
140
+ ...(caps.dispatches != null ? { max_agent_dispatches: caps.dispatches } : {}),
141
+ ...(caps.usd != null ? { usd: caps.usd } : {}),
142
+ ...(caps.ms != null ? { ms: caps.ms } : {}),
143
+ },
144
+ consumed: {
145
+ tokens: spent.tokens ?? 0,
146
+ dispatches: spent.dispatches ?? 0,
147
+ dollars: spent.usd ?? 0,
148
+ wall_s: (spent.ms ?? 0) / 1000,
149
+ },
150
+ };
151
+ }
152
+
115
153
  // Maps a stratum budget axis → human label for diagnostics.
116
154
  const AXIS_LABEL = {
117
155
  max_tokens: 'tokens',
package/lib/gsd-prompt.js CHANGED
@@ -2,10 +2,9 @@
2
2
  //
3
3
  // COMP-GSD-2 T4: buildTaskDescription — pure string assembly.
4
4
  //
5
- // Stratum's parallel_dispatch only interpolates {task.id|description|
6
- // files_owned|files_read|depends_on|index} and {input.<field>}. Every other
7
- // piece of context — the per-slice produces/consumes contract, the upstream
8
- // tasks summary, the gate commands — must be packed inside `task.description`.
5
+ // TS consumer fanout renders each item, but richer per-slice context — the
6
+ // produces/consumes contract, upstream summary, and gate commands — is packed
7
+ // inside `task.description`.
9
8
  //
10
9
  // This module produces the rich `description` string. It is also used by
11
10
  // runGsd as a deterministic fallback when the decompose_gsd agent failed to
package/lib/gsd-stuck.js CHANGED
@@ -15,7 +15,7 @@
15
15
  * no-progress are the only new bookkeeping.
16
16
  *
17
17
  * Consumes BuildStreamEvents from stratum.onEvent inside
18
- * executeParallelDispatchServer, keyed by event.task_id. gsd runs the execute
18
+ * the TS consumer runner, keyed by event.task_id. GSD runs the execute
19
19
  * step max_concurrent:1, so per-task state is unambiguous. Telemetry contract
20
20
  * (schema 0.2.7, STRAT-PAR-STREAM-TOOLDETAIL):
21
21
  * tool_use_summary.metadata = { tool, summary, ok, duration_ms, input, tool_use_id }