@smartmemory/compose 0.2.8-beta → 0.2.10-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/contracts/par-merge-bounce.json +39 -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 +140 -17
- 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 +504 -49
- package/lib/step-prompt.js +6 -0
- package/lib/stratum-mcp-client.js +3 -1
- package/package.json +1 -1
- package/pipelines/gsd.stratum.yaml +12 -4
- 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/build.js
CHANGED
|
@@ -58,6 +58,8 @@ import { CrossLayerAudit, loadDebugConfig } from './cross-layer-audit.js';
|
|
|
58
58
|
import { emitCheckpoint } from './bug-checkpoint.js';
|
|
59
59
|
import { appendHypothesisEntry, readHypotheses } from './bug-ledger.js';
|
|
60
60
|
import { tier1CodexReview, tier2FreshAgent } from './bug-escalation.js';
|
|
61
|
+
import { recordTaskStates, writeTimingSidecar } from './gsd-timing.js';
|
|
62
|
+
import { writeGsdTaskDiff } from './gsd-diff-capture.js';
|
|
61
63
|
|
|
62
64
|
// ---------------------------------------------------------------------------
|
|
63
65
|
// COMP-FIX-HARD T5: per-step retries cap parser
|
|
@@ -2958,6 +2960,11 @@ export async function executeParallelDispatchServer(
|
|
|
2958
2960
|
// mode invokes this fn with 6 args (opts={}) so every detector branch below
|
|
2959
2961
|
// is skipped and build behavior stays byte-identical.
|
|
2960
2962
|
const stuckDetector = opts.stuckDetector ?? null;
|
|
2963
|
+
// COMP-GSD-6: optional per-task heartbeat. ONLY the gsd headless path passes
|
|
2964
|
+
// one; build mode (opts={}) leaves it null so behavior stays byte-identical.
|
|
2965
|
+
// Bumped from the push-event callback so a long task sitting in the dispatch
|
|
2966
|
+
// poll loop keeps state.json's heartbeat fresh (else it would look crashed).
|
|
2967
|
+
const onHeartbeat = typeof opts.onHeartbeat === 'function' ? opts.onHeartbeat : null;
|
|
2961
2968
|
const startedTasks = new Set();
|
|
2962
2969
|
|
|
2963
2970
|
if (streamWriter) {
|
|
@@ -2989,11 +2996,15 @@ export async function executeParallelDispatchServer(
|
|
|
2989
2996
|
// state-machine driver. Forward valid events through streamWriter so the
|
|
2990
2997
|
// bridge rebroadcasts them via SSE under the buildStreamEvent wrapper.
|
|
2991
2998
|
let unsubscribePush = null;
|
|
2992
|
-
if (typeof stratum.onEvent === 'function' && (streamWriter || stuckDetector)) {
|
|
2999
|
+
if (typeof stratum.onEvent === 'function' && (streamWriter || stuckDetector || onHeartbeat)) {
|
|
2993
3000
|
unsubscribePush = stratum.onEvent(flowId, stepId, (event) => {
|
|
2994
3001
|
// Accept all KNOWN_VERSIONS (producer emits 0.2.6); pinning '0.2.5' dropped
|
|
2995
3002
|
// every push event from the current producer. Client already validated.
|
|
2996
3003
|
if (!event || !KNOWN_VERSIONS.has(event.schema_version)) return;
|
|
3004
|
+
// COMP-GSD-6: any accepted event proves the run is alive — bump heartbeat.
|
|
3005
|
+
if (onHeartbeat) {
|
|
3006
|
+
try { onHeartbeat(event); } catch (err) { console.error('[build] heartbeat hook failed:', err); }
|
|
3007
|
+
}
|
|
2997
3008
|
// COMP-GSD-5: feed the per-task stuck detector (no-op in build mode —
|
|
2998
3009
|
// stuckDetector is null there). record() ignores all but
|
|
2999
3010
|
// tool_use_summary/tool_result and keys by event.task_id.
|
|
@@ -3009,6 +3020,20 @@ export async function executeParallelDispatchServer(
|
|
|
3009
3020
|
});
|
|
3010
3021
|
}
|
|
3011
3022
|
|
|
3023
|
+
// COMP-GSD-7: capture poll-observed per-task timing (gsd path only). Stratum's
|
|
3024
|
+
// poll response carries no per-task timing, so we stamp first-sight / first-
|
|
3025
|
+
// terminal from our own poll loop and persist a timing.json sidecar for the
|
|
3026
|
+
// milestone report. Gated on context.gsd so build mode is byte-identical.
|
|
3027
|
+
const captureTiming = context?.gsd === true && !!context?.featureCode;
|
|
3028
|
+
const taskTiming = {};
|
|
3029
|
+
|
|
3030
|
+
// COMP-PAR-MERGE-QUEUE: hold the merge+advance outcome (and its summary) so the
|
|
3031
|
+
// re-dispatch check can run AFTER the finally (unsubscribe) below, and so the
|
|
3032
|
+
// terminal `build_step_done` event fires only for the FINAL attempt — not for an
|
|
3033
|
+
// attempt that is immediately re-dispatched. Terminal stuck/budget returns inside
|
|
3034
|
+
// the try short-circuit and never set these.
|
|
3035
|
+
let _redispatchOutcome = null;
|
|
3036
|
+
let _finalSummary = null;
|
|
3012
3037
|
try {
|
|
3013
3038
|
// Poll until outcome is present (NOT can_advance — see design §3)
|
|
3014
3039
|
let pollResult;
|
|
@@ -3021,6 +3046,7 @@ export async function executeParallelDispatchServer(
|
|
|
3021
3046
|
`stratum_parallel_poll failed: ${pollResult.error}: ${pollResult.message || ''}`,
|
|
3022
3047
|
);
|
|
3023
3048
|
}
|
|
3049
|
+
if (captureTiming) recordTaskStates(taskTiming, pollResult.tasks, new Date().toISOString());
|
|
3024
3050
|
emitPerTaskProgress(streamWriter, pollResult, emittedStates);
|
|
3025
3051
|
|
|
3026
3052
|
// COMP-GSD-5: real-time stuck detection (gsd path only — null in build).
|
|
@@ -3063,6 +3089,13 @@ export async function executeParallelDispatchServer(
|
|
|
3063
3089
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
3064
3090
|
}
|
|
3065
3091
|
|
|
3092
|
+
// COMP-GSD-7: persist the timing sidecar on every terminal path (complete,
|
|
3093
|
+
// stuck, budget) — best-effort; a write failure must never derail dispatch.
|
|
3094
|
+
if (captureTiming) {
|
|
3095
|
+
try { writeTimingSidecar(baseCwd, context.featureCode, taskTiming); }
|
|
3096
|
+
catch (err) { console.error('[build] gsd timing sidecar write failed:', err.message); }
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3066
3099
|
// COMP-GSD-5: short-circuit return on a stuck verdict. The cancel outcome is
|
|
3067
3100
|
// returned verbatim with the verdict attached; the gsd run loop branches on
|
|
3068
3101
|
// `.stuck`. This bypasses the merge/advance bookkeeping below — there is
|
|
@@ -3134,7 +3167,7 @@ export async function executeParallelDispatchServer(
|
|
|
3134
3167
|
if (hasServerMerge) {
|
|
3135
3168
|
if (pollResult.outcome?.status === 'awaiting_consumer_advance') {
|
|
3136
3169
|
// DEFER PATH: merge locally, report merge_status, let flow advance with truth.
|
|
3137
|
-
const { mergeStatus, conflictedTaskId, conflictError } = applyServerDispatchDiffsCore(
|
|
3170
|
+
const { mergeStatus, conflictedTaskId, conflictError, conflictFiles } = applyServerDispatchDiffsCore(
|
|
3138
3171
|
dispatchResponse.tasks ?? [],
|
|
3139
3172
|
pollResult.tasks,
|
|
3140
3173
|
baseCwd,
|
|
@@ -3152,7 +3185,14 @@ export async function executeParallelDispatchServer(
|
|
|
3152
3185
|
});
|
|
3153
3186
|
}
|
|
3154
3187
|
|
|
3155
|
-
|
|
3188
|
+
// COMP-PAR-MERGE-QUEUE: on conflict, send a structured payload carrying a
|
|
3189
|
+
// merge_conflict bounce record so Stratum surfaces it on ensure_failed and
|
|
3190
|
+
// Compose's retry prompt can name the task/files (back-compat: 'clean'
|
|
3191
|
+
// stays a bare string).
|
|
3192
|
+
const advancePayload = mergeStatus === 'conflict'
|
|
3193
|
+
? { status: 'conflict', bounced_tasks: [buildMergeConflictBounce(conflictedTaskId, conflictError, conflictFiles)] }
|
|
3194
|
+
: 'clean';
|
|
3195
|
+
const advanceResult = await stratum.parallelAdvance(flowId, stepId, advancePayload);
|
|
3156
3196
|
if (advanceResult?.error) {
|
|
3157
3197
|
throw new Error(
|
|
3158
3198
|
`stratum_parallel_advance failed: ${advanceResult.error}: ${advanceResult.message || ''}`,
|
|
@@ -3184,18 +3224,57 @@ export async function executeParallelDispatchServer(
|
|
|
3184
3224
|
}
|
|
3185
3225
|
}
|
|
3186
3226
|
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
});
|
|
3193
|
-
}
|
|
3194
|
-
|
|
3195
|
-
return pollResult.outcome;
|
|
3227
|
+
_redispatchOutcome = pollResult.outcome;
|
|
3228
|
+
_finalSummary = pollResult.summary;
|
|
3229
|
+
// Fall through to the finally (unsubscribe), then the re-dispatch check below.
|
|
3230
|
+
// build_step_done is emitted AFTER that check so a re-dispatched attempt does
|
|
3231
|
+
// not surface a terminal step event.
|
|
3196
3232
|
} finally {
|
|
3197
3233
|
if (unsubscribePush) { try { unsubscribePush(); } catch { /* ignore */ } }
|
|
3198
3234
|
}
|
|
3235
|
+
|
|
3236
|
+
// COMP-PAR-MERGE-QUEUE: a parallel step that failed its pre-merge gate / require
|
|
3237
|
+
// policy / merge returns `ensure_failed`/`schema_failed` carrying the parallel
|
|
3238
|
+
// surface (tasks). Re-dispatch it as a PARALLEL step rather than leaking the
|
|
3239
|
+
// failure to the outer single-agent retry path (which can't re-run parallel
|
|
3240
|
+
// tasks). Stratum reverted current_idx and persisted the bounce onto each
|
|
3241
|
+
// failed task, so the re-run task prompts carry the failure context. The
|
|
3242
|
+
// server's retry cap bounds this — it returns retries_exhausted/error (a
|
|
3243
|
+
// terminal, non-parallel envelope) when exhausted, ending the recursion.
|
|
3244
|
+
const out = _redispatchOutcome;
|
|
3245
|
+
// Defensive cap so a misbehaving server/stub that keeps returning ensure_failed
|
|
3246
|
+
// can't recurse without bound (Stratum's own retry cap should end it first).
|
|
3247
|
+
const redispatchDepth = (opts._redispatchDepth ?? 0);
|
|
3248
|
+
if (
|
|
3249
|
+
out
|
|
3250
|
+
&& (out.status === 'ensure_failed' || out.status === 'schema_failed')
|
|
3251
|
+
&& Array.isArray(out.tasks)
|
|
3252
|
+
&& out.step_id === stepId
|
|
3253
|
+
&& redispatchDepth < 10
|
|
3254
|
+
) {
|
|
3255
|
+
// The ensure_failed envelope is built from Stratum's parallel *surface*, which
|
|
3256
|
+
// omits the merge-config fields (capture_diff/isolation) that gate the
|
|
3257
|
+
// worktree-merge path (hasServerMerge). Carry them over from THIS dispatch (the
|
|
3258
|
+
// same step) so the re-run still merges its diffs instead of silently skipping.
|
|
3259
|
+
const redispatch = {
|
|
3260
|
+
...out,
|
|
3261
|
+
isolation: dispatchResponse.isolation ?? out.isolation,
|
|
3262
|
+
capture_diff: dispatchResponse.capture_diff ?? out.capture_diff,
|
|
3263
|
+
};
|
|
3264
|
+
return await executeParallelDispatchServer(
|
|
3265
|
+
redispatch, stratum, context, progress, streamWriter, baseCwd,
|
|
3266
|
+
{ ...opts, _redispatchDepth: redispatchDepth + 1 },
|
|
3267
|
+
);
|
|
3268
|
+
}
|
|
3269
|
+
// Terminal attempt — now surface the step-done lifecycle event.
|
|
3270
|
+
if (streamWriter) {
|
|
3271
|
+
streamWriter.write({
|
|
3272
|
+
type: 'build_step_done', stepId,
|
|
3273
|
+
parallel: true,
|
|
3274
|
+
summary: _finalSummary, flowId,
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
return out;
|
|
3199
3278
|
}
|
|
3200
3279
|
|
|
3201
3280
|
/**
|
|
@@ -3211,9 +3290,42 @@ export async function executeParallelDispatchServer(
|
|
|
3211
3290
|
* @param {string} patchDir — directory to write temporary .patch files
|
|
3212
3291
|
* @returns {{mergeStatus:'clean'|'conflict', appliedFiles:string[], conflictedTaskId:string|null, conflictError:string|null}}
|
|
3213
3292
|
*/
|
|
3293
|
+
/**
|
|
3294
|
+
* COMP-PAR-MERGE-QUEUE: best-effort extraction of the conflicting files from a
|
|
3295
|
+
* `git apply` error message, falling back to the task's declared owned files.
|
|
3296
|
+
* Used to populate a merge_conflict bounce record's `files`.
|
|
3297
|
+
*/
|
|
3298
|
+
export function extractConflictFiles(conflictError, ownedFiles = []) {
|
|
3299
|
+
const files = new Set();
|
|
3300
|
+
if (conflictError) {
|
|
3301
|
+
// "error: patch failed: path/to/file:LINE"
|
|
3302
|
+
for (const m of conflictError.matchAll(/patch failed:\s+(.+?):\d+/g)) files.add(m[1]);
|
|
3303
|
+
// "error: path/to/file: patch does not apply"
|
|
3304
|
+
for (const m of conflictError.matchAll(/error:\s+(.+?):\s+patch does not apply/g)) files.add(m[1]);
|
|
3305
|
+
}
|
|
3306
|
+
if (files.size === 0) for (const f of (ownedFiles ?? [])) files.add(f);
|
|
3307
|
+
return [...files];
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
/**
|
|
3311
|
+
* COMP-PAR-MERGE-QUEUE: build a structured merge_conflict bounce record (the
|
|
3312
|
+
* Compose-side counterpart of Stratum's gate_failed bounce). Shape matches
|
|
3313
|
+
* contracts/par-merge-bounce.json.
|
|
3314
|
+
*/
|
|
3315
|
+
export function buildMergeConflictBounce(taskId, error, files) {
|
|
3316
|
+
return {
|
|
3317
|
+
task_id: taskId,
|
|
3318
|
+
reason: 'merge_conflict',
|
|
3319
|
+
files: files ?? [],
|
|
3320
|
+
command: null,
|
|
3321
|
+
exit_code: null,
|
|
3322
|
+
excerpt: (error ?? '').slice(-2048),
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3214
3326
|
function applyTaskDiffsToBaseCwd(tasks, diffMap, baseCwd, streamWriter, stepId, patchDir) {
|
|
3215
3327
|
if (diffMap.size === 0) {
|
|
3216
|
-
return { mergeStatus: 'clean', appliedFiles: [], conflictedTaskId: null, conflictError: null };
|
|
3328
|
+
return { mergeStatus: 'clean', appliedFiles: [], conflictedTaskId: null, conflictError: null, conflictFiles: [] };
|
|
3217
3329
|
}
|
|
3218
3330
|
|
|
3219
3331
|
// Topological sort on depends_on edges (DFS)
|
|
@@ -3246,6 +3358,7 @@ function applyTaskDiffsToBaseCwd(tasks, diffMap, baseCwd, streamWriter, stepId,
|
|
|
3246
3358
|
let mergeStatus = 'clean';
|
|
3247
3359
|
let conflictedTaskId = null;
|
|
3248
3360
|
let conflictError = null;
|
|
3361
|
+
let conflictFiles = [];
|
|
3249
3362
|
const appliedFiles = new Set();
|
|
3250
3363
|
|
|
3251
3364
|
for (const taskId of topoOrder) {
|
|
@@ -3274,6 +3387,7 @@ function applyTaskDiffsToBaseCwd(tasks, diffMap, baseCwd, streamWriter, stepId,
|
|
|
3274
3387
|
mergeStatus = 'conflict';
|
|
3275
3388
|
conflictedTaskId = taskId;
|
|
3276
3389
|
conflictError = err.message;
|
|
3390
|
+
conflictFiles = extractConflictFiles(err.message, taskMap.get(taskId)?.files_owned ?? []);
|
|
3277
3391
|
if (streamWriter) {
|
|
3278
3392
|
streamWriter.write({
|
|
3279
3393
|
type: 'build_error',
|
|
@@ -3310,6 +3424,7 @@ function applyTaskDiffsToBaseCwd(tasks, diffMap, baseCwd, streamWriter, stepId,
|
|
|
3310
3424
|
appliedFiles: [...appliedFiles],
|
|
3311
3425
|
conflictedTaskId,
|
|
3312
3426
|
conflictError,
|
|
3427
|
+
conflictFiles,
|
|
3313
3428
|
};
|
|
3314
3429
|
}
|
|
3315
3430
|
|
|
@@ -3340,16 +3455,24 @@ function applyServerDispatchDiffsCore(taskList, pollTasks, baseCwd, streamWriter
|
|
|
3340
3455
|
}
|
|
3341
3456
|
continue;
|
|
3342
3457
|
}
|
|
3343
|
-
if (ts?.diff != null)
|
|
3458
|
+
if (ts?.diff != null) {
|
|
3459
|
+
diffMap.set(taskId, ts.diff);
|
|
3460
|
+
// COMP-GSD-7: snapshot the diff before the worktree is cleaned up, so the
|
|
3461
|
+
// milestone report can inline it. gsd path only (context.gsd); best-effort.
|
|
3462
|
+
if (context?.gsd === true && context?.featureCode) {
|
|
3463
|
+
try { writeGsdTaskDiff(baseCwd, context.featureCode, taskId, ts.diff); }
|
|
3464
|
+
catch (err) { console.error('[build] gsd diff snapshot write failed:', err.message); }
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3344
3467
|
}
|
|
3345
3468
|
|
|
3346
3469
|
if (diffMap.size === 0) {
|
|
3347
|
-
return { mergeStatus: 'clean', conflictedTaskId: null, conflictError: null, appliedFiles: [] };
|
|
3470
|
+
return { mergeStatus: 'clean', conflictedTaskId: null, conflictError: null, appliedFiles: [], conflictFiles: [] };
|
|
3348
3471
|
}
|
|
3349
3472
|
|
|
3350
3473
|
const patchDir = mkdtempSync(join(tmpdir(), 'compose-server-patch-'));
|
|
3351
3474
|
try {
|
|
3352
|
-
const { mergeStatus, conflictedTaskId, conflictError, appliedFiles } =
|
|
3475
|
+
const { mergeStatus, conflictedTaskId, conflictError, appliedFiles, conflictFiles } =
|
|
3353
3476
|
applyTaskDiffsToBaseCwd(taskList, diffMap, baseCwd, streamWriter, stepId, patchDir);
|
|
3354
3477
|
|
|
3355
3478
|
if (mergeStatus !== 'conflict' && appliedFiles.length > 0 && context) {
|
|
@@ -3358,7 +3481,7 @@ function applyServerDispatchDiffsCore(taskList, pollTasks, baseCwd, streamWriter
|
|
|
3358
3481
|
context.filesChanged = [...set];
|
|
3359
3482
|
}
|
|
3360
3483
|
|
|
3361
|
-
return { mergeStatus, conflictedTaskId, conflictError, appliedFiles };
|
|
3484
|
+
return { mergeStatus, conflictedTaskId, conflictError, appliedFiles, conflictFiles };
|
|
3362
3485
|
} finally {
|
|
3363
3486
|
try { rmSync(patchDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
3364
3487
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// lib/gsd-diff-capture.js
|
|
2
|
+
//
|
|
3
|
+
// COMP-GSD-7 S3: persist per-task diff snapshots for the milestone report.
|
|
4
|
+
//
|
|
5
|
+
// When a GSD parallel task runs in a worktree with capture_diff:true, Stratum
|
|
6
|
+
// returns the unified diff in the poll payload (ts.diff). build.js consumes it
|
|
7
|
+
// at the merge site and then drops it once the worktree is cleaned up. This
|
|
8
|
+
// helper snapshots that diff to .compose/gsd/<feature>/diffs/<taskId>.diff so
|
|
9
|
+
// the report (lib/gsd-milestone-report.js) can inline it. The path helper is the
|
|
10
|
+
// single source of truth shared with the report reader.
|
|
11
|
+
//
|
|
12
|
+
// Atomic write: tmp+rename, mirrors lib/gsd-state.js:44.
|
|
13
|
+
|
|
14
|
+
import { writeFileSync, existsSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
export function gsdDiffsDir(cwd, featureCode) {
|
|
18
|
+
return join(cwd, '.compose', 'gsd', featureCode, 'diffs');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function gsdTaskDiffPath(cwd, featureCode, taskId) {
|
|
22
|
+
return join(gsdDiffsDir(cwd, featureCode), `${taskId}.diff`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Atomic write of a task's unified diff. Returns the path. */
|
|
26
|
+
export function writeGsdTaskDiff(cwd, featureCode, taskId, diffText) {
|
|
27
|
+
mkdirSync(gsdDiffsDir(cwd, featureCode), { recursive: true });
|
|
28
|
+
const target = gsdTaskDiffPath(cwd, featureCode, taskId);
|
|
29
|
+
const tmp = `${target}.tmp`;
|
|
30
|
+
if (existsSync(tmp)) { try { unlinkSync(tmp); } catch { /* ignore */ } }
|
|
31
|
+
writeFileSync(tmp, diffText);
|
|
32
|
+
renameSync(tmp, target);
|
|
33
|
+
return target;
|
|
34
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// lib/gsd-events.js
|
|
2
|
+
//
|
|
3
|
+
// COMP-GSD-7-EVENTLOG: append-only GSD run-event log at
|
|
4
|
+
// .compose/gsd/<feature>/events.jsonl — one JSON object per line,
|
|
5
|
+
// { ts, kind, ...detail }. GSD otherwise persists only snapshots, so the
|
|
6
|
+
// milestone report's timeline (COMP-GSD-7) had to reconstruct order from
|
|
7
|
+
// whatever artifacts happened to exist. This log records what happened, in
|
|
8
|
+
// order, across resume sessions; the report consumes it (snapshot fallback).
|
|
9
|
+
//
|
|
10
|
+
// All writes are BEST-EFFORT — an event-log failure must never affect the run.
|
|
11
|
+
// The reader tolerates a torn/corrupt final line (crash mid-append) by skipping
|
|
12
|
+
// unparseable lines.
|
|
13
|
+
//
|
|
14
|
+
// Contract: `detail` must NOT contain `ts` or `kind` keys (the event kind would
|
|
15
|
+
// be clobbered). For a `paused` event the stuck/budget discriminator is
|
|
16
|
+
// `pauseKind`, not `kind`.
|
|
17
|
+
|
|
18
|
+
import { appendFileSync, readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
function gsdDir(cwd, featureCode) {
|
|
22
|
+
return join(cwd, '.compose', 'gsd', featureCode);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function gsdEventsPath(cwd, featureCode) {
|
|
26
|
+
return join(gsdDir(cwd, featureCode), 'events.jsonl');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function appendGsdEvent(cwd, featureCode, kind, detail = {}) {
|
|
30
|
+
try {
|
|
31
|
+
mkdirSync(gsdDir(cwd, featureCode), { recursive: true });
|
|
32
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), kind, ...detail }) + '\n';
|
|
33
|
+
appendFileSync(gsdEventsPath(cwd, featureCode), line);
|
|
34
|
+
} catch { /* best-effort — never affect the run */ }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readGsdEvents(cwd, featureCode) {
|
|
38
|
+
const p = gsdEventsPath(cwd, featureCode);
|
|
39
|
+
if (!existsSync(p)) return [];
|
|
40
|
+
let raw;
|
|
41
|
+
try { raw = readFileSync(p, 'utf-8'); } catch { return []; }
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const line of raw.split('\n')) {
|
|
44
|
+
if (!line.trim()) continue;
|
|
45
|
+
let parsed;
|
|
46
|
+
try { parsed = JSON.parse(line); } catch { continue; /* skip torn/corrupt line */ }
|
|
47
|
+
// Only event OBJECTS are usable — a parseable scalar/array/null is not an
|
|
48
|
+
// event (and would throw in the report's label mapping). Treat as "no event".
|
|
49
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) out.push(parsed);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// COMP-GSD-7-EVENTLOG: fresh-run truncate. Empties the log so a new run's
|
|
55
|
+
// timeline doesn't inherit a prior (abandoned) run's events. Called at the
|
|
56
|
+
// planning checkpoint, AFTER preconditions pass (so a failed fresh invocation
|
|
57
|
+
// doesn't destroy a prior run's history).
|
|
58
|
+
export function clearGsdEvents(cwd, featureCode) {
|
|
59
|
+
const p = gsdEventsPath(cwd, featureCode);
|
|
60
|
+
try { if (existsSync(p)) writeFileSync(p, ''); } catch { /* best-effort */ }
|
|
61
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// lib/gsd-headless-config.js
|
|
2
|
+
//
|
|
3
|
+
// COMP-GSD-6 S05: headless auto-resume policy. Reads `gsd.headless.*` from
|
|
4
|
+
// .compose/compose.json and merges over conservative defaults. Every pause kind
|
|
5
|
+
// is independently overridable (per the product decision): crash and stuck
|
|
6
|
+
// auto-resume by default, budget does NOT (opting in would defeat the GSD-4
|
|
7
|
+
// ceiling), but a user MAY set budget.enabled:true for a fully-unattended burn.
|
|
8
|
+
//
|
|
9
|
+
// Unset config ⇒ defaults ⇒ behavior is a plain run plus supervision.
|
|
10
|
+
|
|
11
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export const HEADLESS_DEFAULTS = Object.freeze({
|
|
15
|
+
autoResume: {
|
|
16
|
+
crash: { enabled: true, maxAttempts: 5 },
|
|
17
|
+
stuck: { enabled: true, maxAttempts: 2 },
|
|
18
|
+
budget: { enabled: false, maxAttempts: 0 },
|
|
19
|
+
// COMP-GSD-6-WATCHDOG: a hung child (heartbeat frozen on a live pid) is
|
|
20
|
+
// killed + resumed like a crash. On by default — that's the whole point of
|
|
21
|
+
// unattended robustness.
|
|
22
|
+
hung: { enabled: true, maxAttempts: 3 },
|
|
23
|
+
},
|
|
24
|
+
backoff: { baseMs: 2000, factor: 2, maxMs: 60000 },
|
|
25
|
+
heartbeatStaleMs: 90000,
|
|
26
|
+
// COMP-GSD-6-WATCHDOG: supervisor poll cadence, SIGTERM→SIGKILL grace, and the
|
|
27
|
+
// child's independent wall-clock heartbeat cadence (must be < heartbeatStaleMs).
|
|
28
|
+
watchdogPollMs: 15000,
|
|
29
|
+
watchdogKillGraceMs: 5000,
|
|
30
|
+
watchdogHeartbeatMs: 30000,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function num(v, fallback) {
|
|
34
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : fallback;
|
|
35
|
+
}
|
|
36
|
+
function bool(v, fallback) {
|
|
37
|
+
return typeof v === 'boolean' ? v : fallback;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mergeKind(override, def) {
|
|
41
|
+
const o = override ?? {};
|
|
42
|
+
return {
|
|
43
|
+
enabled: bool(o.enabled, def.enabled),
|
|
44
|
+
maxAttempts: num(o.maxAttempts, def.maxAttempts),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Merge a raw `gsd.headless` object over HEADLESS_DEFAULTS with per-field type
|
|
49
|
+
// validation (a malformed field falls back to its default, never throws).
|
|
50
|
+
export function resolveHeadlessConfig(raw) {
|
|
51
|
+
const h = raw && typeof raw === 'object' ? raw : {};
|
|
52
|
+
const ar = h.autoResume && typeof h.autoResume === 'object' ? h.autoResume : {};
|
|
53
|
+
const bo = h.backoff && typeof h.backoff === 'object' ? h.backoff : {};
|
|
54
|
+
const d = HEADLESS_DEFAULTS;
|
|
55
|
+
|
|
56
|
+
// COMP-GSD-6-WATCHDOG: enforce the load-bearing invariant
|
|
57
|
+
// `watchdogHeartbeatMs < heartbeatStaleMs`. The child must restamp its heartbeat
|
|
58
|
+
// at least twice within the stale window, or a healthy quiet child trips the
|
|
59
|
+
// watchdog. Two guards:
|
|
60
|
+
// - a degenerate stale window (< 2ms, incl. 0) is unusable — there's no room
|
|
61
|
+
// for a smaller heartbeat — so it falls back to the default.
|
|
62
|
+
// - a heartbeat cadence ≥ the (now positive) stale window is clamped to half
|
|
63
|
+
// it (always strictly < stale for stale ≥ 2), rather than honored.
|
|
64
|
+
let heartbeatStaleMs = num(h.heartbeatStaleMs, d.heartbeatStaleMs);
|
|
65
|
+
if (heartbeatStaleMs < 2) heartbeatStaleMs = d.heartbeatStaleMs;
|
|
66
|
+
let watchdogHeartbeatMs = num(h.watchdogHeartbeatMs, d.watchdogHeartbeatMs);
|
|
67
|
+
if (watchdogHeartbeatMs >= heartbeatStaleMs) {
|
|
68
|
+
watchdogHeartbeatMs = Math.floor(heartbeatStaleMs / 2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
autoResume: {
|
|
73
|
+
crash: mergeKind(ar.crash, d.autoResume.crash),
|
|
74
|
+
stuck: mergeKind(ar.stuck, d.autoResume.stuck),
|
|
75
|
+
budget: mergeKind(ar.budget, d.autoResume.budget),
|
|
76
|
+
hung: mergeKind(ar.hung, d.autoResume.hung),
|
|
77
|
+
},
|
|
78
|
+
backoff: {
|
|
79
|
+
baseMs: num(bo.baseMs, d.backoff.baseMs),
|
|
80
|
+
factor: num(bo.factor, d.backoff.factor),
|
|
81
|
+
maxMs: num(bo.maxMs, d.backoff.maxMs),
|
|
82
|
+
},
|
|
83
|
+
heartbeatStaleMs,
|
|
84
|
+
watchdogPollMs: num(h.watchdogPollMs, d.watchdogPollMs),
|
|
85
|
+
watchdogKillGraceMs: num(h.watchdogKillGraceMs, d.watchdogKillGraceMs),
|
|
86
|
+
watchdogHeartbeatMs,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Read .compose/compose.json gsd.headless.* and resolve against defaults.
|
|
91
|
+
export function readHeadlessConfig(cwd) {
|
|
92
|
+
const configPath = join(cwd, '.compose', 'compose.json');
|
|
93
|
+
let raw = {};
|
|
94
|
+
if (existsSync(configPath)) {
|
|
95
|
+
try {
|
|
96
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
97
|
+
raw = cfg?.gsd?.headless ?? {};
|
|
98
|
+
} catch {
|
|
99
|
+
/* malformed config → defaults */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return resolveHeadlessConfig(raw);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Backoff for attempt N (1-based): base * factor^(N-1), capped at maxMs.
|
|
106
|
+
export function backoffMs(cfg, attempt) {
|
|
107
|
+
const { baseMs, factor, maxMs } = cfg.backoff;
|
|
108
|
+
const raw = baseMs * Math.pow(factor, Math.max(0, attempt - 1));
|
|
109
|
+
return Math.min(raw, maxMs);
|
|
110
|
+
}
|