@smartmemory/compose 0.2.7-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 +112 -3
- package/contracts/gsd-state.json +140 -0
- package/contracts/gsd-stuck.json +141 -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/budget-ledger.js +84 -0
- package/lib/build-stream-schema.js +5 -3
- package/lib/build.js +122 -2
- package/lib/feature-validator.js +40 -8
- package/lib/gsd-budget.js +205 -0
- 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-stuck.js +275 -0
- package/lib/gsd-supervisor.js +223 -0
- package/lib/gsd-timing.js +89 -0
- package/lib/gsd.js +908 -16
- 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
|
@@ -7,15 +7,17 @@
|
|
|
7
7
|
* Design decisions:
|
|
8
8
|
* - Uses AJV (already in compose deps) compiled once at module load.
|
|
9
9
|
* - On validation failure the caller should warn+drop — never throw.
|
|
10
|
-
* - KNOWN_VERSIONS: set of accepted schema_version strings. v0.2.5 accepted
|
|
11
|
-
*
|
|
10
|
+
* - KNOWN_VERSIONS: set of accepted schema_version strings. v0.2.5/v0.2.6 accepted
|
|
11
|
+
* for backward compatibility; v0.2.7 is current (STRAT-PAR-STREAM-TOOLDETAIL:
|
|
12
|
+
* enriched tool_use_summary.input + tool_use_id, new tool_result kind — both
|
|
13
|
+
* ride the open catch-all, so no closed metadata schema is added here).
|
|
12
14
|
* - reply_required (Option A, STRAT-PAR-STREAM-CONSUMER-VALIDATE design):
|
|
13
15
|
* optional boolean reserved for future gate/permission/question kinds.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
17
19
|
|
|
18
|
-
export const KNOWN_VERSIONS = new Set(['0.2.5', '0.2.6']);
|
|
20
|
+
export const KNOWN_VERSIONS = new Set(['0.2.5', '0.2.6', '0.2.7']);
|
|
19
21
|
|
|
20
22
|
// ---------------------------------------------------------------------------
|
|
21
23
|
// Envelope schema (top-level fields only; metadata shape is kind-specific)
|
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
|
|
@@ -2947,12 +2949,24 @@ export async function executeParallelDispatchServer(
|
|
|
2947
2949
|
progress,
|
|
2948
2950
|
streamWriter,
|
|
2949
2951
|
baseCwd,
|
|
2952
|
+
opts = {},
|
|
2950
2953
|
) {
|
|
2951
2954
|
const { flow_id: flowId, step_id: stepId,
|
|
2952
2955
|
step_number: stepNum, total_steps: totalSteps,
|
|
2953
2956
|
tasks } = dispatchResponse;
|
|
2954
2957
|
const emittedStates = new Map();
|
|
2955
2958
|
|
|
2959
|
+
// COMP-GSD-5: optional stuck detector. ONLY the gsd path passes one; build
|
|
2960
|
+
// mode invokes this fn with 6 args (opts={}) so every detector branch below
|
|
2961
|
+
// is skipped and build behavior stays byte-identical.
|
|
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;
|
|
2968
|
+
const startedTasks = new Set();
|
|
2969
|
+
|
|
2956
2970
|
if (streamWriter) {
|
|
2957
2971
|
streamWriter.write({
|
|
2958
2972
|
type: 'build_step_start', stepId,
|
|
@@ -2982,11 +2996,22 @@ export async function executeParallelDispatchServer(
|
|
|
2982
2996
|
// state-machine driver. Forward valid events through streamWriter so the
|
|
2983
2997
|
// bridge rebroadcasts them via SSE under the buildStreamEvent wrapper.
|
|
2984
2998
|
let unsubscribePush = null;
|
|
2985
|
-
if (typeof stratum.onEvent === 'function' && streamWriter) {
|
|
2999
|
+
if (typeof stratum.onEvent === 'function' && (streamWriter || stuckDetector || onHeartbeat)) {
|
|
2986
3000
|
unsubscribePush = stratum.onEvent(flowId, stepId, (event) => {
|
|
2987
3001
|
// Accept all KNOWN_VERSIONS (producer emits 0.2.6); pinning '0.2.5' dropped
|
|
2988
3002
|
// every push event from the current producer. Client already validated.
|
|
2989
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
|
+
}
|
|
3008
|
+
// COMP-GSD-5: feed the per-task stuck detector (no-op in build mode —
|
|
3009
|
+
// stuckDetector is null there). record() ignores all but
|
|
3010
|
+
// tool_use_summary/tool_result and keys by event.task_id.
|
|
3011
|
+
if (stuckDetector && (event.kind === 'tool_use_summary' || event.kind === 'tool_result')) {
|
|
3012
|
+
try { stuckDetector.record(event); } catch (err) { console.error('[build] stuck detector record failed:', err); }
|
|
3013
|
+
}
|
|
3014
|
+
if (!streamWriter) return;
|
|
2990
3015
|
try {
|
|
2991
3016
|
streamWriter.write({ type: 'build_stream_event', event });
|
|
2992
3017
|
} catch (err) {
|
|
@@ -2995,9 +3020,17 @@ export async function executeParallelDispatchServer(
|
|
|
2995
3020
|
});
|
|
2996
3021
|
}
|
|
2997
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
|
+
|
|
2998
3030
|
try {
|
|
2999
3031
|
// Poll until outcome is present (NOT can_advance — see design §3)
|
|
3000
3032
|
let pollResult;
|
|
3033
|
+
let stuckVerdict = null;
|
|
3001
3034
|
const intervalMs = SERVER_DISPATCH_POLL_MS();
|
|
3002
3035
|
while (true) {
|
|
3003
3036
|
pollResult = await stratum.parallelPoll(flowId, stepId);
|
|
@@ -3006,11 +3039,90 @@ export async function executeParallelDispatchServer(
|
|
|
3006
3039
|
`stratum_parallel_poll failed: ${pollResult.error}: ${pollResult.message || ''}`,
|
|
3007
3040
|
);
|
|
3008
3041
|
}
|
|
3042
|
+
if (captureTiming) recordTaskStates(taskTiming, pollResult.tasks, new Date().toISOString());
|
|
3009
3043
|
emitPerTaskProgress(streamWriter, pollResult, emittedStates);
|
|
3044
|
+
|
|
3045
|
+
// COMP-GSD-5: real-time stuck detection (gsd path only — null in build).
|
|
3046
|
+
// For each running task: establish a wall-clock baseline on first sight,
|
|
3047
|
+
// then ask the detector for a verdict. On the first stuck task, cancel the
|
|
3048
|
+
// step via the terminal cascade primitive (parallelAdvance 'conflict' —
|
|
3049
|
+
// the same path T2-F5 uses) and break with a stuck outcome.
|
|
3050
|
+
if (stuckDetector) {
|
|
3051
|
+
const now = Date.now();
|
|
3052
|
+
for (const [taskId, ts] of Object.entries(pollResult.tasks ?? {})) {
|
|
3053
|
+
if (ts.state === 'running') stuckDetector.startTask(taskId, now);
|
|
3054
|
+
if (ts.state !== 'running') continue;
|
|
3055
|
+
const v = stuckDetector.check(taskId, now);
|
|
3056
|
+
if (v?.stuck) {
|
|
3057
|
+
stuckVerdict = {
|
|
3058
|
+
...v,
|
|
3059
|
+
taskId,
|
|
3060
|
+
attemptCounts: stuckDetector.attemptCounts(taskId),
|
|
3061
|
+
};
|
|
3062
|
+
if (streamWriter) {
|
|
3063
|
+
streamWriter.write({
|
|
3064
|
+
type: 'system', subtype: 'gsd_stuck',
|
|
3065
|
+
stepId, taskId, signal: v.signal, detail: v.detail, parallel: true,
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
const cancel = await stratum.parallelAdvance(flowId, stepId, 'conflict');
|
|
3069
|
+
if (cancel?.error) {
|
|
3070
|
+
throw new Error(
|
|
3071
|
+
`stratum_parallel_advance (stuck cancel) failed: ${cancel.error}: ${cancel.message || ''}`,
|
|
3072
|
+
);
|
|
3073
|
+
}
|
|
3074
|
+
pollResult = { ...pollResult, outcome: cancel };
|
|
3075
|
+
break;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
if (stuckVerdict) break;
|
|
3080
|
+
|
|
3010
3081
|
if (pollResult.outcome != null) break;
|
|
3011
3082
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
3012
3083
|
}
|
|
3013
3084
|
|
|
3085
|
+
// COMP-GSD-7: persist the timing sidecar on every terminal path (complete,
|
|
3086
|
+
// stuck, budget) — best-effort; a write failure must never derail dispatch.
|
|
3087
|
+
if (captureTiming) {
|
|
3088
|
+
try { writeTimingSidecar(baseCwd, context.featureCode, taskTiming); }
|
|
3089
|
+
catch (err) { console.error('[build] gsd timing sidecar write failed:', err.message); }
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
// COMP-GSD-5: short-circuit return on a stuck verdict. The cancel outcome is
|
|
3093
|
+
// returned verbatim with the verdict attached; the gsd run loop branches on
|
|
3094
|
+
// `.stuck`. This bypasses the merge/advance bookkeeping below — there is
|
|
3095
|
+
// nothing to merge for a cancelled task.
|
|
3096
|
+
if (stuckVerdict) {
|
|
3097
|
+
if (streamWriter) {
|
|
3098
|
+
streamWriter.write({
|
|
3099
|
+
type: 'build_step_done', stepId, parallel: true,
|
|
3100
|
+
summary: { ...(pollResult.summary ?? {}), stuck: stuckVerdict.signal }, flowId,
|
|
3101
|
+
});
|
|
3102
|
+
}
|
|
3103
|
+
// unsubscribePush is invoked by the enclosing finally.
|
|
3104
|
+
return { ...(pollResult.outcome ?? {}), stuck: stuckVerdict };
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// COMP-GSD-4: budget_exhausted is a stratum terminal status. When the run
|
|
3108
|
+
// budget trips mid-dispatch, the flow has already cascade-cancelled the
|
|
3109
|
+
// in-flight siblings (server-side) and the poll returns the terminal
|
|
3110
|
+
// envelope (carrying budget_state). There is nothing to merge — short-circuit
|
|
3111
|
+
// like stuck and hand the envelope back so the gsd run loop halts. No-op for
|
|
3112
|
+
// build mode: build flows declare no `budget:` block, so the status never
|
|
3113
|
+
// appears and this branch is never taken (byte-identical). The
|
|
3114
|
+
// advance-carried case (a parallelAdvance that returns budget_exhausted)
|
|
3115
|
+
// falls through to the final `return pollResult.outcome` below.
|
|
3116
|
+
if (pollResult.outcome?.status === 'budget_exhausted') {
|
|
3117
|
+
if (streamWriter) {
|
|
3118
|
+
streamWriter.write({
|
|
3119
|
+
type: 'build_step_done', stepId, parallel: true,
|
|
3120
|
+
summary: { ...(pollResult.summary ?? {}), budget_exhausted: true }, flowId,
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
return pollResult.outcome;
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3014
3126
|
if (pollResult.outcome.status === 'already_advanced') {
|
|
3015
3127
|
throw new Error(
|
|
3016
3128
|
`stratum_parallel_poll returned already_advanced for step ${stepId} — ` +
|
|
@@ -3254,7 +3366,15 @@ function applyServerDispatchDiffsCore(taskList, pollTasks, baseCwd, streamWriter
|
|
|
3254
3366
|
}
|
|
3255
3367
|
continue;
|
|
3256
3368
|
}
|
|
3257
|
-
if (ts?.diff != null)
|
|
3369
|
+
if (ts?.diff != null) {
|
|
3370
|
+
diffMap.set(taskId, ts.diff);
|
|
3371
|
+
// COMP-GSD-7: snapshot the diff before the worktree is cleaned up, so the
|
|
3372
|
+
// milestone report can inline it. gsd path only (context.gsd); best-effort.
|
|
3373
|
+
if (context?.gsd === true && context?.featureCode) {
|
|
3374
|
+
try { writeGsdTaskDiff(baseCwd, context.featureCode, taskId, ts.diff); }
|
|
3375
|
+
catch (err) { console.error('[build] gsd diff snapshot write failed:', err.message); }
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3258
3378
|
}
|
|
3259
3379
|
|
|
3260
3380
|
if (diffMap.size === 0) {
|
package/lib/feature-validator.js
CHANGED
|
@@ -385,6 +385,17 @@ function normalizeStatus(s) {
|
|
|
385
385
|
return String(s).toUpperCase();
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
// vision-state's status vocabulary (contracts/vision-state.schema.json) is the
|
|
389
|
+
// tracker's set MINUS `PARTIAL` — it cannot represent "partially shipped". A
|
|
390
|
+
// tracker status of PARTIAL is the same lifecycle reality as vision's
|
|
391
|
+
// IN_PROGRESS (partially shipped = still in progress), so project the tracker
|
|
392
|
+
// side onto the vision vocabulary before any *_VS_VISION_STATE comparison.
|
|
393
|
+
// Tracker↔tracker comparisons (ROADMAP_VS_FEATUREJSON) keep the full
|
|
394
|
+
// vocabulary — PARTIAL vs IN_PROGRESS there is a real distinction.
|
|
395
|
+
function projectToVisionStatus(s) {
|
|
396
|
+
return s === 'PARTIAL' ? 'IN_PROGRESS' : s;
|
|
397
|
+
}
|
|
398
|
+
|
|
388
399
|
function runStateMismatchChecks(fctx, findings) {
|
|
389
400
|
const { code, roadmap, vision, featureJson } = fctx;
|
|
390
401
|
const rStatus = normalizeStatus(roadmap?.status);
|
|
@@ -401,22 +412,43 @@ function runStateMismatchChecks(fctx, findings) {
|
|
|
401
412
|
'STATUS_MISMATCH_ROADMAP_VS_FEATUREJSON', code,
|
|
402
413
|
`ROADMAP says ${rStatus}, feature.json says ${fStatus}`));
|
|
403
414
|
}
|
|
404
|
-
|
|
405
|
-
|
|
415
|
+
// Project BOTH sides to the vision vocabulary (PARTIAL→IN_PROGRESS) before
|
|
416
|
+
// comparing. Projecting the tracker side stops a legitimately-PARTIAL feature
|
|
417
|
+
// false-firing against a vision item that can only say in_progress; projecting
|
|
418
|
+
// the vision side keeps it symmetric so a malformed/legacy vision status of
|
|
419
|
+
// "partial" (schema-invalid — reported as VISION_STATE_SCHEMA_VIOLATION) still
|
|
420
|
+
// aligns with tracker PARTIAL instead of double-reporting. Real drift (PARTIAL
|
|
421
|
+
// vs complete/planned) still differs and fires.
|
|
422
|
+
const rVis = projectToVisionStatus(rStatus);
|
|
423
|
+
const fVis = projectToVisionStatus(fStatus);
|
|
424
|
+
const vVis = projectToVisionStatus(vStatus);
|
|
425
|
+
if (rStatus && vStatus && rVis !== vVis) {
|
|
426
|
+
findings.push(finding(statusSeverity(rVis, vVis),
|
|
406
427
|
'STATUS_MISMATCH_ROADMAP_VS_VISION_STATE', code,
|
|
407
428
|
`ROADMAP says ${rStatus}, vision-state says ${vStatus}`));
|
|
408
429
|
}
|
|
409
|
-
if (fStatus && vStatus &&
|
|
410
|
-
findings.push(finding(statusSeverity(
|
|
430
|
+
if (fStatus && vStatus && fVis !== vVis) {
|
|
431
|
+
findings.push(finding(statusSeverity(fVis, vVis),
|
|
411
432
|
'STATUS_MISMATCH_FEATUREJSON_VS_VISION_STATE', code,
|
|
412
433
|
`feature.json says ${fStatus}, vision-state says ${vStatus}`));
|
|
413
434
|
}
|
|
414
|
-
// CONTRADICTORY_PHASE_CLAIM
|
|
415
|
-
|
|
416
|
-
|
|
435
|
+
// CONTRADICTORY_PHASE_CLAIM — compare LIFECYCLE phase to LIFECYCLE phase.
|
|
436
|
+
// feature.json's top-level `phase` holds the ROADMAP heading (e.g. "Phase 7:
|
|
437
|
+
// MCP Writers"), NOT a lifecycle stage; comparing it to vision-state's
|
|
438
|
+
// lifecycle phase ("vision"/"explore_design"/…) is a category mismatch that
|
|
439
|
+
// false-fired on ~every feature with a vision item. Use only the lifecycle
|
|
440
|
+
// sources on both sides — which is what the "does not involve the roadmap"
|
|
441
|
+
// comment above always intended. (feature.json doesn't currently carry a
|
|
442
|
+
// lifecycle phase, so this correctly yields no finding until it does.)
|
|
443
|
+
const fPhase = featureJson?.lifecycle?.currentPhase;
|
|
444
|
+
// Lifecycle phase ONLY on both sides. Do NOT fall back to vision.phase — that
|
|
445
|
+
// is the legacy board-column taxonomy (planning|implementation|…), a different
|
|
446
|
+
// vocabulary from lifecycle.currentPhase (explore_design|blueprint|…), and
|
|
447
|
+
// mixing them reintroduces the category mismatch this fix removes (Codex).
|
|
448
|
+
const vPhase = vision?.lifecycle?.currentPhase;
|
|
417
449
|
if (fPhase && vPhase && fPhase !== vPhase) {
|
|
418
450
|
findings.push(finding('error', 'CONTRADICTORY_PHASE_CLAIM', code,
|
|
419
|
-
`feature.json phase '${fPhase}' vs vision-state phase '${vPhase}'`));
|
|
451
|
+
`feature.json lifecycle phase '${fPhase}' vs vision-state phase '${vPhase}'`));
|
|
420
452
|
}
|
|
421
453
|
// COMPLEXITY_OR_DESCRIPTION_DRIFT
|
|
422
454
|
if (roadmap && featureJson) {
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gsd-budget.js — COMP-GSD-4 budget-ceiling helpers for autonomous `compose gsd`.
|
|
3
|
+
*
|
|
4
|
+
* This module does NOT count tokens or enforce budgets — that is the stratum
|
|
5
|
+
* flow budget (STRAT-WORKFLOW-BUDGET): a `budget:` block on the flow makes
|
|
6
|
+
* stratum debit every server-dispatched agent and halt the run with a terminal
|
|
7
|
+
* `budget_exhausted` status that carries `budget_state = {caps, consumed}`.
|
|
8
|
+
*
|
|
9
|
+
* GSD-4's job is purely compose-side glue:
|
|
10
|
+
* - readGsdBudgetConfig: read `.compose/compose.json` `gsd.budget.*` (no defaults).
|
|
11
|
+
* - buildBudgetBlock: map that config → the stratum flow `budget` block
|
|
12
|
+
* (+ a per-task task_timeout in seconds).
|
|
13
|
+
* - injectBudget: inject the block into the gsd spec YAML — IDENTITY
|
|
14
|
+
* when nothing is configured (byte-identical guarantee).
|
|
15
|
+
* - composeBudgetDiagnostic: render budget.json + budget.md from budget_state.
|
|
16
|
+
*
|
|
17
|
+
* Enforced axes (stratum): ms (wall-clock), max_agent_dispatches, max_tokens, usd.
|
|
18
|
+
* See: docs/features/COMP-GSD-4/{design,blueprint}.md, stratum run_budget.py.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import YAML from 'yaml';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read `.compose/compose.json` → `gsd.budget`. Returns {} when absent or
|
|
27
|
+
* unparseable. NO defaults (gate decision 7): a gsd run is unbounded unless the
|
|
28
|
+
* user sets a budget. Mirrors readGsdStuckConfig in gsd.js.
|
|
29
|
+
*/
|
|
30
|
+
export function readGsdBudgetConfig(cwd) {
|
|
31
|
+
const configPath = join(cwd, '.compose', 'compose.json');
|
|
32
|
+
if (!existsSync(configPath)) return {};
|
|
33
|
+
try {
|
|
34
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
35
|
+
return cfg?.gsd?.budget ?? {};
|
|
36
|
+
} catch {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Map snake_case `gsd.budget.*` config → the stratum flow budget block and an
|
|
43
|
+
* optional per-task timeout (seconds). Only keys the user set appear.
|
|
44
|
+
*
|
|
45
|
+
* Config keys:
|
|
46
|
+
* max_tokens, max_agent_dispatches, usd → flow budget axes
|
|
47
|
+
* per_run_ms (alias: ms) → flow budget `ms` (wall-clock)
|
|
48
|
+
* per_task_ms → execute step `task_timeout` (sec)
|
|
49
|
+
* cumulative: { max_total_tokens, max_total_cost_usd } → cross-session ceiling
|
|
50
|
+
*
|
|
51
|
+
* @returns {{ budget?: object, taskTimeoutSec?: number, cumulative?: object }}
|
|
52
|
+
*/
|
|
53
|
+
export function buildBudgetBlock(cfg = {}) {
|
|
54
|
+
const out = {};
|
|
55
|
+
|
|
56
|
+
const budget = {};
|
|
57
|
+
if (cfg.max_tokens != null) budget.max_tokens = cfg.max_tokens;
|
|
58
|
+
if (cfg.max_agent_dispatches != null) budget.max_agent_dispatches = cfg.max_agent_dispatches;
|
|
59
|
+
if (cfg.usd != null) budget.usd = cfg.usd;
|
|
60
|
+
const ms = cfg.per_run_ms ?? cfg.ms;
|
|
61
|
+
if (ms != null) budget.ms = ms;
|
|
62
|
+
if (Object.keys(budget).length > 0) out.budget = budget;
|
|
63
|
+
|
|
64
|
+
if (cfg.per_task_ms != null) {
|
|
65
|
+
// stratum parallel_dispatch per-task timeout is `task_timeout` in SECONDS
|
|
66
|
+
// (spec.py:145, schema minimum 1). Convert from ms, floor at 1s.
|
|
67
|
+
out.taskTimeoutSec = Math.max(1, Math.ceil(cfg.per_task_ms / 1000));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (cfg.cumulative && typeof cfg.cumulative === 'object') {
|
|
71
|
+
const cum = {};
|
|
72
|
+
if (cfg.cumulative.max_total_tokens != null) cum.maxTotalTokens = cfg.cumulative.max_total_tokens;
|
|
73
|
+
if (cfg.cumulative.max_total_cost_usd != null) cum.maxTotalCostUsd = cfg.cumulative.max_total_cost_usd;
|
|
74
|
+
if (Object.keys(cum).length > 0) out.cumulative = cum;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Inject the budget block into the gsd flow spec YAML.
|
|
82
|
+
*
|
|
83
|
+
* BYTE-IDENTICAL GUARANTEE: when nothing is configured (no flow budget AND no
|
|
84
|
+
* per-task timeout), the original `specYaml` string is returned VERBATIM — no
|
|
85
|
+
* YAML.parse/stringify round-trip (which would reorder/reformat). This keeps an
|
|
86
|
+
* un-budgeted `compose gsd` (and plain `compose build`) bit-for-bit unchanged.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} specYaml — the gsd.stratum.yaml contents
|
|
89
|
+
* @param {object} cfg — raw gsd.budget config (from readGsdBudgetConfig)
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
export function injectBudget(specYaml, cfg = {}) {
|
|
93
|
+
const built = buildBudgetBlock(cfg);
|
|
94
|
+
if (!built.budget && built.taskTimeoutSec == null) {
|
|
95
|
+
return specYaml; // identity — nothing to inject
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const parsed = YAML.parse(specYaml);
|
|
99
|
+
const flow = parsed?.flows?.gsd;
|
|
100
|
+
if (!flow) {
|
|
101
|
+
// Defensive: spec shape changed. Don't silently drop the budget — surface it.
|
|
102
|
+
throw new Error('injectBudget: spec has no flows.gsd to attach a budget to');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (built.budget) flow.budget = built.budget;
|
|
106
|
+
|
|
107
|
+
if (built.taskTimeoutSec != null && Array.isArray(flow.steps)) {
|
|
108
|
+
const execute = flow.steps.find((s) => s && s.id === 'execute');
|
|
109
|
+
if (execute) execute.task_timeout = built.taskTimeoutSec;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return YAML.stringify(parsed);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Maps a stratum budget axis → human label for diagnostics.
|
|
116
|
+
const AXIS_LABEL = {
|
|
117
|
+
max_tokens: 'tokens',
|
|
118
|
+
max_agent_dispatches: 'agent dispatches',
|
|
119
|
+
ms: 'wall-clock',
|
|
120
|
+
usd: 'cost (USD)',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Identify which enforced axis tripped, comparing consumed vs caps.
|
|
125
|
+
* Mirrors stratum run_budget.budget_exhausted() (consumed >= cap), in the same
|
|
126
|
+
* precedence order. Returns null if nothing is over (shouldn't happen on a
|
|
127
|
+
* budget_exhausted terminal, but the diagnostic stays honest).
|
|
128
|
+
*/
|
|
129
|
+
export function trippedAxis(budgetState) {
|
|
130
|
+
const caps = budgetState?.caps ?? {};
|
|
131
|
+
const consumed = budgetState?.consumed ?? {};
|
|
132
|
+
if (caps.ms != null && (consumed.wall_s ?? 0) >= caps.ms / 1000) return 'ms';
|
|
133
|
+
if (caps.max_agent_dispatches != null && (consumed.dispatches ?? 0) >= caps.max_agent_dispatches) return 'max_agent_dispatches';
|
|
134
|
+
if (caps.max_tokens != null && (consumed.tokens ?? 0) >= caps.max_tokens) return 'max_tokens';
|
|
135
|
+
if (caps.usd != null && (consumed.dollars ?? 0) >= caps.usd) return 'usd';
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Build the budget.json + budget.md diagnostic from the stratum terminal
|
|
141
|
+
* envelope's budget_state.
|
|
142
|
+
*
|
|
143
|
+
* @param {object} budgetState — {caps, consumed:{tokens,dispatches,wall_s,dollars}}
|
|
144
|
+
* @param {{feature:string, decomposedTasks?:Array, completedTaskIds?:Array, cumulative?:object}} meta
|
|
145
|
+
* @returns {{ json: object, md: string }}
|
|
146
|
+
*/
|
|
147
|
+
export function composeBudgetDiagnostic(budgetState, meta = {}) {
|
|
148
|
+
const caps = budgetState?.caps ?? {};
|
|
149
|
+
const consumed = budgetState?.consumed ?? {};
|
|
150
|
+
const axis = meta.axis ?? trippedAxis(budgetState);
|
|
151
|
+
const feature = meta.feature ?? '';
|
|
152
|
+
|
|
153
|
+
const completed = new Set(meta.completedTaskIds ?? []);
|
|
154
|
+
const remaining = (meta.decomposedTasks ?? [])
|
|
155
|
+
.map((t) => t.id)
|
|
156
|
+
.filter((id) => id && !completed.has(id));
|
|
157
|
+
|
|
158
|
+
const json = {
|
|
159
|
+
feature,
|
|
160
|
+
kind: 'budget',
|
|
161
|
+
axis,
|
|
162
|
+
caps,
|
|
163
|
+
consumed,
|
|
164
|
+
remainingTaskIds: remaining,
|
|
165
|
+
ts: new Date().toISOString(),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const rows = [];
|
|
169
|
+
if (caps.max_tokens != null) rows.push(`| tokens | ${consumed.tokens ?? 0} | ${caps.max_tokens} |`);
|
|
170
|
+
if (caps.max_agent_dispatches != null) rows.push(`| agent dispatches | ${consumed.dispatches ?? 0} | ${caps.max_agent_dispatches} |`);
|
|
171
|
+
if (caps.ms != null) rows.push(`| wall-clock (s) | ${Math.round(consumed.wall_s ?? 0)} | ${Math.round(caps.ms / 1000)} |`);
|
|
172
|
+
if (caps.usd != null) rows.push(`| cost (USD) | ${(consumed.dollars ?? 0).toFixed(4)} | ${Number(caps.usd).toFixed(4)} |`);
|
|
173
|
+
|
|
174
|
+
const md = [
|
|
175
|
+
`# GSD budget halt — ${feature}`,
|
|
176
|
+
'',
|
|
177
|
+
`**Tripped axis:** ${AXIS_LABEL[axis] ?? axis ?? 'cumulative'}`,
|
|
178
|
+
`**When:** ${json.ts}`,
|
|
179
|
+
'',
|
|
180
|
+
'## Consumed vs cap',
|
|
181
|
+
'',
|
|
182
|
+
'| Axis | Consumed | Cap |',
|
|
183
|
+
'|------|----------|-----|',
|
|
184
|
+
...rows,
|
|
185
|
+
'',
|
|
186
|
+
`## Remaining tasks (${remaining.length})`,
|
|
187
|
+
'',
|
|
188
|
+
remaining.length ? remaining.map((id) => `- ${id}`).join('\n') : '_none — all tasks completed before the halt._',
|
|
189
|
+
'',
|
|
190
|
+
'## Resume',
|
|
191
|
+
'',
|
|
192
|
+
'Raise the relevant `gsd.budget.*` cap in `.compose/compose.json` (or run with',
|
|
193
|
+
'`--reset-budget` to clear the cumulative ledger), then:',
|
|
194
|
+
'',
|
|
195
|
+
'```',
|
|
196
|
+
`compose gsd ${feature} --resume`,
|
|
197
|
+
'```',
|
|
198
|
+
'',
|
|
199
|
+
'Completed task results are preserved in the blackboard; --resume re-dispatches',
|
|
200
|
+
'only the remaining tasks.',
|
|
201
|
+
'',
|
|
202
|
+
].join('\n');
|
|
203
|
+
|
|
204
|
+
return { json, md };
|
|
205
|
+
}
|
|
@@ -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
|
+
}
|