@smartmemory/compose 0.2.57-beta → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/compose.js +55 -54
- package/contracts/feature-json.schema.json +16 -0
- package/dist/assets/{App-BmhlHOXF.js → App-DJ5xk_Wx.js} +219 -219
- package/dist/assets/{abnfDiagram-VRR7QNED-CMOTYAnt.js → abnfDiagram-VRR7QNED-BPWGdCFx.js} +1 -1
- package/dist/assets/{arc-BJAv_6dL.js → arc-DX5jqmcO.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-xa4DLUcp.js → architectureDiagram-ZJ3FMSHR-B6jmNVw7.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-BdkDqeQF.js → blockDiagram-677ZJIJ3-DAx2i-sB.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-DiNrLIuL.js → c4Diagram-LMCZKHZV-zmsZCplj.js} +1 -1
- package/dist/assets/channel-vsDnTvkh.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BsjtgrSL.js → chunk-2Q5K7J3B-3QSh6sI7.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-DpImULWy.js → chunk-32BRIVSS-BiH5Di_y.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-DTdG5JnR.js → chunk-5VM5RSS4-C0EfjFLd.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-Cj1jNuKY.js → chunk-EX3LRPZG-BGyfmVm-.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DwQ0wwWn.js → chunk-JWPE2WC7-Ba0mrNlg.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-DZZa7unc.js → chunk-MOJQB5TN-D3QO9EzH.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-CgdjqYQj.js → chunk-RYQCIY6F-D1uvNA6d.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DrPIj6XI.js → chunk-V7JOEXUC-fihlnodT.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-BBUOVC5W.js → chunk-VR4S4FIN-nmRUEo1H.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BEIiZ6gS.js → chunk-XXDRQBXY-CM693yEg.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-DO-wdSFZ.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-DO-wdSFZ.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-ClFTQEyD.js → cose-bilkent-JH36ORCC-CkacPn8W.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-5Bp7xFO0.js → cynefin-VYW2F7L2-BiHaIptG.js} +1 -1
- package/dist/assets/{cynefinDiagram-TSTJHNR4-C-0QlSda.js → cynefinDiagram-TSTJHNR4-CdKxMmiy.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-DLkqsfBy.js → dagre-VKFMJZFB-BQLY5y_W.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-Cwh_tJ_r.js → diagram-FQU43EPY-D7uMBHvq.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-CvvAjulf.js → diagram-G47NLZAW-B3Z1cuH7.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-CCoyx8_e.js → diagram-NH7WQ7WH-BZyRD45e.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-DfjhKYCp.js → diagram-OA4YK3LP-DcThOTt7.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-Vjz0ygfB.js → diagram-WEI45ONY-BcgRAkqY.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-GsA8c6g2.js → ebnfDiagram-CCIWWBDH-CGwfO_xH.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-Cc0lJ9sQ.js → erDiagram-Q63AITRT-pV58-Ncc.js} +1 -1
- package/dist/assets/{flowDiagram-23GEKE2U-C_sotK-k.js → flowDiagram-23GEKE2U-DJ_SqE8h.js} +1 -1
- package/dist/assets/{ganttDiagram-NO4QXBWP-BBtu_WUe.js → ganttDiagram-NO4QXBWP-Dgy0Iyss.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-BV_aSB8-.js → gitGraphDiagram-IHSO6WYX-CbiZw9fb.js} +1 -1
- package/dist/assets/{index-CKfOVv2N.js → index-DZTJEk-y.js} +2 -2
- package/dist/assets/{infoDiagram-FWYZ7A6U-DMAvNGtX.js → infoDiagram-FWYZ7A6U-CtsyyEc-.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-DfQB91MZ.js → ishikawaDiagram-FXEZZL3T-BJilNFkK.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bq4Llm9o.js → journeyDiagram-5HDEW3XC-C2UCMP4t.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-DTXHiDiE.js → kanban-definition-HUTT4EX6-DmLDJBRy.js} +1 -1
- package/dist/assets/{linear-BQSYZcVg.js → linear-C1paCqE7.js} +1 -1
- package/dist/assets/{mindmap-definition-LN4V7U3C-KuH2NIj0.js → mindmap-definition-LN4V7U3C-CX-RxKVn.js} +1 -1
- package/dist/assets/{pegDiagram-2B236MQR-BKoNGQFr.js → pegDiagram-2B236MQR-CTU32H2W.js} +1 -1
- package/dist/assets/{pieDiagram-ENE6RG2P-EpYv162Y.js → pieDiagram-ENE6RG2P-D8L1aYoo.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-C9deUjQ2.js → quadrantDiagram-ABIIQ3AL-hQ3bXosy.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-Csqw2hK9.js → railroadDiagram-RFXS5EU6--_vuYcda.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-DCuz3emr.js → requirementDiagram-TGXJPOKE-C6h_m3Az.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-BTeTL24f.js → sankeyDiagram-HTMAVEWB-Dpc7CfIQ.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-Cf1ZkTwO.js → sequenceDiagram-DBY2YBRQ-CK5ZzbvJ.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-D3Lbewzg.js → sizeCapture-X5ZJPWSS-BAxVvM9G.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-O8l37yJI.js → stateDiagram-2N3HPSRC-7j33_2PY.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-GviN0FpJ.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-BikmTh8b.js → swimlanes-5IMT3BWC-CBG2yOob.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-D0wMoxl1.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-BOxSosG0.js → timeline-definition-FHXFAJF6-BOnZkQvc.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-BSClOErZ.js → vennDiagram-L72KCM5P-D52NhIfj.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-DHFRjOK0.js → wardleyDiagram-EHGQE667-YoPY2ZU-.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-Dh1v4x-O.js → xychartDiagram-FW5EYKEG-BanNfgtO.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/build-all.js +0 -5
- package/lib/build-stream-schema.js +1 -1
- package/lib/build.js +1809 -2646
- package/lib/consumer-fanout.js +1317 -0
- package/lib/escalation.js +69 -0
- package/lib/feature-validator.js +6 -4
- package/lib/feature-writer.js +58 -3
- package/lib/flow-state.js +15 -14
- package/lib/gsd-budget.js +48 -10
- package/lib/gsd-prompt.js +3 -4
- package/lib/gsd-stuck.js +1 -1
- package/lib/gsd.js +303 -149
- package/lib/lane-gate.js +200 -0
- package/lib/local-claude-connector.js +149 -0
- package/lib/new.js +162 -307
- package/lib/result-normalizer.js +233 -18
- package/lib/review-lenses.js +1 -1
- package/lib/step-prompt.js +41 -119
- package/lib/stratum-engine.js +297 -0
- package/lib/stratum-mcp-client.js +224 -213
- package/lib/triage.js +144 -0
- package/lib/vocabulary-compliance.js +268 -0
- package/lib/vocabulary-inject.js +1 -36
- package/package.json +2 -1
- package/pipelines/build-quick.stratum.yaml +5 -17
- package/pipelines/build.profiles.json +11 -0
- package/pipelines/build.stratum.yaml +288 -467
- package/pipelines/gsd.stratum.yaml +73 -125
- package/pipelines/new.stratum.yaml +68 -149
- package/server/build-routes.js +4 -1
- package/server/design-routes.js +37 -21
- package/server/index.js +13 -21
- package/server/lifecycle-guard.js +1 -1
- package/server/pipeline-routes.js +113 -31
- package/server/stratum-client.js +33 -47
- package/server/stratum-sync.js +3 -4
- package/server/vision-server.js +1 -1
- package/dist/assets/channel-CYErfopw.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-qCC_hnXu.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-qCC_hnXu.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-BWqk_9py.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-C60ICdks.js +0 -8
- package/lib/connector-factory-shim.js +0 -167
- package/server/agent-mcp.js +0 -10
package/lib/gsd.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Self-contained status loop. Does NOT modify lib/build.js. Reuses primitives:
|
|
6
6
|
// - StratumMcpClient (lib/stratum-mcp-client.js) for plan/stepDone/runAgentText
|
|
7
|
-
// -
|
|
7
|
+
// - runConsumerIssuance (lib/build.js) for TS consumer fanout work
|
|
8
8
|
// - validateBoundaryMap (lib/boundary-map.js) for precondition check
|
|
9
9
|
// - enrichTaskGraph (lib/gsd-decompose-enrich.js) for decompose validation
|
|
10
10
|
// - buildTaskDescription (lib/gsd-prompt.js) for description repair fallback
|
|
@@ -17,15 +17,23 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, rmSync
|
|
|
17
17
|
import { join, resolve, dirname } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { execSync } from 'node:child_process';
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
import YAML from 'yaml';
|
|
20
22
|
|
|
21
23
|
import { StratumMcpClient } from './stratum-mcp-client.js';
|
|
24
|
+
import { resolveStratumMcpConnection } from './stratum-engine.js';
|
|
22
25
|
import { validateBoundaryMap } from './boundary-map.js';
|
|
23
26
|
import { enrichTaskGraph } from './gsd-decompose-enrich.js';
|
|
24
27
|
import { buildTaskDescription } from './gsd-prompt.js';
|
|
25
28
|
import { writeAll, validate as validateTaskResult, read as readBlackboard } from './gsd-blackboard.js';
|
|
26
|
-
import {
|
|
29
|
+
import { executeShipStep, runConsumerIssuance, ConsumerStuckError, filesOwnedConflict, toEngineUsage } from './build.js';
|
|
30
|
+
import {
|
|
31
|
+
ConsumerFanoutArtifacts,
|
|
32
|
+
ConsumerMergeDecisionError,
|
|
33
|
+
isConsumerDescriptor,
|
|
34
|
+
} from './consumer-fanout.js';
|
|
27
35
|
import { GsdStuckDetector, DEFAULT_THRESHOLDS } from './gsd-stuck.js';
|
|
28
|
-
import { readGsdBudgetConfig, buildBudgetBlock, injectBudget, composeBudgetDiagnostic } from './gsd-budget.js';
|
|
36
|
+
import { readGsdBudgetConfig, buildBudgetBlock, injectBudget, composeBudgetDiagnostic, budgetStateFromLedger } from './gsd-budget.js';
|
|
29
37
|
import { recordGsdUsage, checkGsdCumulativeBudget } from './budget-ledger.js';
|
|
30
38
|
// COMP-GSD-6: continuous run-state checkpoint + canonical pid-liveness probe.
|
|
31
39
|
// pidAlive is canonical in gsd-state.js (EPERM=alive) and imported one-way here.
|
|
@@ -45,6 +53,10 @@ const DEFAULT_GATE_COMMANDS = ['pnpm lint', 'pnpm build', 'pnpm test'];
|
|
|
45
53
|
// (execute.pre_merge_verify) and the instructed gate (task descriptions).
|
|
46
54
|
const DEFAULT_FAST_GATE = ['pnpm lint', 'pnpm build'];
|
|
47
55
|
|
|
56
|
+
function sha256(value) {
|
|
57
|
+
return createHash('sha256').update(String(value)).digest('hex');
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
// ---------- Public API ----------
|
|
49
61
|
|
|
50
62
|
export async function runGsd(featureCode, opts = {}) {
|
|
@@ -138,6 +150,8 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
138
150
|
// run (and plain `compose build`) is byte-identical.
|
|
139
151
|
const budgetCfg = readGsdBudgetConfig(cwd);
|
|
140
152
|
const specYaml = injectBudget(readFileSync(specPath, 'utf-8'), budgetCfg);
|
|
153
|
+
const localSpec = YAML.parse(specYaml);
|
|
154
|
+
const localSpecDigest = sha256(JSON.stringify(localSpec));
|
|
141
155
|
|
|
142
156
|
// 4a. COMP-GSD-4: cumulative cross-session ceiling pre-check (tokens/cost).
|
|
143
157
|
// Refuse to start/resume a run that has already spent its lifetime budget —
|
|
@@ -154,13 +168,13 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
154
168
|
|
|
155
169
|
// 4b. COMP-GSD-5 stuck detector — thresholds from .compose/compose.json
|
|
156
170
|
// `gsd.stuck.*` with documented defaults. ONLY gsd passes this into the
|
|
157
|
-
//
|
|
171
|
+
// TS consumer runner, so build mode remains unaffected.
|
|
158
172
|
const stuckDetector = buildStuckDetector(cwd);
|
|
159
173
|
|
|
160
174
|
// 5. Connect Stratum + plan (only after preconditions pass)
|
|
161
175
|
const stratum = opts.stratum ?? new StratumMcpClient();
|
|
162
176
|
const ownsStratum = !opts.stratum;
|
|
163
|
-
if (ownsStratum) await stratum.connect();
|
|
177
|
+
if (ownsStratum) await stratum.connect(resolveStratumMcpConnection(cwd));
|
|
164
178
|
// COMP-GSD-4: ownership flag — release the resume lock in finally ONLY if THIS
|
|
165
179
|
// process successfully claimed it (set below). Prevents (a) a non-resume run
|
|
166
180
|
// from clobbering a concurrent resume's valid claim and (b) a claim-race loser
|
|
@@ -217,8 +231,12 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
217
231
|
// docs unless context.filesChanged is provided.
|
|
218
232
|
stepCtx = {
|
|
219
233
|
stratum, cwd, featureCode, blueprintText, gateCommands, preMergeGate,
|
|
234
|
+
localSpec, localSpecDigest,
|
|
220
235
|
filesChanged: [],
|
|
221
236
|
stuckDetector,
|
|
237
|
+
// D2(a): per-ITEM wall-clock ceiling from gsd.budget.per_task_ms, enforced
|
|
238
|
+
// compose-side in runConsumerIssuance (the engine can't bound N items).
|
|
239
|
+
perItemTimeoutMs: budgetCfg.per_task_ms ?? null,
|
|
222
240
|
resumeTaskGraph,
|
|
223
241
|
stuck: null, // set by runOneStep on a stuck verdict
|
|
224
242
|
runState: initialState, // COMP-GSD-6: flushState merges into this
|
|
@@ -269,43 +287,22 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
269
287
|
featureCode,
|
|
270
288
|
gateCommands,
|
|
271
289
|
pre_merge_gate: preMergeGate,
|
|
272
|
-
});
|
|
273
|
-
const flowId = response.
|
|
290
|
+
}, { workspaceRoot: cwd });
|
|
291
|
+
const flowId = response.runId;
|
|
274
292
|
flushState(stepCtx, { flowId, phase: 'decompose' });
|
|
275
293
|
emitPhaseOnce(stepCtx, 'decompose'); // COMP-GSD-7-EVENTLOG
|
|
276
294
|
|
|
277
|
-
// 5.
|
|
278
|
-
//
|
|
279
|
-
// is the stratum flow-budget terminal, surfaced verbatim through the advance/
|
|
280
|
-
// poll envelopes (and carries budget_state).
|
|
295
|
+
// 5. TS status loop. `stuck` is a Compose-side terminal and
|
|
296
|
+
// `budget_exhausted` is the Stratum flow-budget terminal.
|
|
281
297
|
while (
|
|
282
|
-
response.status !== '
|
|
283
|
-
response.status !== '
|
|
298
|
+
response.status !== 'completed' &&
|
|
299
|
+
response.status !== 'failed' &&
|
|
284
300
|
response.status !== 'stuck' &&
|
|
285
|
-
response.status !== 'budget_exhausted'
|
|
286
|
-
response.status !== 'error' // COMP-PAR-MERGE-QUEUE: terminal step failure (e.g. retries_exhausted)
|
|
301
|
+
response.status !== 'budget_exhausted'
|
|
287
302
|
) {
|
|
288
303
|
response = await runOneStep(response, stepCtx);
|
|
289
304
|
}
|
|
290
305
|
|
|
291
|
-
// COMP-PAR-MERGE-QUEUE: a step that exhausted its retries (e.g. the execute
|
|
292
|
-
// step after repeated pre-merge gate failures) surfaces as a terminal `error`
|
|
293
|
-
// envelope rather than silently advancing to ship. Stop here with the failure
|
|
294
|
-
// and its bounce context instead of throwing `unknown response status`.
|
|
295
|
-
if (response.status === 'error') {
|
|
296
|
-
emitCompletionDeltas(stepCtx);
|
|
297
|
-
flushState(stepCtx, { status: 'failed' });
|
|
298
|
-
return {
|
|
299
|
-
status: 'failed',
|
|
300
|
-
flowId,
|
|
301
|
-
stepId: response.step_id ?? stepCtx.lastStepId ?? null,
|
|
302
|
-
errorType: response.error_type ?? 'step_failed',
|
|
303
|
-
message: response.message ?? 'GSD step failed',
|
|
304
|
-
violations: response.violations ?? [],
|
|
305
|
-
bouncedTasks: response.bounced_tasks ?? [],
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
306
|
if (response.status === 'stuck') {
|
|
310
307
|
// Artifacts (stuck.md/json + pause.json) were written by runOneStep.
|
|
311
308
|
// COMP-GSD-7-EVENTLOG: flush any completions that finished before the stuck
|
|
@@ -327,9 +324,10 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
327
324
|
// cascade-cancelled in-flight siblings. Persist budget.{md,json} +
|
|
328
325
|
// pause.json (kind:budget) for --resume, record cumulative usage, and
|
|
329
326
|
// return a terminal `budget` envelope. pause.lock is released by finally.
|
|
330
|
-
|
|
327
|
+
// D2(c): derive Compose's budget diagnostic from the TS ledger.
|
|
328
|
+
const budgetState = budgetStateFromLedger(response.ledger) ?? {};
|
|
331
329
|
writeBudgetArtifacts(stepCtx, response, budgetState);
|
|
332
|
-
recordGsdUsageFromState(cwd, featureCode, budgetState);
|
|
330
|
+
recordGsdUsageFromState(cwd, featureCode, budgetState, stepCtx.recordedUsage);
|
|
333
331
|
const axis = composeBudgetDiagnostic(budgetState, { feature: featureCode }).json.axis;
|
|
334
332
|
// COMP-GSD-7-EVENTLOG: flush pre-halt completions, then record the pause.
|
|
335
333
|
emitCompletionDeltas(stepCtx);
|
|
@@ -347,20 +345,22 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
347
345
|
|
|
348
346
|
// 6b. COMP-GSD-5: a clean (non-stuck) finish clears any pause.json — the
|
|
349
347
|
// resume completed, or a fresh run superseded a stale pause.
|
|
350
|
-
if (response.status === 'complete') {
|
|
348
|
+
if (response.status === 'complete' || response.status === 'completed') {
|
|
351
349
|
// COMP-GSD-4: record this run's cumulative usage (best-effort; no-op when
|
|
352
|
-
// the
|
|
353
|
-
|
|
350
|
+
// the terminal envelope carries no budget accounting, e.g. un-budgeted
|
|
351
|
+
// runs). D2(c): the TS engine returns `ledger`, not `budget_state`.
|
|
352
|
+
const terminalBudgetState = response.budget_state ?? budgetStateFromLedger(response.ledger);
|
|
353
|
+
recordGsdUsageFromState(cwd, featureCode, terminalBudgetState, stepCtx.recordedUsage);
|
|
354
354
|
clearPauseFile(cwd, featureCode);
|
|
355
355
|
// COMP-GSD-7: on a clean complete, budget.json is NOT written (only halts
|
|
356
356
|
// write it). Persist a budget-final.json snapshot so the milestone report
|
|
357
357
|
// (auto + retroactive `gsd report`) has actuals-vs-caps. No-op when the
|
|
358
|
-
// envelope carries no
|
|
359
|
-
// a derived report input — a write failure must NEVER demote a
|
|
360
|
-
// run to 'failed' via the outer catch.
|
|
361
|
-
if (
|
|
358
|
+
// envelope carries no budget accounting (un-budgeted run). Best-effort:
|
|
359
|
+
// this is a derived report input — a write failure must NEVER demote a
|
|
360
|
+
// successful run to 'failed' via the outer catch.
|
|
361
|
+
if (terminalBudgetState) {
|
|
362
362
|
try {
|
|
363
|
-
writeBudgetFinalSnapshot(stepCtx,
|
|
363
|
+
writeBudgetFinalSnapshot(stepCtx, terminalBudgetState);
|
|
364
364
|
} catch (err) {
|
|
365
365
|
console.warn(`[gsd] budget-final snapshot failed: ${err.message}`);
|
|
366
366
|
}
|
|
@@ -371,7 +371,7 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
371
371
|
// other terminal here (e.g. stratum 'killed') maps to 'failed' so we stay
|
|
372
372
|
// within the closed status vocabulary the contract + supervisor share.
|
|
373
373
|
// COMP-GSD-7: stamp completedAt so retroactive reports can recover wall-clock.
|
|
374
|
-
const terminalStatus = response.status
|
|
374
|
+
const terminalStatus = ['complete', 'completed'].includes(response.status) ? 'complete' : 'failed';
|
|
375
375
|
// COMP-GSD-7-EVENTLOG: emit the terminal event. complete → final completion
|
|
376
376
|
// deltas + 'completed'; any other terminal (e.g. stratum 'killed') → 'failed'.
|
|
377
377
|
if (terminalStatus === 'complete') {
|
|
@@ -475,61 +475,78 @@ export function resolvePreMergeGate(cwd, override) {
|
|
|
475
475
|
}
|
|
476
476
|
|
|
477
477
|
async function runOneStep(response, ctx) {
|
|
478
|
-
const { stratum, cwd, featureCode,
|
|
479
|
-
const flowId = response.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
cwd,
|
|
494
|
-
{
|
|
495
|
-
stuckDetector: ctx.stuckDetector, // COMP-GSD-5 (null in non-gsd callers)
|
|
496
|
-
// COMP-GSD-6: bump state.json's heartbeat on every task event so a long
|
|
497
|
-
// task sitting in the dispatch poll loop isn't mistaken for crashed.
|
|
498
|
-
onHeartbeat: ctx.runState ? () => { try { flushState(ctx, {}); } catch { /* best-effort */ } } : null,
|
|
499
|
-
},
|
|
500
|
-
);
|
|
501
|
-
|
|
502
|
-
// COMP-GSD-5: a stuck verdict halts the run. Persist the diagnostic +
|
|
503
|
-
// resume state, then return a terminal `stuck` envelope so runGsd's loop
|
|
504
|
-
// exits. The task was already cancelled (conflict) inside dispatch.
|
|
505
|
-
if (outcome && outcome.stuck) {
|
|
506
|
-
ctx.stuck = outcome.stuck;
|
|
507
|
-
writeStuckArtifacts(ctx, response, outcome.stuck);
|
|
508
|
-
return { status: 'stuck', flow_id: flowId, step_id: stepId };
|
|
478
|
+
const { stratum, cwd, featureCode, localSpec, localSpecDigest } = ctx;
|
|
479
|
+
const flowId = response.runId;
|
|
480
|
+
|
|
481
|
+
if (response.status === 'ready') {
|
|
482
|
+
const ready = response.ready ?? [];
|
|
483
|
+
if (ready.length === 0) throw new Error('runGsd: TS engine returned ready without a ready entry');
|
|
484
|
+
const descriptor = ready.find(isConsumerDescriptor);
|
|
485
|
+
if (descriptor) {
|
|
486
|
+
if (!ctx.consumerArtifacts) {
|
|
487
|
+
ctx.consumerArtifacts = new ConsumerFanoutArtifacts({
|
|
488
|
+
runId: flowId,
|
|
489
|
+
targetCwd: cwd,
|
|
490
|
+
revisionDigest: descriptor.revisionDigest,
|
|
491
|
+
specDigest: localSpecDigest,
|
|
492
|
+
});
|
|
509
493
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
494
|
+
ctx.consumerArtifacts.bindRunRevision({
|
|
495
|
+
revisionDigest: descriptor.revisionDigest,
|
|
496
|
+
specDigest: localSpecDigest,
|
|
497
|
+
});
|
|
498
|
+
// D7(a): render the exact TaskResult path from the item's task id (the
|
|
499
|
+
// fanout `over` is decompose_gsd.output.tasks, indexed by itemIndex).
|
|
500
|
+
const consumerItem = ctx.lastTaskGraph?.tasks?.[descriptor.itemIndex];
|
|
501
|
+
const taskResultPath = consumerItem?.id
|
|
502
|
+
? gsdTaskResultPath(featureCode, consumerItem.id)
|
|
503
|
+
: undefined;
|
|
504
|
+
try {
|
|
505
|
+
return await runConsumerIssuance({
|
|
506
|
+
descriptor,
|
|
507
|
+
flowId,
|
|
508
|
+
stratum,
|
|
509
|
+
artifacts: ctx.consumerArtifacts,
|
|
510
|
+
audit: await stratum.audit(flowId),
|
|
511
|
+
localSpec,
|
|
512
|
+
// D2(b): onUsage debits each item's agent usage into the cumulative
|
|
513
|
+
// ledger. D2(a): per-item wall-clock ceiling from gsd.budget.per_task_ms.
|
|
514
|
+
// D3: the stuck detector observes this item's agent tool events.
|
|
515
|
+
// D7(a): taskResultPath spells out the exact TaskResult filename.
|
|
516
|
+
// I3: `gsd: true` is the REAL GSD marker the milestone instrumentation
|
|
517
|
+
// in runConsumerIssuance keys on — every consumer item dispatched here is
|
|
518
|
+
// by definition a GSD task, so timing.json + diffs/<id>.diff are written
|
|
519
|
+
// for the milestone report. The build consumer path omits it (no marker),
|
|
520
|
+
// so build-mode fanout stays byte-identical.
|
|
521
|
+
context: { cwd, featureCode, gsd: true, gsdTaskId: consumerItem?.id, filesChanged: ctx.filesChanged, onUsage: (usage) => recordTsAgentUsage(ctx, usage), taskResultPath },
|
|
522
|
+
// runAndNormalize narrates via progress.debug/warn/info/toolUse — gsd
|
|
523
|
+
// has no cockpit, so a COMPLETE no-op (not just stepStart/stepDone) is
|
|
524
|
+
// required or the item throws "progress.debug is not a function".
|
|
525
|
+
progress: NOOP_PROGRESS,
|
|
526
|
+
streamWriter: { write() {} },
|
|
527
|
+
perItemTimeoutMs: ctx.perItemTimeoutMs,
|
|
528
|
+
stuckDetector: ctx.stuckDetector,
|
|
529
|
+
});
|
|
530
|
+
} catch (error) {
|
|
531
|
+
// D3: a stuck verdict halts the run. Persist the diagnostic + resume
|
|
532
|
+
// state and return a terminal `stuck` envelope so runGsd's loop exits.
|
|
533
|
+
if (!(error instanceof ConsumerStuckError)) throw error;
|
|
534
|
+
const verdict = {
|
|
535
|
+
taskId: error.taskId,
|
|
536
|
+
signal: error.verdict?.signal,
|
|
537
|
+
detail: error.verdict?.detail,
|
|
538
|
+
attemptCounts: ctx.stuckDetector?.attemptCounts?.(error.taskId) ?? {},
|
|
539
|
+
};
|
|
540
|
+
ctx.stuck = verdict;
|
|
541
|
+
writeStuckArtifacts(ctx, { flow_id: flowId, step_id: descriptor.step ?? descriptor.id }, verdict);
|
|
542
|
+
return { status: 'stuck', flow_id: flowId, step_id: descriptor.step ?? descriptor.id };
|
|
523
543
|
}
|
|
524
|
-
// executeParallelDispatchServer returns the next-step dispatch envelope
|
|
525
|
-
return outcome;
|
|
526
544
|
}
|
|
527
545
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const shipResult = await executeShipStep(
|
|
546
|
+
const step = ready[0];
|
|
547
|
+
ctx.lastStepId = step.id;
|
|
548
|
+
if (step.id === 'ship_gsd') {
|
|
549
|
+
const result = await executeShipStep(
|
|
533
550
|
featureCode,
|
|
534
551
|
cwd,
|
|
535
552
|
cwd,
|
|
@@ -537,80 +554,161 @@ async function runOneStep(response, ctx) {
|
|
|
537
554
|
'',
|
|
538
555
|
null,
|
|
539
556
|
);
|
|
540
|
-
|
|
541
|
-
// user-facing operation deferred to the user in v1; runBuild's ship
|
|
542
|
-
// step doesn't auto-push either. Document via ship intent later.
|
|
543
|
-
return await stratum.stepDone(flowId, stepId, shipResult);
|
|
557
|
+
return stratum.stepDone(flowId, step.id, { output: result }, step.dispatchToken);
|
|
544
558
|
}
|
|
545
|
-
|
|
546
|
-
// COMP-GSD-5 resume: skip the decompose AGENT entirely and substitute the
|
|
547
|
-
// persisted task graph (already enriched/repaired during the original run
|
|
548
|
-
// and already filtered to exclude completedTaskIds). We do NOT re-run
|
|
549
|
-
// validateAndRepairTaskGraph: enrichTaskGraph would flag the completed
|
|
550
|
-
// tasks' Boundary Map slices as orphaned (no task in the SUBSET owns them).
|
|
551
|
-
// Stable task IDs + no re-decompose are the whole point.
|
|
552
|
-
if (stepId === 'decompose_gsd' && ctx.resumeTaskGraph) {
|
|
559
|
+
if (step.id === 'decompose_gsd' && ctx.resumeTaskGraph) {
|
|
553
560
|
ctx.lastTaskGraph = ctx.resumeTaskGraph;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
flushState(ctx, {
|
|
558
|
-
phase: 'execute',
|
|
559
|
-
resumeReady: true,
|
|
560
|
-
decomposedTasks: (ctx.resumeTaskGraph.tasks ?? []).map((t) => ({ ...t })),
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
return await stratum.stepDone(flowId, stepId, ctx.resumeTaskGraph);
|
|
561
|
+
return stratum.stepDone(
|
|
562
|
+
flowId, step.id, { output: ctx.resumeTaskGraph }, step.dispatchToken,
|
|
563
|
+
);
|
|
564
564
|
}
|
|
565
565
|
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
const
|
|
569
|
-
const text =
|
|
566
|
+
// D2(b): dispatch via agentRun (not runAgentText) so the TS complete
|
|
567
|
+
// envelope's usage is captured and debited into the cumulative ledger.
|
|
568
|
+
const runOut = await stratum.agentRun(step.agent ?? 'claude', step.do ?? '', { cwd });
|
|
569
|
+
const text = runOut?.text ?? '';
|
|
570
|
+
recordTsAgentUsage(ctx, runOut?.usage);
|
|
571
|
+
// V1: also report usage in the step_done envelope so the ENGINE debits its
|
|
572
|
+
// flow budget (not just compose's cumulative ledger above).
|
|
573
|
+
const ordinaryUsage = toEngineUsage(runOut?.usage);
|
|
570
574
|
let result;
|
|
571
575
|
try {
|
|
572
576
|
result = parseJsonFromText(text);
|
|
573
577
|
} catch (err) {
|
|
574
|
-
throw new Error(
|
|
575
|
-
`runGsd: step ${stepId} agent did not return parseable JSON: ${err.message}`,
|
|
576
|
-
);
|
|
578
|
+
throw new Error(`runGsd: step ${step.id} agent did not return parseable JSON: ${err.message}`);
|
|
577
579
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
580
|
+
if (step.id === 'decompose_gsd') {
|
|
581
|
+
try {
|
|
582
|
+
result = validateAndRepairTaskGraph(result, ctx.blueprintText, ctx.preMergeGate ?? ctx.gateCommands);
|
|
583
|
+
} catch (err) {
|
|
584
|
+
// F6: an ownership conflict is an expected, retryable rejection. Send a
|
|
585
|
+
// FAILURE step_done envelope (not a thrown abort) so decompose_gsd's
|
|
586
|
+
// declared attempts govern — the engine retries with previousFailure
|
|
587
|
+
// feedback. Genuinely unexpected errors still propagate.
|
|
588
|
+
if (!(err instanceof TaskGraphOwnershipError) && !(err instanceof TaskGraphDuplicateIdError)) throw err;
|
|
589
|
+
return stratum.stepDone(
|
|
590
|
+
flowId, step.id,
|
|
591
|
+
{ failure: err.message, ...(ordinaryUsage ? { usage: ordinaryUsage } : {}) },
|
|
592
|
+
step.dispatchToken,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
588
595
|
ctx.lastTaskGraph = result;
|
|
589
|
-
// COMP-GSD-6: the task graph now exists → resumeReady true; persist it so a
|
|
590
|
-
// crash during execute can synthesize a resume graph from state.json.
|
|
591
596
|
if (ctx.runState) {
|
|
592
597
|
flushState(ctx, {
|
|
593
598
|
phase: 'execute',
|
|
594
599
|
resumeReady: true,
|
|
595
|
-
decomposedTasks: (result.tasks ?? []).map((
|
|
600
|
+
decomposedTasks: (result.tasks ?? []).map((task) => ({ ...task })),
|
|
596
601
|
});
|
|
597
602
|
}
|
|
598
603
|
}
|
|
599
|
-
|
|
600
|
-
|
|
604
|
+
return stratum.stepDone(
|
|
605
|
+
flowId, step.id,
|
|
606
|
+
{ output: result, ...(ordinaryUsage ? { usage: ordinaryUsage } : {}) },
|
|
607
|
+
step.dispatchToken,
|
|
608
|
+
);
|
|
601
609
|
}
|
|
602
610
|
|
|
603
|
-
if (response.status === '
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
611
|
+
if (response.status === 'running') {
|
|
612
|
+
const audit = await stratum.audit(flowId);
|
|
613
|
+
const waiting = Object.entries(audit?.steps ?? {})
|
|
614
|
+
.find(([, state]) => state?.status === 'waiting_gate');
|
|
615
|
+
if (!waiting) throw new Error('runGsd: TS engine is running without ready work or a waiting gate');
|
|
616
|
+
const [gateStepId, gateState] = waiting;
|
|
617
|
+
const steps = localSpec.flows[localSpec.flows.entry].steps;
|
|
618
|
+
const gateStep = steps.find((step) => step.id === gateStepId);
|
|
619
|
+
const predecessorIds = new Set(gateStep?.after ?? []);
|
|
620
|
+
const fanoutStep = steps.find((step) => predecessorIds.has(step.id) && step.fanout?.dispatch === 'consumer');
|
|
621
|
+
if (!fanoutStep) {
|
|
622
|
+
throw new Error(`runGsd: unexpected non-consumer gate ${gateStepId} on the TS path`);
|
|
623
|
+
}
|
|
624
|
+
if (!ctx.consumerArtifacts) {
|
|
625
|
+
ctx.consumerArtifacts = new ConsumerFanoutArtifacts({
|
|
626
|
+
runId: flowId,
|
|
627
|
+
targetCwd: cwd,
|
|
628
|
+
revisionDigest: response.revisionDigest,
|
|
629
|
+
specDigest: localSpecDigest,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
const artifacts = ctx.consumerArtifacts;
|
|
633
|
+
artifacts.recordGateBinding({ gateStepId, fanoutStepId: fanoutStep.id });
|
|
634
|
+
let transaction;
|
|
635
|
+
let outcome = 'approve';
|
|
636
|
+
let rationale = 'consumer fanout artifacts merged';
|
|
637
|
+
try {
|
|
638
|
+
transaction = artifacts.prepareMerge({
|
|
639
|
+
gateStepId,
|
|
640
|
+
gateToken: gateState.gateToken,
|
|
641
|
+
fanoutStepId: fanoutStep.id,
|
|
642
|
+
audit,
|
|
643
|
+
});
|
|
644
|
+
await artifacts.applyMerge(transaction);
|
|
645
|
+
} catch (error) {
|
|
646
|
+
if (!(error instanceof ConsumerMergeDecisionError)) throw error;
|
|
647
|
+
outcome = gateStep?.gate?.on_revise ? 'revise' : 'kill';
|
|
648
|
+
rationale = `${error.code}: ${error.message}`;
|
|
649
|
+
transaction ??= artifacts.journal.mergeTransactions.find(
|
|
650
|
+
(entry) => entry.gateToken === gateState.gateToken,
|
|
651
|
+
);
|
|
652
|
+
if (transaction) artifacts.restoreMergeBaseline(transaction, audit);
|
|
653
|
+
}
|
|
654
|
+
const next = await stratum.gateResolve(
|
|
655
|
+
flowId, gateStepId, outcome, rationale, 'system', gateState.gateToken,
|
|
607
656
|
);
|
|
657
|
+
artifacts.markGateResolved(transaction, outcome);
|
|
658
|
+
if (outcome !== 'revise') artifacts.cleanupWorktrees(`GSD merge gate ${outcome}`);
|
|
659
|
+
if (outcome === 'approve') ctx.filesChanged = collectChangedFiles(cwd);
|
|
660
|
+
return next;
|
|
608
661
|
}
|
|
609
662
|
|
|
610
|
-
|
|
663
|
+
return response;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* F6: raised by validateAndRepairTaskGraph on a files_owned conflict. A typed
|
|
668
|
+
* class lets the decompose_gsd site distinguish this expected, retryable rejection
|
|
669
|
+
* (→ failure step_done envelope, engine attempts govern) from a genuinely
|
|
670
|
+
* unexpected error (which still propagates).
|
|
671
|
+
*/
|
|
672
|
+
export class TaskGraphOwnershipError extends Error {
|
|
673
|
+
constructor(conflict) {
|
|
674
|
+
super(`decompose_gsd file-ownership conflict: ${conflict}`);
|
|
675
|
+
this.name = 'TaskGraphOwnershipError';
|
|
676
|
+
this.conflict = conflict;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Raised by validateAndRepairTaskGraph when two tasks share an `id`. Task ids are
|
|
682
|
+
* the primary key for EVERY GSD id-keyed system: the blackboard, `results/<id>.json`,
|
|
683
|
+
* the milestone timing/diff instrumentation (keyed by gsdTaskId), and the stuck
|
|
684
|
+
* detector's per-task bookkeeping. A duplicate id silently cross-contaminates all of
|
|
685
|
+
* them (last-writer-wins results, inherited detector state), so reject it up front —
|
|
686
|
+
* a retryable malformed-decompose rejection, same channel as the ownership conflict.
|
|
687
|
+
*/
|
|
688
|
+
export class TaskGraphDuplicateIdError extends Error {
|
|
689
|
+
constructor(id) {
|
|
690
|
+
super(`decompose_gsd duplicate task id: ${id} (task ids must be unique)`);
|
|
691
|
+
this.name = 'TaskGraphDuplicateIdError';
|
|
692
|
+
this.id = id;
|
|
693
|
+
}
|
|
611
694
|
}
|
|
612
695
|
|
|
613
696
|
export function validateAndRepairTaskGraph(taskGraph, blueprintText, gateCommands) {
|
|
697
|
+
// D5: deterministic file-ownership enforcement — reject overlapping
|
|
698
|
+
// files_owned before any repair. Two tasks owning the same file would race
|
|
699
|
+
// in their worktrees and collide at merge; fail loudly with the conflict.
|
|
700
|
+
const conflict = filesOwnedConflict(taskGraph?.tasks);
|
|
701
|
+
if (conflict) throw new TaskGraphOwnershipError(conflict);
|
|
702
|
+
|
|
703
|
+
// Unique-id enforcement — every id-keyed GSD system (blackboard, results,
|
|
704
|
+
// milestone instrumentation, stuck detector) assumes task ids are unique.
|
|
705
|
+
const seenIds = new Set();
|
|
706
|
+
for (const task of taskGraph?.tasks ?? []) {
|
|
707
|
+
if (task?.id == null) continue;
|
|
708
|
+
if (seenIds.has(task.id)) throw new TaskGraphDuplicateIdError(task.id);
|
|
709
|
+
seenIds.add(task.id);
|
|
710
|
+
}
|
|
711
|
+
|
|
614
712
|
// Structural check via enrichTaskGraph. Throws on orphan slice/task —
|
|
615
713
|
// that's a "fail loudly" case (no reliable repair path).
|
|
616
714
|
const enriched = enrichTaskGraph(taskGraph, blueprintText);
|
|
@@ -1226,8 +1324,8 @@ function writeBudgetArtifacts(ctx, response, budgetState) {
|
|
|
1226
1324
|
writeFileSync(join(dir, 'budget.md'), md);
|
|
1227
1325
|
|
|
1228
1326
|
const pause = {
|
|
1229
|
-
flowId: response.
|
|
1230
|
-
stepId:
|
|
1327
|
+
flowId: response.runId ?? null,
|
|
1328
|
+
stepId: ctx.lastStepId ?? 'execute',
|
|
1231
1329
|
kind: 'budget',
|
|
1232
1330
|
budget: { axis: json.axis, caps: budgetState.caps ?? {}, consumed: budgetState.consumed ?? {} },
|
|
1233
1331
|
decomposedTasks,
|
|
@@ -1263,15 +1361,71 @@ export function writeBudgetFinalSnapshot(ctx, budgetState) {
|
|
|
1263
1361
|
* from the stratum budget_state.consumed ({tokens,dispatches,wall_s,dollars}).
|
|
1264
1362
|
* No-op when budget_state is absent (un-budgeted runs).
|
|
1265
1363
|
*/
|
|
1266
|
-
function recordGsdUsageFromState(cwd, featureCode, budgetState) {
|
|
1364
|
+
export function recordGsdUsageFromState(cwd, featureCode, budgetState, alreadyRecorded = null) {
|
|
1267
1365
|
const consumed = budgetState?.consumed;
|
|
1268
1366
|
if (!consumed) return;
|
|
1269
|
-
|
|
1367
|
+
const engine = {
|
|
1270
1368
|
tokens: consumed.tokens ?? 0,
|
|
1271
1369
|
costUsd: consumed.dollars ?? 0,
|
|
1272
1370
|
dispatches: consumed.dispatches ?? 0,
|
|
1273
1371
|
timeMs: Math.round((consumed.wall_s ?? 0) * 1000),
|
|
1274
|
-
}
|
|
1372
|
+
};
|
|
1373
|
+
// G4: the engine ledger is ground truth, but on the TS path it now INCLUDES the
|
|
1374
|
+
// per-invocation usage compose already recorded incrementally into the same
|
|
1375
|
+
// cumulative store (via recordTsAgentUsage, from the step_done envelopes). Fold
|
|
1376
|
+
// only the DELTA — the engine-only debits (e.g. judged predicates) not already
|
|
1377
|
+
// counted — so nothing is double-debited. `alreadyRecorded` is this run's
|
|
1378
|
+
// incremental total (ctx.recordedUsage); absent → full fold.
|
|
1379
|
+
const prior = alreadyRecorded ?? { tokens: 0, costUsd: 0, dispatches: 0, timeMs: 0 };
|
|
1380
|
+
const delta = {
|
|
1381
|
+
tokens: Math.max(0, engine.tokens - (prior.tokens ?? 0)),
|
|
1382
|
+
costUsd: Math.max(0, engine.costUsd - (prior.costUsd ?? 0)),
|
|
1383
|
+
dispatches: Math.max(0, engine.dispatches - (prior.dispatches ?? 0)),
|
|
1384
|
+
timeMs: Math.max(0, engine.timeMs - (prior.timeMs ?? 0)),
|
|
1385
|
+
};
|
|
1386
|
+
if (delta.tokens === 0 && delta.costUsd === 0 && delta.dispatches === 0 && delta.timeMs === 0) return;
|
|
1387
|
+
recordGsdUsage(join(cwd, '.compose'), featureCode, delta);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* D7(a): the exact TaskResult path a gsd execute item must write, keyed by the
|
|
1392
|
+
* item's task id. Single-sources the filename with the blackboard reader
|
|
1393
|
+
* (collectBlackboard reads .compose/gsd/<feature>/results/<taskId>.json).
|
|
1394
|
+
*/
|
|
1395
|
+
export function gsdTaskResultPath(featureCode, taskId) {
|
|
1396
|
+
return `.compose/gsd/${featureCode}/results/${taskId}.json`;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// runAndNormalize narrates through a progress object (debug/warn/info/toolUse/
|
|
1400
|
+
// toolSummary/stepStart/stepDone). GSD runs headless with no cockpit, so it
|
|
1401
|
+
// hands the consumer loop a COMPLETE no-op — a partial stub throws mid-item.
|
|
1402
|
+
const NOOP_PROGRESS = Object.freeze({
|
|
1403
|
+
stepStart() {}, stepDone() {}, debug() {}, warn() {}, info() {},
|
|
1404
|
+
toolUse() {}, toolSummary() {},
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* D2(b): debit one TS-route agent invocation's usage into the cumulative gsd
|
|
1409
|
+
* ledger. On the TS path the engine's flow ledger never sees compose-dispatched
|
|
1410
|
+
* agents, so compose accounts each dispatch itself. `usage` is the normalized
|
|
1411
|
+
* shape from runAndNormalize ({input_tokens, output_tokens, cost_usd,
|
|
1412
|
+
* duration_ms}) or the raw TS complete usage ({tokens, usd, ms}).
|
|
1413
|
+
*/
|
|
1414
|
+
export function recordTsAgentUsage(ctx, usage) {
|
|
1415
|
+
if (!usage || !ctx?.cwd || !ctx?.featureCode) return;
|
|
1416
|
+
const tokens = usage.tokens ?? ((usage.input_tokens ?? 0) + (usage.output_tokens ?? 0));
|
|
1417
|
+
const costUsd = usage.cost_usd ?? usage.usd ?? 0;
|
|
1418
|
+
const timeMs = usage.duration_ms ?? usage.ms ?? 0;
|
|
1419
|
+
if (tokens === 0 && costUsd === 0 && timeMs === 0) return;
|
|
1420
|
+
recordGsdUsage(join(ctx.cwd, '.compose'), ctx.featureCode, { tokens, costUsd, timeMs, dispatches: 1 });
|
|
1421
|
+
// G4: remember what THIS run recorded incrementally so the terminal/budget-halt
|
|
1422
|
+
// fold (recordGsdUsageFromState) records only the engine-only delta, never
|
|
1423
|
+
// double-counting these envelope-reported invocations.
|
|
1424
|
+
ctx.recordedUsage ??= { tokens: 0, costUsd: 0, dispatches: 0, timeMs: 0 };
|
|
1425
|
+
ctx.recordedUsage.tokens += tokens;
|
|
1426
|
+
ctx.recordedUsage.costUsd += costUsd;
|
|
1427
|
+
ctx.recordedUsage.timeMs += timeMs;
|
|
1428
|
+
ctx.recordedUsage.dispatches += 1;
|
|
1275
1429
|
}
|
|
1276
1430
|
|
|
1277
1431
|
/**
|