@smartmemory/compose 0.2.8-beta → 0.2.9-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.
- package/bin/compose.js +75 -1
- package/contracts/gsd-state.json +140 -0
- package/dist/assets/{App-D3ehVPvi.js → App-CG-2euMe.js} +164 -164
- package/dist/assets/{arc-Dmf69iHG.js → arc-7QBWoLra.js} +1 -1
- package/dist/assets/{architectureDiagram-3BPJPVTR-xYo993Yw.js → architectureDiagram-3BPJPVTR-CUw-7uLm.js} +1 -1
- package/dist/assets/{blockDiagram-GPEHLZMM-UX4EF98O.js → blockDiagram-GPEHLZMM-COU1vmr7.js} +1 -1
- package/dist/assets/{c4Diagram-AAUBKEIU-DaP9CGWb.js → c4Diagram-AAUBKEIU-XPO9PSJL.js} +1 -1
- package/dist/assets/channel-Bcu04MIK.js +1 -0
- package/dist/assets/{chunk-2J33WTMH-CKk_RN3A.js → chunk-2J33WTMH-zMzVB2a6.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-DboAwYKw.js → chunk-4BX2VUAB-Kke_qcHU.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Dsy9RYvI.js → chunk-55IACEB6-hMeFx5Nh.js} +1 -1
- package/dist/assets/{chunk-727SXJPM-fAH0QO9v.js → chunk-727SXJPM-DesUnrEw.js} +1 -1
- package/dist/assets/{chunk-AQP2D5EJ-DyZYerFP.js → chunk-AQP2D5EJ-1uGGvkxW.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BnboGO5t.js → chunk-FMBD7UC4-DYHv1PcZ.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-Di9tYXme.js → chunk-ND2GUHAM-D0MENOLX.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-zRPRlAIL.js → chunk-QZHKN3VN-8nn3HP-N.js} +1 -1
- package/dist/assets/classDiagram-4FO5ZUOK-DU4yxldU.js +1 -0
- package/dist/assets/classDiagram-v2-Q7XG4LA2-DU4yxldU.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-C7Hqukaf.js → cose-bilkent-S5V4N54A-BoZPVIny.js} +1 -1
- package/dist/assets/{dagre-BM42HDAG-B-cR-BjI.js → dagre-BM42HDAG-BgZzdLG9.js} +1 -1
- package/dist/assets/{diagram-2AECGRRQ-B6-5onDk.js → diagram-2AECGRRQ-CknAnpSu.js} +1 -1
- package/dist/assets/{diagram-5GNKFQAL-DoZZgFAM.js → diagram-5GNKFQAL-CZUEbKim.js} +1 -1
- package/dist/assets/{diagram-KO2AKTUF-77jEGlJh.js → diagram-KO2AKTUF-DCs-pLdH.js} +1 -1
- package/dist/assets/{diagram-LMA3HP47-D3S7XDRD.js → diagram-LMA3HP47-lRaDjIfM.js} +1 -1
- package/dist/assets/{diagram-OG6HWLK6-KbYL9aCY.js → diagram-OG6HWLK6-CIGqmehP.js} +1 -1
- package/dist/assets/{erDiagram-TEJ5UH35-DezFbJP-.js → erDiagram-TEJ5UH35-Lx3c2N6F.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-4x31cK9j.js → flowDiagram-I6XJVG4X-VoluKqSq.js} +1 -1
- package/dist/assets/{ganttDiagram-6RSMTGT7-FopfSTyZ.js → ganttDiagram-6RSMTGT7-D7hETiNZ.js} +1 -1
- package/dist/assets/{gitGraphDiagram-PVQCEYII-DSiQGKbN.js → gitGraphDiagram-PVQCEYII-DenEcUvY.js} +1 -1
- package/dist/assets/{index-ClX6LVAf.js → index-B4dv3acY.js} +2 -2
- package/dist/assets/{infoDiagram-5YYISTIA-DE6BqzK_.js → infoDiagram-5YYISTIA-v7cq9Er9.js} +1 -1
- package/dist/assets/{ishikawaDiagram-YF4QCWOH-Dml8NwQI.js → ishikawaDiagram-YF4QCWOH-CfCCXt2x.js} +1 -1
- package/dist/assets/{journeyDiagram-JHISSGLW-CwWeJgjE.js → journeyDiagram-JHISSGLW-Bbokl_xO.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-DnG956Wh.js → kanban-definition-UN3LZRKU-DhkOZ2hg.js} +1 -1
- package/dist/assets/{linear-CA3N7Rpi.js → linear-bHjluRm2.js} +1 -1
- package/dist/assets/{mindmap-definition-RKZ34NQL-CxfIOjLX.js → mindmap-definition-RKZ34NQL-C1bHpoXH.js} +1 -1
- package/dist/assets/{pieDiagram-4H26LBE5-O7aIwy1x.js → pieDiagram-4H26LBE5-CZb1i55T.js} +1 -1
- package/dist/assets/{quadrantDiagram-W4KKPZXB-CPQ2qq7c.js → quadrantDiagram-W4KKPZXB-o37AwRHB.js} +1 -1
- package/dist/assets/{requirementDiagram-4Y6WPE33-C23horL4.js → requirementDiagram-4Y6WPE33-BVErWDzU.js} +1 -1
- package/dist/assets/{sankeyDiagram-5OEKKPKP-DPY04kOW.js → sankeyDiagram-5OEKKPKP-BhBK8gHQ.js} +1 -1
- package/dist/assets/{sequenceDiagram-3UESZ5HK-BKaTfIvo.js → sequenceDiagram-3UESZ5HK-CsICF23P.js} +1 -1
- package/dist/assets/{stateDiagram-AJRCARHV-B9na_6mY.js → stateDiagram-AJRCARHV-TN1AXwim.js} +1 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-BLR6AkKX.js +1 -0
- package/dist/assets/{timeline-definition-PNZ67QCA-BBWPqd7X.js → timeline-definition-PNZ67QCA-DftAajbU.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-tWqiHsOZ.js → vennDiagram-CIIHVFJN-cFTMstT7.js} +1 -1
- package/dist/assets/{wardley-L42UT6IY-DorxG6os.js → wardley-L42UT6IY-DL8CivzO.js} +1 -1
- package/dist/assets/{wardleyDiagram-YWT4CUSO-B49f8GzW.js → wardleyDiagram-YWT4CUSO-BDZT1hQj.js} +1 -1
- package/dist/assets/{xychartDiagram-2RQKCTM6-BgKSj8Qb.js → xychartDiagram-2RQKCTM6-DQQSkfC4.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/build.js +36 -2
- package/lib/gsd-diff-capture.js +34 -0
- package/lib/gsd-events.js +61 -0
- package/lib/gsd-headless-config.js +110 -0
- package/lib/gsd-milestone-report.js +323 -0
- package/lib/gsd-state.js +165 -0
- package/lib/gsd-supervisor.js +223 -0
- package/lib/gsd-timing.js +89 -0
- package/lib/gsd.js +446 -45
- package/package.json +1 -1
- package/dist/assets/channel-D_RXsFFT.js +0 -1
- package/dist/assets/classDiagram-4FO5ZUOK-K6wdB4ic.js +0 -1
- package/dist/assets/classDiagram-v2-Q7XG4LA2-K6wdB4ic.js +0 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-Cf84VDiH.js +0 -1
package/lib/gsd.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// V1 limitation: runtime task-to-task handoff is not implemented; tasks see
|
|
14
14
|
// only spec-level upstream context (Boundary Map declarations) per blueprint.
|
|
15
15
|
|
|
16
|
-
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, rmSync } from 'node:fs';
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, rmSync, statSync, renameSync } from 'node:fs';
|
|
17
17
|
import { join, resolve, dirname } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { execSync } from 'node:child_process';
|
|
@@ -27,6 +27,12 @@ import { executeParallelDispatchServer, executeShipStep } from './build.js';
|
|
|
27
27
|
import { GsdStuckDetector, DEFAULT_THRESHOLDS } from './gsd-stuck.js';
|
|
28
28
|
import { readGsdBudgetConfig, buildBudgetBlock, injectBudget, composeBudgetDiagnostic } from './gsd-budget.js';
|
|
29
29
|
import { recordGsdUsage, checkGsdCumulativeBudget } from './budget-ledger.js';
|
|
30
|
+
// COMP-GSD-6: continuous run-state checkpoint + canonical pid-liveness probe.
|
|
31
|
+
// pidAlive is canonical in gsd-state.js (EPERM=alive) and imported one-way here.
|
|
32
|
+
import { writeGsdState, readGsdState, gsdStatePath, pidAlive, clearGsdHaltArtifacts } from './gsd-state.js';
|
|
33
|
+
import { generateGsdMilestoneReport } from './gsd-milestone-report.js';
|
|
34
|
+
import { readHeadlessConfig } from './gsd-headless-config.js';
|
|
35
|
+
import { appendGsdEvent, clearGsdEvents } from './gsd-events.js';
|
|
30
36
|
|
|
31
37
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
38
|
const PACKAGE_ROOT = resolve(__dirname, '..');
|
|
@@ -41,6 +47,15 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
41
47
|
}
|
|
42
48
|
const cwd = opts.cwd ?? process.cwd();
|
|
43
49
|
|
|
50
|
+
// COMP-GSD-6: a FRESH (non-resume) run must not inherit a prior run's
|
|
51
|
+
// state.json. Clear it up front so that if a precondition below throws BEFORE
|
|
52
|
+
// the planning checkpoint, NO running state remains → the headless supervisor
|
|
53
|
+
// (and `query`) read 'absent' → fatal-by-absence, never a stale 'complete'
|
|
54
|
+
// success. A resume keeps the old state.json (the crash-bridge may need it).
|
|
55
|
+
if (!opts.resume) {
|
|
56
|
+
try { rmSync(gsdStatePath(cwd, featureCode), { force: true }); } catch { /* ignore */ }
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
// 1. Validate preconditions: blueprint exists + Boundary Map ok
|
|
45
60
|
const blueprintPath = join(cwd, 'docs', 'features', featureCode, 'blueprint.md');
|
|
46
61
|
if (!existsSync(blueprintPath)) {
|
|
@@ -143,7 +158,20 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
143
158
|
// from clobbering a concurrent resume's valid claim and (b) a claim-race loser
|
|
144
159
|
// (EEXIST) from deleting the winner's lock on its way out.
|
|
145
160
|
let lockClaimed = false;
|
|
161
|
+
let runLockClaimed = false;
|
|
162
|
+
// COMP-GSD-6: the in-memory run-state, threaded through stepCtx and flushed to
|
|
163
|
+
// state.json. Declared here so the catch/finally can read it.
|
|
164
|
+
let stepCtx = null;
|
|
165
|
+
// COMP-GSD-6-WATCHDOG: independent wall-clock heartbeat timer (see below).
|
|
166
|
+
// Declared here so the finally can always clear it.
|
|
167
|
+
let heartbeatTimer = null;
|
|
146
168
|
try {
|
|
169
|
+
// COMP-GSD-6: claim the live-run lock BEFORE any stratum side effect, so two
|
|
170
|
+
// fresh `compose gsd <same-feature>` runs can't race the results dir. Takes
|
|
171
|
+
// over a stale lock (dead owner) and refuses a live one.
|
|
172
|
+
claimRunLock(cwd, featureCode);
|
|
173
|
+
runLockClaimed = true;
|
|
174
|
+
|
|
147
175
|
// COMP-GSD-4: claim the resume lock HERE (first statement in the try) so the
|
|
148
176
|
// finally releases it on EVERY exit — budget/stuck re-halt, throw, or clean
|
|
149
177
|
// finish. loadResumeTaskGraph above already read+guarded (claim:false).
|
|
@@ -152,22 +180,90 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
152
180
|
lockClaimed = true;
|
|
153
181
|
}
|
|
154
182
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
// COMP-GSD-6: pre-plan "planning" checkpoint. A crash during plan/decompose
|
|
184
|
+
// now leaves a dead-pid state.json — the failed-vs-fatal boundary. A throw
|
|
185
|
+
// BEFORE this point (preconditions) leaves no running state → fatal by
|
|
186
|
+
// absence; a throw AFTER → the catch converts it to status:"failed".
|
|
187
|
+
// On resume, seed the planning checkpoint from the (in-memory) resume graph
|
|
188
|
+
// so that if THIS resume re-crashes before its decompose step repopulates
|
|
189
|
+
// state.json, the crash-bridge still has a task graph to recover from
|
|
190
|
+
// (otherwise the fresh empty checkpoint would clobber the prior good data).
|
|
191
|
+
const resumeTasks = opts.resume ? (resumeTaskGraph?.tasks ?? []).map((t) => ({ ...t })) : [];
|
|
192
|
+
const initialState = {
|
|
193
|
+
feature: featureCode,
|
|
194
|
+
flowId: null,
|
|
195
|
+
pid: process.pid,
|
|
196
|
+
mode: 'gsd',
|
|
197
|
+
phase: 'planning',
|
|
198
|
+
status: 'running',
|
|
199
|
+
startedAt: new Date().toISOString(),
|
|
200
|
+
headless: !!opts.headless,
|
|
201
|
+
attempt: opts.attempt ?? 1,
|
|
202
|
+
resumeReady: opts.resume && resumeTasks.length > 0,
|
|
203
|
+
decomposedTasks: resumeTasks,
|
|
204
|
+
completedTaskIds: collectCompletedTaskIds(cwd, featureCode),
|
|
205
|
+
};
|
|
160
206
|
|
|
161
207
|
// Track files merged into the base cwd by the execute step so ship_gsd
|
|
162
208
|
// can stage them. executeShipStep's default filter only stages feature
|
|
163
209
|
// docs unless context.filesChanged is provided.
|
|
164
|
-
|
|
210
|
+
stepCtx = {
|
|
165
211
|
stratum, cwd, featureCode, blueprintText, gateCommands,
|
|
166
212
|
filesChanged: [],
|
|
167
213
|
stuckDetector,
|
|
168
214
|
resumeTaskGraph,
|
|
169
215
|
stuck: null, // set by runOneStep on a stuck verdict
|
|
216
|
+
runState: initialState, // COMP-GSD-6: flushState merges into this
|
|
217
|
+
// COMP-GSD-7-EVENTLOG: tasks already completed at run start (a resume
|
|
218
|
+
// preloads them) are seeded as already-emitted so the appended log never
|
|
219
|
+
// re-fires task_completed for prior-session completions.
|
|
220
|
+
emittedCompletions: new Set(initialState.completedTaskIds),
|
|
221
|
+
// COMP-GSD-7-EVENTLOG: phases already announced (dedupe — runState.phase is
|
|
222
|
+
// set to 'execute' before the merge checkpoint, so it can't gate emission).
|
|
223
|
+
emittedPhases: new Set(),
|
|
170
224
|
};
|
|
225
|
+
flushState(stepCtx, {}); // write the planning checkpoint
|
|
226
|
+
|
|
227
|
+
// COMP-GSD-7-EVENTLOG: at the planning checkpoint — AFTER preconditions
|
|
228
|
+
// passed (so a failed fresh invocation never wipes a prior run's history) —
|
|
229
|
+
// a fresh run truncates the event log and clears stale halt artifacts so the
|
|
230
|
+
// timeline reflects only this run; a resume appends to the existing log.
|
|
231
|
+
if (!opts.resume) {
|
|
232
|
+
clearGsdEvents(cwd, featureCode);
|
|
233
|
+
clearGsdHaltArtifacts(cwd, featureCode);
|
|
234
|
+
}
|
|
235
|
+
appendGsdEvent(cwd, featureCode, 'run_started', {
|
|
236
|
+
mode: opts.resume ? 'resume' : 'fresh',
|
|
237
|
+
attempt: opts.attempt ?? 1,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// COMP-GSD-6-WATCHDOG: an INDEPENDENT wall-clock heartbeat. The existing
|
|
241
|
+
// heartbeat only advances on agent push-events (onHeartbeat below), so a
|
|
242
|
+
// quiet-but-healthy task would look stale. This timer restamps state.json's
|
|
243
|
+
// heartbeat on a fixed cadence whenever the event loop is still turning — so
|
|
244
|
+
// a stale heartbeat genuinely means the loop is WEDGED (or the process dead),
|
|
245
|
+
// which is what the headless watchdog keys its hung-kill on. .unref() so it
|
|
246
|
+
// never holds the process open; cleared in finally. Same empty-patch restamp
|
|
247
|
+
// onHeartbeat uses, so it's behavior-compatible.
|
|
248
|
+
//
|
|
249
|
+
// Gated to SUPERVISED children only (GSD_HEADLESS_ATTEMPT, set by the
|
|
250
|
+
// supervisor's spawner) — the supervisor is the sole watcher, so an
|
|
251
|
+
// interactive `compose gsd` stays byte-identical (no extra state.json writes).
|
|
252
|
+
if (process.env.GSD_HEADLESS_ATTEMPT != null) {
|
|
253
|
+
const hbMs = readHeadlessConfig(cwd).watchdogHeartbeatMs;
|
|
254
|
+
heartbeatTimer = setInterval(() => {
|
|
255
|
+
try { if (stepCtx?.runState) flushState(stepCtx, {}); } catch { /* best-effort */ }
|
|
256
|
+
}, hbMs);
|
|
257
|
+
heartbeatTimer.unref?.();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let response = await stratum.plan(specYaml, 'gsd', {
|
|
261
|
+
featureCode,
|
|
262
|
+
gateCommands,
|
|
263
|
+
});
|
|
264
|
+
const flowId = response.flow_id;
|
|
265
|
+
flushState(stepCtx, { flowId, phase: 'decompose' });
|
|
266
|
+
emitPhaseOnce(stepCtx, 'decompose'); // COMP-GSD-7-EVENTLOG
|
|
171
267
|
|
|
172
268
|
// 5. Status loop. `stuck` (COMP-GSD-5) and `budget_exhausted` (COMP-GSD-4)
|
|
173
269
|
// are terminal statuses. `stuck` is set compose-side by runOneStep; budget
|
|
@@ -184,6 +280,12 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
184
280
|
|
|
185
281
|
if (response.status === 'stuck') {
|
|
186
282
|
// Artifacts (stuck.md/json + pause.json) were written by runOneStep.
|
|
283
|
+
// COMP-GSD-7-EVENTLOG: flush any completions that finished before the stuck
|
|
284
|
+
// verdict (the stuck path returns early, before the execute-merge delta),
|
|
285
|
+
// then record the pause.
|
|
286
|
+
emitCompletionDeltas(stepCtx);
|
|
287
|
+
appendGsdEvent(cwd, featureCode, 'paused', { pauseKind: 'stuck', taskId: stepCtx.stuck?.taskId ?? null });
|
|
288
|
+
flushState(stepCtx, { status: 'stuck' }); // COMP-GSD-6 terminal checkpoint
|
|
187
289
|
return {
|
|
188
290
|
status: 'stuck',
|
|
189
291
|
flowId,
|
|
@@ -201,6 +303,10 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
201
303
|
writeBudgetArtifacts(stepCtx, response, budgetState);
|
|
202
304
|
recordGsdUsageFromState(cwd, featureCode, budgetState);
|
|
203
305
|
const axis = composeBudgetDiagnostic(budgetState, { feature: featureCode }).json.axis;
|
|
306
|
+
// COMP-GSD-7-EVENTLOG: flush pre-halt completions, then record the pause.
|
|
307
|
+
emitCompletionDeltas(stepCtx);
|
|
308
|
+
appendGsdEvent(cwd, featureCode, 'paused', { pauseKind: 'budget', axis });
|
|
309
|
+
flushState(stepCtx, { status: 'budget' }); // COMP-GSD-6 terminal checkpoint
|
|
204
310
|
return { status: 'budget', flowId, axis, consumed: budgetState.consumed ?? {}, caps: budgetState.caps ?? {} };
|
|
205
311
|
}
|
|
206
312
|
|
|
@@ -218,14 +324,73 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
218
324
|
// the complete envelope carries no budget_state, e.g. un-budgeted runs).
|
|
219
325
|
recordGsdUsageFromState(cwd, featureCode, response.budget_state);
|
|
220
326
|
clearPauseFile(cwd, featureCode);
|
|
327
|
+
// COMP-GSD-7: on a clean complete, budget.json is NOT written (only halts
|
|
328
|
+
// write it). Persist a budget-final.json snapshot so the milestone report
|
|
329
|
+
// (auto + retroactive `gsd report`) has actuals-vs-caps. No-op when the
|
|
330
|
+
// envelope carries no budget_state (un-budgeted run). Best-effort: this is
|
|
331
|
+
// a derived report input — a write failure must NEVER demote a successful
|
|
332
|
+
// run to 'failed' via the outer catch.
|
|
333
|
+
if (response.budget_state) {
|
|
334
|
+
try {
|
|
335
|
+
writeBudgetFinalSnapshot(stepCtx, response.budget_state);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
console.warn(`[gsd] budget-final snapshot failed: ${err.message}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// COMP-GSD-6: terminal state.json flush. Only 'complete' is a success; any
|
|
343
|
+
// other terminal here (e.g. stratum 'killed') maps to 'failed' so we stay
|
|
344
|
+
// within the closed status vocabulary the contract + supervisor share.
|
|
345
|
+
// COMP-GSD-7: stamp completedAt so retroactive reports can recover wall-clock.
|
|
346
|
+
const terminalStatus = response.status === 'complete' ? 'complete' : 'failed';
|
|
347
|
+
// COMP-GSD-7-EVENTLOG: emit the terminal event. complete → final completion
|
|
348
|
+
// deltas + 'completed'; any other terminal (e.g. stratum 'killed') → 'failed'.
|
|
349
|
+
if (terminalStatus === 'complete') {
|
|
350
|
+
emitCompletionDeltas(stepCtx);
|
|
351
|
+
appendGsdEvent(cwd, featureCode, 'completed', {});
|
|
352
|
+
} else {
|
|
353
|
+
appendGsdEvent(cwd, featureCode, 'failed', { reason: response.status ?? 'unknown' });
|
|
354
|
+
}
|
|
355
|
+
flushState(stepCtx, { status: terminalStatus, phase: 'done', completedAt: new Date().toISOString() });
|
|
356
|
+
|
|
357
|
+
// COMP-GSD-7: best-effort milestone report on a clean complete. A report
|
|
358
|
+
// failure must never fail the run — it is a derived artifact.
|
|
359
|
+
if (terminalStatus === 'complete') {
|
|
360
|
+
try {
|
|
361
|
+
const r = generateGsdMilestoneReport(featureCode, cwd);
|
|
362
|
+
if (!r.ok) console.warn(`[gsd] milestone report skipped: ${r.error}`);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
console.warn(`[gsd] milestone report generation failed: ${err.message}`);
|
|
365
|
+
}
|
|
221
366
|
}
|
|
222
367
|
|
|
368
|
+
// Return the normalized closed-vocabulary status (not the raw stratum status)
|
|
369
|
+
// so the CLI/callers don't mistake a 'killed' terminal for success.
|
|
223
370
|
return {
|
|
224
|
-
status:
|
|
371
|
+
status: terminalStatus,
|
|
225
372
|
flowId,
|
|
226
373
|
blackboardEntries: Object.keys(blackboard).length,
|
|
227
374
|
};
|
|
375
|
+
} catch (err) {
|
|
376
|
+
// COMP-GSD-6: an orderly throw AFTER the planning checkpoint becomes a
|
|
377
|
+
// terminal status:"failed" so the supervisor treats it as non-recoverable
|
|
378
|
+
// (vs a hard crash → status stays "running" + dead pid → reader-derived
|
|
379
|
+
// "crashed"). Guard on a persisted running state so pre-checkpoint throws
|
|
380
|
+
// (which left no running state) stay fatal-by-absence, not "failed".
|
|
381
|
+
if (stepCtx?.runState && readGsdState(cwd, featureCode)?.status === 'running') {
|
|
382
|
+
try { flushState(stepCtx, { status: 'failed' }); } catch { /* best-effort */ }
|
|
383
|
+
// COMP-GSD-7-EVENTLOG: record the failure (only when a run actually started
|
|
384
|
+
// — a pre-checkpoint throw left no running state and gets no event). Append
|
|
385
|
+
// is best-effort; never mask the original error.
|
|
386
|
+
appendGsdEvent(cwd, featureCode, 'failed', { reason: err?.message ?? 'error' });
|
|
387
|
+
}
|
|
388
|
+
throw err;
|
|
228
389
|
} finally {
|
|
390
|
+
// COMP-GSD-6-WATCHDOG: stop the independent heartbeat timer.
|
|
391
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
392
|
+
// COMP-GSD-6: release the live-run lock if THIS process claimed it.
|
|
393
|
+
if (runLockClaimed) releaseRunLock(cwd, featureCode);
|
|
229
394
|
// COMP-GSD-4: release the resume claim ONLY if THIS process claimed it
|
|
230
395
|
// (ownership-aware — never clobber a concurrent run's valid claim, and don't
|
|
231
396
|
// release after losing the claim race). pause.json persists for --resume
|
|
@@ -270,11 +435,16 @@ async function runOneStep(response, ctx) {
|
|
|
270
435
|
const outcome = await executeParallelDispatchServer(
|
|
271
436
|
response,
|
|
272
437
|
stratum,
|
|
273
|
-
{ cwd, featureCode },
|
|
438
|
+
{ cwd, featureCode, gsd: true }, // COMP-GSD-7: gates timing+diff capture
|
|
274
439
|
null, // progress
|
|
275
440
|
{ write: () => {} }, // streamWriter — no-op for v1
|
|
276
441
|
cwd,
|
|
277
|
-
{
|
|
442
|
+
{
|
|
443
|
+
stuckDetector: ctx.stuckDetector, // COMP-GSD-5 (null in non-gsd callers)
|
|
444
|
+
// COMP-GSD-6: bump state.json's heartbeat on every task event so a long
|
|
445
|
+
// task sitting in the dispatch poll loop isn't mistaken for crashed.
|
|
446
|
+
onHeartbeat: ctx.runState ? () => { try { flushState(ctx, {}); } catch { /* best-effort */ } } : null,
|
|
447
|
+
},
|
|
278
448
|
);
|
|
279
449
|
|
|
280
450
|
// COMP-GSD-5: a stuck verdict halts the run. Persist the diagnostic +
|
|
@@ -290,6 +460,15 @@ async function runOneStep(response, ctx) {
|
|
|
290
460
|
// staging. The clean-workspace precondition above guarantees every
|
|
291
461
|
// file in the post-execute dirty set is genuinely a GSD-produced change.
|
|
292
462
|
ctx.filesChanged = collectChangedFiles(cwd);
|
|
463
|
+
// COMP-GSD-6: checkpoint completed tasks after the execute merge.
|
|
464
|
+
// COMP-GSD-7-EVENTLOG: emit the execute-phase transition once, then a
|
|
465
|
+
// task_completed event per newly-completed task.
|
|
466
|
+
if (ctx.runState) {
|
|
467
|
+
const completed = collectCompletedTaskIds(cwd, featureCode);
|
|
468
|
+
flushState(ctx, { phase: 'execute', completedTaskIds: completed });
|
|
469
|
+
emitPhaseOnce(ctx, 'execute'); // dedupes; runState.phase can't gate this
|
|
470
|
+
emitCompletionDeltas(ctx, completed);
|
|
471
|
+
}
|
|
293
472
|
// executeParallelDispatchServer returns the next-step dispatch envelope
|
|
294
473
|
return outcome;
|
|
295
474
|
}
|
|
@@ -320,6 +499,15 @@ async function runOneStep(response, ctx) {
|
|
|
320
499
|
// Stable task IDs + no re-decompose are the whole point.
|
|
321
500
|
if (stepId === 'decompose_gsd' && ctx.resumeTaskGraph) {
|
|
322
501
|
ctx.lastTaskGraph = ctx.resumeTaskGraph;
|
|
502
|
+
// COMP-GSD-6: a resume already has the (filtered) task graph — mark
|
|
503
|
+
// resumeReady so a re-crash during execute resumes rather than restarts.
|
|
504
|
+
if (ctx.runState) {
|
|
505
|
+
flushState(ctx, {
|
|
506
|
+
phase: 'execute',
|
|
507
|
+
resumeReady: true,
|
|
508
|
+
decomposedTasks: (ctx.resumeTaskGraph.tasks ?? []).map((t) => ({ ...t })),
|
|
509
|
+
});
|
|
510
|
+
}
|
|
323
511
|
return await stratum.stepDone(flowId, stepId, ctx.resumeTaskGraph);
|
|
324
512
|
}
|
|
325
513
|
|
|
@@ -343,6 +531,15 @@ async function runOneStep(response, ctx) {
|
|
|
343
531
|
// persist the full task definitions (with descriptions/produces/consumes)
|
|
344
532
|
// into pause.json — resume re-dispatches these without re-enriching.
|
|
345
533
|
ctx.lastTaskGraph = result;
|
|
534
|
+
// COMP-GSD-6: the task graph now exists → resumeReady true; persist it so a
|
|
535
|
+
// crash during execute can synthesize a resume graph from state.json.
|
|
536
|
+
if (ctx.runState) {
|
|
537
|
+
flushState(ctx, {
|
|
538
|
+
phase: 'execute',
|
|
539
|
+
resumeReady: true,
|
|
540
|
+
decomposedTasks: (result.tasks ?? []).map((t) => ({ ...t })),
|
|
541
|
+
});
|
|
542
|
+
}
|
|
346
543
|
}
|
|
347
544
|
|
|
348
545
|
return await stratum.stepDone(flowId, stepId, result);
|
|
@@ -507,6 +704,139 @@ function gsdDir(cwd, featureCode) {
|
|
|
507
704
|
return join(cwd, '.compose', 'gsd', featureCode);
|
|
508
705
|
}
|
|
509
706
|
|
|
707
|
+
// ===========================================================================
|
|
708
|
+
// COMP-GSD-6: run.lock (live-run exclusivity) + state.json flush helpers
|
|
709
|
+
// ===========================================================================
|
|
710
|
+
|
|
711
|
+
const RUN_LOCK_STALE_MS = 90000;
|
|
712
|
+
|
|
713
|
+
function runLockDir(cwd, featureCode) {
|
|
714
|
+
return join(gsdDir(cwd, featureCode), 'run.lock');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Atomically take over a stale lock dir. The naive `rmSync` + `mkdirSync` is
|
|
718
|
+
// racy — two reclaimers can both see "stale", both rm, and one deletes the
|
|
719
|
+
// other's fresh lock. renameSync IS atomic, so only one racer can rename the
|
|
720
|
+
// stale dir aside; the loser gets ENOENT. The winner removes the renamed copy
|
|
721
|
+
// and re-creates the lock; if a NEW claimant raced into the freed name first,
|
|
722
|
+
// our mkdir gets EEXIST and we (correctly) report we lost. Returns true iff WE
|
|
723
|
+
// recreated the lock.
|
|
724
|
+
function takeoverStaleLock(lockPath) {
|
|
725
|
+
const aside = `${lockPath}.stale.${process.pid}.${Date.now()}`;
|
|
726
|
+
try {
|
|
727
|
+
renameSync(lockPath, aside); // atomic — loser gets ENOENT
|
|
728
|
+
} catch {
|
|
729
|
+
return false; // another racer already took it over (or it vanished)
|
|
730
|
+
}
|
|
731
|
+
try { rmSync(aside, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
732
|
+
try {
|
|
733
|
+
mkdirSync(lockPath);
|
|
734
|
+
return true;
|
|
735
|
+
} catch (err) {
|
|
736
|
+
if (err.code === 'EEXIST') return false; // a fresh claimant won the freed name
|
|
737
|
+
throw err;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Read the owning pid for a run.lock: run.lock/owner.json first (lock-local
|
|
742
|
+
// record), then state.json (Codex review precedence). Returns a number or null.
|
|
743
|
+
function runLockOwnerPid(cwd, featureCode) {
|
|
744
|
+
const ownerPath = join(runLockDir(cwd, featureCode), 'owner.json');
|
|
745
|
+
if (existsSync(ownerPath)) {
|
|
746
|
+
try {
|
|
747
|
+
const o = JSON.parse(readFileSync(ownerPath, 'utf-8'));
|
|
748
|
+
if (typeof o.pid === 'number') return o.pid;
|
|
749
|
+
} catch { /* fall through to state.json */ }
|
|
750
|
+
}
|
|
751
|
+
const state = readGsdState(cwd, featureCode);
|
|
752
|
+
return typeof state?.pid === 'number' ? state.pid : null;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Atomic live-run claim, taken BEFORE the first stratum side effect. mkdirSync
|
|
756
|
+
// is atomic on POSIX: the loser gets EEXIST. On EEXIST we take over a STALE lock
|
|
757
|
+
// — owner pid dead, OR (no owner record AND lock-dir mtime older than the stale
|
|
758
|
+
// window, which covers the sub-ms gap before owner.json lands). A live owner
|
|
759
|
+
// refuses. Writes run.lock/owner.json {pid,startedAt} immediately after winning.
|
|
760
|
+
export function claimRunLock(cwd, featureCode) {
|
|
761
|
+
const dir = gsdDir(cwd, featureCode);
|
|
762
|
+
mkdirSync(dir, { recursive: true });
|
|
763
|
+
const lock = runLockDir(cwd, featureCode);
|
|
764
|
+
const write = () => {
|
|
765
|
+
writeFileSync(
|
|
766
|
+
join(lock, 'owner.json'),
|
|
767
|
+
JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }, null, 2),
|
|
768
|
+
);
|
|
769
|
+
};
|
|
770
|
+
try {
|
|
771
|
+
mkdirSync(lock);
|
|
772
|
+
write();
|
|
773
|
+
return;
|
|
774
|
+
} catch (err) {
|
|
775
|
+
if (err.code !== 'EEXIST') throw err;
|
|
776
|
+
}
|
|
777
|
+
// EEXIST — decide stale vs live.
|
|
778
|
+
const ownerPid = runLockOwnerPid(cwd, featureCode);
|
|
779
|
+
let stale = false;
|
|
780
|
+
if (typeof ownerPid === 'number') {
|
|
781
|
+
stale = !pidAlive(ownerPid);
|
|
782
|
+
} else {
|
|
783
|
+
// No owner record yet: fall back to lock-dir age.
|
|
784
|
+
try {
|
|
785
|
+
stale = Date.now() - statSync(lock).mtimeMs > RUN_LOCK_STALE_MS;
|
|
786
|
+
} catch { stale = true; }
|
|
787
|
+
}
|
|
788
|
+
if (!stale) {
|
|
789
|
+
throw new Error(
|
|
790
|
+
`runGsd: another gsd run owns ${featureCode} (.compose/gsd/${featureCode}/run.lock, ` +
|
|
791
|
+
`pid ${ownerPid ?? 'unknown'} alive). Refusing to start a concurrent run.`,
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
// Atomic stale takeover (rename-aside). If we lose the takeover race, another
|
|
795
|
+
// run now legitimately owns the feature — refuse.
|
|
796
|
+
if (!takeoverStaleLock(lock)) {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`runGsd: another gsd run claimed ${featureCode} during stale-lock takeover. ` +
|
|
799
|
+
`Refusing to start a concurrent run.`,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
write();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export function releaseRunLock(cwd, featureCode) {
|
|
806
|
+
rmSync(runLockDir(cwd, featureCode), { recursive: true, force: true });
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Merge a patch into ctx.runState and atomically flush state.json. ctx.runState
|
|
810
|
+
// is the single in-memory source of truth; every flush restamps heartbeatAt.
|
|
811
|
+
function flushState(ctx, patch) {
|
|
812
|
+
ctx.runState = { ...(ctx.runState ?? {}), ...patch };
|
|
813
|
+
writeGsdState(ctx.cwd, ctx.featureCode, ctx.runState);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// COMP-GSD-7-EVENTLOG: emit a `task_completed` event for each task that has
|
|
817
|
+
// completed since the last emit. Dedupes via ctx.emittedCompletions (seeded from
|
|
818
|
+
// the run's initial completed snapshot, so a resume never re-fires prior-session
|
|
819
|
+
// completions). Called at the execute-merge checkpoint and before each halt
|
|
820
|
+
// (stuck/budget) — the halt paths return early, before the merge checkpoint.
|
|
821
|
+
function emitCompletionDeltas(ctx, completedIds) {
|
|
822
|
+
if (!ctx?.emittedCompletions) return;
|
|
823
|
+
const ids = completedIds ?? collectCompletedTaskIds(ctx.cwd, ctx.featureCode);
|
|
824
|
+
for (const id of ids) {
|
|
825
|
+
if (!id || ctx.emittedCompletions.has(id)) continue;
|
|
826
|
+
ctx.emittedCompletions.add(id);
|
|
827
|
+
appendGsdEvent(ctx.cwd, ctx.featureCode, 'task_completed', { taskId: id });
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// COMP-GSD-7-EVENTLOG: emit a `phase` event the first time a phase is entered.
|
|
832
|
+
// Deduped via ctx.emittedPhases — runState.phase is set to 'execute' before the
|
|
833
|
+
// execute-merge checkpoint runs, so it can't itself gate the emission.
|
|
834
|
+
function emitPhaseOnce(ctx, phase) {
|
|
835
|
+
if (!ctx?.emittedPhases || ctx.emittedPhases.has(phase)) return;
|
|
836
|
+
ctx.emittedPhases.add(phase);
|
|
837
|
+
appendGsdEvent(ctx.cwd, ctx.featureCode, 'phase', { phase });
|
|
838
|
+
}
|
|
839
|
+
|
|
510
840
|
/**
|
|
511
841
|
* Build a GsdStuckDetector from `.compose/compose.json` `gsd.stuck.*`, falling
|
|
512
842
|
* back to documented defaults (sameFileEdits=3, errorRepeats=3,
|
|
@@ -662,17 +992,41 @@ State for resume is in \`pause.json\` (schema: \`contracts/gsd-stuck.json#/defin
|
|
|
662
992
|
*/
|
|
663
993
|
export function loadResumeTaskGraph(cwd, featureCode, { claim = true } = {}) {
|
|
664
994
|
const pausePath = join(gsdDir(cwd, featureCode), 'pause.json');
|
|
665
|
-
if (!existsSync(pausePath)) {
|
|
666
|
-
throw new Error(
|
|
667
|
-
`runGsd: no pause.json to resume for ${featureCode}. ` +
|
|
668
|
-
`Nothing to resume — run \`compose gsd ${featureCode}\` to start fresh.`,
|
|
669
|
-
);
|
|
670
|
-
}
|
|
671
995
|
let pause;
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
996
|
+
if (existsSync(pausePath)) {
|
|
997
|
+
try {
|
|
998
|
+
pause = JSON.parse(readFileSync(pausePath, 'utf-8'));
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
throw new Error(`runGsd: pause.json for ${featureCode} is unreadable: ${err.message}`);
|
|
1001
|
+
}
|
|
1002
|
+
} else {
|
|
1003
|
+
// COMP-GSD-6 crash bridge: a hard crash never reaches the stuck/budget halt
|
|
1004
|
+
// paths that write pause.json. If state.json shows a running run with a DEAD
|
|
1005
|
+
// pid and a populated task graph (resumeReady), synthesize a pause-shaped
|
|
1006
|
+
// object so the unfinished subset can be re-dispatched through the same
|
|
1007
|
+
// guards/filtering below. An EMPTY graph (crashed pre/at decompose) is NOT
|
|
1008
|
+
// resumable here — it (correctly) falls through to the throw; the supervisor
|
|
1009
|
+
// restarts such runs fresh rather than --resume.
|
|
1010
|
+
const state = readGsdState(cwd, featureCode);
|
|
1011
|
+
if (
|
|
1012
|
+
state && state.status === 'running' && !pidAlive(state.pid) &&
|
|
1013
|
+
Array.isArray(state.decomposedTasks) && state.decomposedTasks.length > 0
|
|
1014
|
+
) {
|
|
1015
|
+
pause = {
|
|
1016
|
+
flowId: state.flowId ?? null,
|
|
1017
|
+
stepId: state.lastStepId ?? 'execute',
|
|
1018
|
+
decomposedTasks: state.decomposedTasks,
|
|
1019
|
+
completedTaskIds: state.completedTaskIds ?? [],
|
|
1020
|
+
pid: state.pid,
|
|
1021
|
+
mode: 'gsd',
|
|
1022
|
+
ts: state.heartbeatAt ?? new Date().toISOString(),
|
|
1023
|
+
};
|
|
1024
|
+
} else {
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
`runGsd: no pause.json to resume for ${featureCode}. ` +
|
|
1027
|
+
`Nothing to resume — run \`compose gsd ${featureCode}\` to start fresh.`,
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
676
1030
|
}
|
|
677
1031
|
|
|
678
1032
|
// Mode guard: refuse to resume a non-gsd pause file.
|
|
@@ -687,7 +1041,7 @@ export function loadResumeTaskGraph(cwd, featureCode, { claim = true } = {}) {
|
|
|
687
1041
|
// run still owns this feature (mirrors `compose fix --resume`). We do not
|
|
688
1042
|
// make a self-pid exception: if a live process holds the pause, resuming is
|
|
689
1043
|
// unsafe regardless of whether that pid happens to match ours.
|
|
690
|
-
if (typeof pause.pid === 'number' &&
|
|
1044
|
+
if (typeof pause.pid === 'number' && pidAlive(pause.pid)) {
|
|
691
1045
|
throw new Error(
|
|
692
1046
|
`runGsd: cannot --resume: pid ${pause.pid} still owns this gsd run (process is live). ` +
|
|
693
1047
|
`Wait for it to exit (or remove a stale pause.json) before resuming.`,
|
|
@@ -724,26 +1078,65 @@ export function loadResumeTaskGraph(cwd, featureCode, { claim = true } = {}) {
|
|
|
724
1078
|
/**
|
|
725
1079
|
* Atomic ownership claim (COMP-GSD-5 Codex review, HIGH). `mkdirSync` is an
|
|
726
1080
|
* atomically exclusive create, so two concurrent --resume invocations cannot
|
|
727
|
-
* both claim — the loser gets EEXIST and refuses.
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
1081
|
+
* both claim — the loser gets EEXIST and refuses.
|
|
1082
|
+
*
|
|
1083
|
+
* COMP-GSD-6: a STALE claim left by a crashed --resume is now auto-recovered.
|
|
1084
|
+
* The HOLDER of pause.lock writes its own pid into pause.lock/owner.json (NOT
|
|
1085
|
+
* pause.json.pid, which is the original crashed run's pid — always dead at
|
|
1086
|
+
* resume time and so useless for liveness). Takeover when that holder pid is
|
|
1087
|
+
* dead, OR no owner record exists and the lock-dir mtime is older than the
|
|
1088
|
+
* stale window. TOCTOU-safe: remove + re-attempt the atomic mkdir; a concurrent
|
|
1089
|
+
* winner still wins.
|
|
732
1090
|
*/
|
|
733
1091
|
export function claimResumeLock(cwd, featureCode) {
|
|
734
1092
|
const claimPath = join(gsdDir(cwd, featureCode), 'pause.lock');
|
|
1093
|
+
const writeOwner = () => {
|
|
1094
|
+
try {
|
|
1095
|
+
writeFileSync(
|
|
1096
|
+
join(claimPath, 'owner.json'),
|
|
1097
|
+
JSON.stringify({ pid: process.pid, ts: new Date().toISOString() }, null, 2),
|
|
1098
|
+
);
|
|
1099
|
+
} catch { /* best-effort; mtime fallback still protects takeover */ }
|
|
1100
|
+
};
|
|
735
1101
|
try {
|
|
736
1102
|
mkdirSync(claimPath);
|
|
1103
|
+
writeOwner();
|
|
1104
|
+
return;
|
|
737
1105
|
} catch (err) {
|
|
738
|
-
if (err.code
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1106
|
+
if (err.code !== 'EEXIST') throw err;
|
|
1107
|
+
}
|
|
1108
|
+
// EEXIST — decide stale vs live by the lock HOLDER's own owner record.
|
|
1109
|
+
let holderPid = null;
|
|
1110
|
+
const ownerPath = join(claimPath, 'owner.json');
|
|
1111
|
+
if (existsSync(ownerPath)) {
|
|
1112
|
+
try {
|
|
1113
|
+
const o = JSON.parse(readFileSync(ownerPath, 'utf-8'));
|
|
1114
|
+
if (typeof o.pid === 'number') holderPid = o.pid;
|
|
1115
|
+
} catch { /* fall through to mtime */ }
|
|
1116
|
+
}
|
|
1117
|
+
let stale = false;
|
|
1118
|
+
if (typeof holderPid === 'number') {
|
|
1119
|
+
stale = !pidAlive(holderPid);
|
|
1120
|
+
} else {
|
|
1121
|
+
try {
|
|
1122
|
+
stale = Date.now() - statSync(claimPath).mtimeMs > RUN_LOCK_STALE_MS;
|
|
1123
|
+
} catch { stale = true; }
|
|
1124
|
+
}
|
|
1125
|
+
if (!stale) {
|
|
1126
|
+
throw new Error(
|
|
1127
|
+
`runGsd: a resume claim already exists for ${featureCode} ` +
|
|
1128
|
+
`(.compose/gsd/${featureCode}/pause.lock, pid ${holderPid ?? 'unknown'} alive). ` +
|
|
1129
|
+
`Another --resume may be in progress; if none is, remove that directory to clear a stale claim.`,
|
|
1130
|
+
);
|
|
746
1131
|
}
|
|
1132
|
+
// Atomic stale takeover (rename-aside) — a concurrent reclaimer can't delete
|
|
1133
|
+
// our fresh lock. If we lose the race, refuse.
|
|
1134
|
+
if (!takeoverStaleLock(claimPath)) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
`runGsd: another --resume claimed ${featureCode} during stale-claim takeover; retry.`,
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
writeOwner();
|
|
747
1140
|
}
|
|
748
1141
|
|
|
749
1142
|
/**
|
|
@@ -791,6 +1184,25 @@ function writeBudgetArtifacts(ctx, response, budgetState) {
|
|
|
791
1184
|
writeFileSync(join(dir, 'pause.json'), JSON.stringify(pause, null, 2) + '\n');
|
|
792
1185
|
}
|
|
793
1186
|
|
|
1187
|
+
/**
|
|
1188
|
+
* COMP-GSD-7: on a clean complete, snapshot the run's final budget actuals-vs-caps
|
|
1189
|
+
* to budget-final.json so the milestone report has them retroactively (a clean
|
|
1190
|
+
* complete writes no budget.json — only halts do). Distinct filename from the
|
|
1191
|
+
* halt artifact budget.json (which buildGsdQuery's precedence reads). Atomic write.
|
|
1192
|
+
*/
|
|
1193
|
+
export function writeBudgetFinalSnapshot(ctx, budgetState) {
|
|
1194
|
+
const { cwd, featureCode } = ctx;
|
|
1195
|
+
const dir = gsdDir(cwd, featureCode);
|
|
1196
|
+
mkdirSync(dir, { recursive: true });
|
|
1197
|
+
const decomposedTasks = (ctx.runState?.decomposedTasks ?? []).map((t) => ({ ...t }));
|
|
1198
|
+
const completedTaskIds = collectCompletedTaskIds(cwd, featureCode);
|
|
1199
|
+
const { json } = composeBudgetDiagnostic(budgetState, { feature: featureCode, decomposedTasks, completedTaskIds });
|
|
1200
|
+
const target = join(dir, 'budget-final.json');
|
|
1201
|
+
const tmp = `${target}.tmp`;
|
|
1202
|
+
writeFileSync(tmp, JSON.stringify(json, null, 2) + '\n');
|
|
1203
|
+
renameSync(tmp, target);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
794
1206
|
/**
|
|
795
1207
|
* COMP-GSD-4: append a run's consumed usage to the cumulative ledger. Sourced
|
|
796
1208
|
* from the stratum budget_state.consumed ({tokens,dispatches,wall_s,dollars}).
|
|
@@ -836,17 +1248,6 @@ function writeCumulativeRefusal(cwd, featureCode, chk, limits) {
|
|
|
836
1248
|
writeFileSync(join(dir, 'budget.md'), md);
|
|
837
1249
|
}
|
|
838
1250
|
|
|
839
|
-
function isPidAlive(pid) {
|
|
840
|
-
try {
|
|
841
|
-
// signal 0 probes existence without sending a signal.
|
|
842
|
-
process.kill(pid, 0);
|
|
843
|
-
return true;
|
|
844
|
-
} catch (err) {
|
|
845
|
-
// ESRCH = no such process; EPERM = exists but not ours (still alive).
|
|
846
|
-
return err.code === 'EPERM';
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
1251
|
function clearPauseFile(cwd, featureCode) {
|
|
851
1252
|
const dir = gsdDir(cwd, featureCode);
|
|
852
1253
|
try { rmSync(join(dir, 'pause.json'), { force: true }); } catch { /* best-effort */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9-beta",
|
|
4
4
|
"description": "Structured AI dev pipeline — goal-to-product orchestration with gates, iteration loops, and feature lifecycle management.",
|
|
5
5
|
"author": "SmartMemory",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{ai as o,aj as n}from"./App-D3ehVPvi.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-727SXJPM-fAH0QO9v.js";import{_ as i}from"./App-D3ehVPvi.js";import"./chunk-FMBD7UC4-BnboGO5t.js";import"./chunk-ND2GUHAM-Di9tYXme.js";import"./chunk-55IACEB6-Dsy9RYvI.js";import"./chunk-2J33WTMH-CKk_RN3A.js";import"./mobile-CG5tLa2S.js";import"./index-ClX6LVAf.js";import"./graph-Cs_vqCR0.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-727SXJPM-fAH0QO9v.js";import{_ as i}from"./App-D3ehVPvi.js";import"./chunk-FMBD7UC4-BnboGO5t.js";import"./chunk-ND2GUHAM-Di9tYXme.js";import"./chunk-55IACEB6-Dsy9RYvI.js";import"./chunk-2J33WTMH-CKk_RN3A.js";import"./mobile-CG5tLa2S.js";import"./index-ClX6LVAf.js";import"./graph-Cs_vqCR0.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 r,b as e,a,S as s}from"./chunk-AQP2D5EJ-DyZYerFP.js";import{_ as i}from"./App-D3ehVPvi.js";import"./chunk-55IACEB6-Dsy9RYvI.js";import"./chunk-2J33WTMH-CKk_RN3A.js";import"./mobile-CG5tLa2S.js";import"./index-ClX6LVAf.js";import"./graph-Cs_vqCR0.js";var n={parser:a,get db(){return new s(2)},renderer:e,styles:r,init:i(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{n as diagram};
|