gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216
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/dist/bundled-resource-path.d.ts +8 -0
- package/dist/bundled-resource-path.js +14 -0
- package/dist/headless-query.js +6 -6
- package/dist/resources/extensions/gsd/auto/session.js +27 -32
- package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
- package/dist/resources/extensions/gsd/auto-loop.js +956 -0
- package/dist/resources/extensions/gsd/auto-observability.js +4 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
- package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
- package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
- package/dist/resources/extensions/gsd/auto-start.js +330 -309
- package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
- package/dist/resources/extensions/gsd/auto-timers.js +3 -4
- package/dist/resources/extensions/gsd/auto-verification.js +35 -73
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
- package/dist/resources/extensions/gsd/auto.js +283 -1013
- package/dist/resources/extensions/gsd/captures.js +10 -4
- package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
- package/dist/resources/extensions/gsd/git-service.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +296 -151
- package/dist/resources/extensions/gsd/index.js +92 -228
- package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
- package/dist/resources/extensions/gsd/progress-score.js +61 -156
- package/dist/resources/extensions/gsd/quick.js +98 -122
- package/dist/resources/extensions/gsd/session-lock.js +13 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/undo.js +43 -48
- package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
- package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
- package/dist/resources/extensions/gsd/verification-gate.js +6 -35
- package/dist/resources/extensions/gsd/worktree-command.js +30 -24
- package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
- package/dist/resources/extensions/gsd/worktree.js +7 -44
- package/dist/tool-bootstrap.js +59 -11
- package/dist/worktree-cli.js +7 -7
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +735 -2588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/src/models.generated.ts +1039 -2892
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +47 -30
- package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
- package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
- package/src/resources/extensions/gsd/auto-observability.ts +4 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
- package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
- package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
- package/src/resources/extensions/gsd/auto-start.ts +440 -354
- package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
- package/src/resources/extensions/gsd/auto-timers.ts +3 -4
- package/src/resources/extensions/gsd/auto-verification.ts +76 -90
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
- package/src/resources/extensions/gsd/auto.ts +515 -1199
- package/src/resources/extensions/gsd/captures.ts +10 -4
- package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
- package/src/resources/extensions/gsd/git-service.ts +8 -1
- package/src/resources/extensions/gsd/gitignore.ts +4 -2
- package/src/resources/extensions/gsd/gsd-db.ts +375 -180
- package/src/resources/extensions/gsd/index.ts +104 -263
- package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
- package/src/resources/extensions/gsd/progress-score.ts +65 -200
- package/src/resources/extensions/gsd/quick.ts +121 -125
- package/src/resources/extensions/gsd/session-lock.ts +11 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
- package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
- package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
- package/src/resources/extensions/gsd/types.ts +90 -81
- package/src/resources/extensions/gsd/undo.ts +42 -46
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
- package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
- package/src/resources/extensions/gsd/verification-gate.ts +6 -39
- package/src/resources/extensions/gsd/worktree-command.ts +36 -24
- package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
- package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
- package/src/resources/extensions/gsd/worktree.ts +7 -44
- package/dist/resources/extensions/gsd/auto-constants.js +0 -5
- package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
- package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
- package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
- package/src/resources/extensions/gsd/auto-constants.ts +0 -6
- package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
- package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
- package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
- package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stuck detection and loop recovery for auto-mode unit dispatch.
|
|
3
|
-
*
|
|
4
|
-
* Tracks dispatch counts per unit, enforces lifetime caps, and attempts
|
|
5
|
-
* stub/artifact recovery before stopping.
|
|
6
|
-
*
|
|
7
|
-
* Extracted from dispatchNextUnit() in auto.ts. Returns action values
|
|
8
|
-
* instead of calling stopAuto/dispatchNextUnit — the caller handles
|
|
9
|
-
* control flow.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
|
13
|
-
import {
|
|
14
|
-
inspectExecuteTaskDurability,
|
|
15
|
-
} from "./unit-runtime.js";
|
|
16
|
-
import {
|
|
17
|
-
verifyExpectedArtifact,
|
|
18
|
-
diagnoseExpectedArtifact,
|
|
19
|
-
skipExecuteTask,
|
|
20
|
-
persistCompletedKey,
|
|
21
|
-
buildLoopRemediationSteps,
|
|
22
|
-
} from "./auto-recovery.js";
|
|
23
|
-
import { closeoutUnit, type CloseoutOptions } from "./auto-unit-closeout.js";
|
|
24
|
-
import { saveActivityLog } from "./activity-log.js";
|
|
25
|
-
import { invalidateAllCaches } from "./cache.js";
|
|
26
|
-
import { sendDesktopNotification } from "./notifications.js";
|
|
27
|
-
import { debugLog } from "./debug-logger.js";
|
|
28
|
-
import {
|
|
29
|
-
resolveMilestonePath,
|
|
30
|
-
resolveSlicePath,
|
|
31
|
-
resolveTasksDir,
|
|
32
|
-
buildTaskFileName,
|
|
33
|
-
} from "./paths.js";
|
|
34
|
-
import {
|
|
35
|
-
MAX_UNIT_DISPATCHES,
|
|
36
|
-
STUB_RECOVERY_THRESHOLD,
|
|
37
|
-
MAX_LIFETIME_DISPATCHES,
|
|
38
|
-
} from "./auto/session.js";
|
|
39
|
-
import type { AutoSession } from "./auto/session.js";
|
|
40
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
41
|
-
import { join } from "node:path";
|
|
42
|
-
import { parseUnitId } from "./unit-id.js";
|
|
43
|
-
|
|
44
|
-
export interface StuckContext {
|
|
45
|
-
s: AutoSession;
|
|
46
|
-
ctx: ExtensionContext;
|
|
47
|
-
unitType: string;
|
|
48
|
-
unitId: string;
|
|
49
|
-
basePath: string;
|
|
50
|
-
buildSnapshotOpts: () => CloseoutOptions & Record<string, unknown>;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export type StuckResult =
|
|
54
|
-
| { action: "proceed" }
|
|
55
|
-
| { action: "recovered"; dispatchAgain: true }
|
|
56
|
-
| { action: "stop"; reason: string; notifyMessage?: string };
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Check dispatch counts, enforce lifetime cap and MAX_UNIT_DISPATCHES,
|
|
60
|
-
* attempt stub/artifact recovery. Returns an action for the caller.
|
|
61
|
-
*/
|
|
62
|
-
export async function checkStuckAndRecover(sctx: StuckContext): Promise<StuckResult> {
|
|
63
|
-
const { s, ctx, unitType, unitId, basePath, buildSnapshotOpts } = sctx;
|
|
64
|
-
const dispatchKey = `${unitType}/${unitId}`;
|
|
65
|
-
const prevCount = s.unitDispatchCount.get(dispatchKey) ?? 0;
|
|
66
|
-
|
|
67
|
-
// Real dispatch reached — clear the consecutive-skip counter for this unit.
|
|
68
|
-
s.unitConsecutiveSkips.delete(dispatchKey);
|
|
69
|
-
|
|
70
|
-
debugLog("dispatch-unit", {
|
|
71
|
-
type: unitType,
|
|
72
|
-
id: unitId,
|
|
73
|
-
cycle: prevCount + 1,
|
|
74
|
-
lifetime: (s.unitLifetimeDispatches.get(dispatchKey) ?? 0) + 1,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Hard lifetime cap — survives counter resets from loop-recovery/self-repair.
|
|
78
|
-
const lifetimeCount = (s.unitLifetimeDispatches.get(dispatchKey) ?? 0) + 1;
|
|
79
|
-
s.unitLifetimeDispatches.set(dispatchKey, lifetimeCount);
|
|
80
|
-
if (lifetimeCount > MAX_LIFETIME_DISPATCHES) {
|
|
81
|
-
if (s.currentUnit) {
|
|
82
|
-
await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts());
|
|
83
|
-
} else {
|
|
84
|
-
saveActivityLog(ctx, s.basePath, unitType, unitId);
|
|
85
|
-
}
|
|
86
|
-
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
87
|
-
return {
|
|
88
|
-
action: "stop",
|
|
89
|
-
reason: `Hard loop: ${unitType} ${unitId}`,
|
|
90
|
-
notifyMessage: `Hard loop detected: ${unitType} ${unitId} dispatched ${lifetimeCount} times total (across reconciliation cycles).${expected ? `\n Expected artifact: ${expected}` : ""}\n This may indicate deriveState() keeps returning the same unit despite artifacts existing.\n Check .gsd/completed-units.json and the slice plan checkbox state.`,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (prevCount >= MAX_UNIT_DISPATCHES) {
|
|
95
|
-
if (s.currentUnit) {
|
|
96
|
-
await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts());
|
|
97
|
-
} else {
|
|
98
|
-
saveActivityLog(ctx, s.basePath, unitType, unitId);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Final reconciliation pass for execute-task
|
|
102
|
-
if (unitType === "execute-task") {
|
|
103
|
-
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
104
|
-
if (mid && sid && tid) {
|
|
105
|
-
const status = await inspectExecuteTaskDurability(basePath, unitId);
|
|
106
|
-
if (status) {
|
|
107
|
-
const reconciled = skipExecuteTask(basePath, mid, sid, tid, status, "loop-recovery", prevCount);
|
|
108
|
-
if (reconciled && verifyExpectedArtifact(unitType, unitId, basePath)) {
|
|
109
|
-
ctx.ui.notify(
|
|
110
|
-
`Loop recovery: ${unitId} reconciled after ${prevCount + 1} dispatches — blocker artifacts written, pipeline advancing.\n Review ${status.summaryPath} and replace the placeholder with real work.`,
|
|
111
|
-
"warning",
|
|
112
|
-
);
|
|
113
|
-
const reconciledKey = `${unitType}/${unitId}`;
|
|
114
|
-
persistCompletedKey(basePath, reconciledKey);
|
|
115
|
-
s.completedKeySet.add(reconciledKey);
|
|
116
|
-
s.unitDispatchCount.delete(dispatchKey);
|
|
117
|
-
invalidateAllCaches();
|
|
118
|
-
return { action: "recovered", dispatchAgain: true };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// General reconciliation: artifact appeared on last attempt
|
|
125
|
-
if (verifyExpectedArtifact(unitType, unitId, basePath)) {
|
|
126
|
-
ctx.ui.notify(
|
|
127
|
-
`Loop recovery: ${unitType} ${unitId} — artifact verified after ${prevCount + 1} dispatches. Advancing.`,
|
|
128
|
-
"info",
|
|
129
|
-
);
|
|
130
|
-
persistCompletedKey(basePath, dispatchKey);
|
|
131
|
-
s.completedKeySet.add(dispatchKey);
|
|
132
|
-
s.unitDispatchCount.delete(dispatchKey);
|
|
133
|
-
invalidateAllCaches();
|
|
134
|
-
return { action: "recovered", dispatchAgain: true };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Last resort for complete-milestone: generate stub summary
|
|
138
|
-
if (unitType === "complete-milestone") {
|
|
139
|
-
try {
|
|
140
|
-
const mPath = resolveMilestonePath(basePath, unitId);
|
|
141
|
-
if (mPath) {
|
|
142
|
-
const stubPath = join(mPath, `${unitId}-SUMMARY.md`);
|
|
143
|
-
if (!existsSync(stubPath)) {
|
|
144
|
-
writeFileSync(stubPath, `# ${unitId} Summary\n\nAuto-generated stub — milestone tasks completed but summary generation failed after ${prevCount + 1} attempts.\nReview and replace this stub with a proper summary.\n`);
|
|
145
|
-
ctx.ui.notify(`Generated stub summary for ${unitId} to unblock pipeline. Review later.`, "warning");
|
|
146
|
-
persistCompletedKey(basePath, dispatchKey);
|
|
147
|
-
s.completedKeySet.add(dispatchKey);
|
|
148
|
-
s.unitDispatchCount.delete(dispatchKey);
|
|
149
|
-
invalidateAllCaches();
|
|
150
|
-
return { action: "recovered", dispatchAgain: true };
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
} catch { /* non-fatal — fall through to normal stop */ }
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
157
|
-
const remediation = buildLoopRemediationSteps(unitType, unitId, basePath);
|
|
158
|
-
sendDesktopNotification("GSD", `Loop detected: ${unitType} ${unitId}`, "error", "error");
|
|
159
|
-
return {
|
|
160
|
-
action: "stop",
|
|
161
|
-
reason: `Loop: ${unitType} ${unitId}`,
|
|
162
|
-
notifyMessage: `Loop detected: ${unitType} ${unitId} dispatched ${prevCount + 1} times total. Expected artifact not found.${expected ? `\n Expected: ${expected}` : ""}${remediation ? `\n\n Remediation steps:\n${remediation}` : "\n Check branch state and .gsd/ artifacts."}`,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
s.unitDispatchCount.set(dispatchKey, prevCount + 1);
|
|
167
|
-
|
|
168
|
-
if (prevCount > 0) {
|
|
169
|
-
// Adaptive self-repair: each retry attempts a different remediation step.
|
|
170
|
-
if (unitType === "execute-task") {
|
|
171
|
-
const status = await inspectExecuteTaskDurability(basePath, unitId);
|
|
172
|
-
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
173
|
-
if (status && mid && sid && tid) {
|
|
174
|
-
if (status.summaryExists && !status.taskChecked) {
|
|
175
|
-
const repaired = skipExecuteTask(basePath, mid, sid, tid, status, "self-repair", 0);
|
|
176
|
-
if (repaired && verifyExpectedArtifact(unitType, unitId, basePath)) {
|
|
177
|
-
ctx.ui.notify(
|
|
178
|
-
`Self-repaired ${unitId}: summary existed but checkbox was unmarked. Marked [x] and advancing.`,
|
|
179
|
-
"warning",
|
|
180
|
-
);
|
|
181
|
-
const repairedKey = `${unitType}/${unitId}`;
|
|
182
|
-
persistCompletedKey(basePath, repairedKey);
|
|
183
|
-
s.completedKeySet.add(repairedKey);
|
|
184
|
-
s.unitDispatchCount.delete(dispatchKey);
|
|
185
|
-
invalidateAllCaches();
|
|
186
|
-
return { action: "recovered", dispatchAgain: true };
|
|
187
|
-
}
|
|
188
|
-
} else if (prevCount >= STUB_RECOVERY_THRESHOLD && !status.summaryExists) {
|
|
189
|
-
const tasksDir = resolveTasksDir(basePath, mid, sid);
|
|
190
|
-
const sDir = resolveSlicePath(basePath, mid, sid);
|
|
191
|
-
const targetDir = tasksDir ?? (sDir ? join(sDir, "tasks") : null);
|
|
192
|
-
if (targetDir) {
|
|
193
|
-
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
194
|
-
const summaryPath = join(targetDir, buildTaskFileName(tid, "SUMMARY"));
|
|
195
|
-
if (!existsSync(summaryPath)) {
|
|
196
|
-
const stubContent = [
|
|
197
|
-
`# PARTIAL RECOVERY — attempt ${prevCount + 1} of ${MAX_UNIT_DISPATCHES}`,
|
|
198
|
-
``,
|
|
199
|
-
`Task \`${tid}\` in slice \`${sid}\` (milestone \`${mid}\`) has not yet produced a real summary.`,
|
|
200
|
-
`This placeholder was written by auto-mode after ${prevCount} dispatch attempts.`,
|
|
201
|
-
``,
|
|
202
|
-
`The next agent session will retry this task. Replace this file with real work when done.`,
|
|
203
|
-
].join("\n");
|
|
204
|
-
writeFileSync(summaryPath, stubContent, "utf-8");
|
|
205
|
-
ctx.ui.notify(
|
|
206
|
-
`Stub recovery (attempt ${prevCount + 1}/${MAX_UNIT_DISPATCHES}): ${unitId} stub summary placeholder written. Retrying with recovery context.`,
|
|
207
|
-
"warning",
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
ctx.ui.notify(
|
|
215
|
-
`${unitType} ${unitId} didn't produce expected artifact. Retrying (${prevCount + 1}/${MAX_UNIT_DISPATCHES}).`,
|
|
216
|
-
"warning",
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return { action: "proceed" };
|
|
221
|
-
}
|
|
@@ -1,430 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mechanical Completion — deterministic post-verification artifact generation.
|
|
3
|
-
*
|
|
4
|
-
* Pure functions that aggregate task-level outputs into slice/milestone summaries,
|
|
5
|
-
* UAT stubs, roadmap checkbox updates, and validation reports. Zero orchestration
|
|
6
|
-
* dependencies — operates on filesystem paths and parsed structures only.
|
|
7
|
-
*
|
|
8
|
-
* ADR-003: replaces LLM-driven complete-slice and validate-milestone units with
|
|
9
|
-
* mechanical aggregation when the data is sufficient.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import { atomicWriteSync } from "./atomic-write.js";
|
|
15
|
-
import { loadFile, parseSummary } from "./files.js";
|
|
16
|
-
import { extractMarkdownSection } from "./auto-prompts.js";
|
|
17
|
-
import {
|
|
18
|
-
resolveTaskFiles,
|
|
19
|
-
resolveTaskJsonFiles,
|
|
20
|
-
resolveTasksDir,
|
|
21
|
-
resolveSliceFile,
|
|
22
|
-
resolveSlicePath,
|
|
23
|
-
resolveMilestoneFile,
|
|
24
|
-
resolveMilestonePath,
|
|
25
|
-
resolveGsdRootFile,
|
|
26
|
-
} from "./paths.js";
|
|
27
|
-
import type { Summary, SummaryFrontmatter } from "./types.js";
|
|
28
|
-
import type { EvidenceJSON } from "./verification-evidence.js";
|
|
29
|
-
|
|
30
|
-
// ─── Slice Completion ────────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Mechanically complete a slice by aggregating task summaries into:
|
|
34
|
-
* - S##-SUMMARY.md (aggregated frontmatter + task one-liners)
|
|
35
|
-
* - S##-UAT.md (extracted from plan Verification section)
|
|
36
|
-
* - Roadmap checkbox [x] update
|
|
37
|
-
*
|
|
38
|
-
* Returns true if completion succeeded, false if data is insufficient
|
|
39
|
-
* (serves as quality gate — caller falls back to LLM completion).
|
|
40
|
-
*/
|
|
41
|
-
export async function mechanicalSliceCompletion(
|
|
42
|
-
base: string, mid: string, sid: string,
|
|
43
|
-
): Promise<boolean> {
|
|
44
|
-
const tDir = resolveTasksDir(base, mid, sid);
|
|
45
|
-
if (!tDir) return false;
|
|
46
|
-
|
|
47
|
-
// Read all task summaries
|
|
48
|
-
const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
|
|
49
|
-
if (summaryFiles.length === 0) return false;
|
|
50
|
-
|
|
51
|
-
const taskSummaries: Array<{ taskId: string; summary: Summary }> = [];
|
|
52
|
-
for (const file of summaryFiles) {
|
|
53
|
-
const content = readFileSync(join(tDir, file), "utf-8");
|
|
54
|
-
if (!content.trim()) continue;
|
|
55
|
-
const summary = parseSummary(content);
|
|
56
|
-
const taskId = file.match(/^(T\d+)/)?.[1] ?? file;
|
|
57
|
-
taskSummaries.push({ taskId, summary });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (taskSummaries.length === 0) return false;
|
|
61
|
-
|
|
62
|
-
// Quality gate: multi-task slices need substantive summaries
|
|
63
|
-
if (taskSummaries.length > 1) {
|
|
64
|
-
const totalContent = taskSummaries
|
|
65
|
-
.map(ts => ts.summary.whatHappened || ts.summary.oneLiner || "")
|
|
66
|
-
.join("");
|
|
67
|
-
if (totalContent.length < 200) return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Aggregate frontmatter
|
|
71
|
-
const aggregated = aggregateFrontmatter(taskSummaries.map(ts => ts.summary.frontmatter));
|
|
72
|
-
|
|
73
|
-
// Build SUMMARY.md
|
|
74
|
-
const summaryLines: string[] = [
|
|
75
|
-
"---",
|
|
76
|
-
`id: ${sid}`,
|
|
77
|
-
`parent: ${mid}`,
|
|
78
|
-
`milestone: ${mid}`,
|
|
79
|
-
];
|
|
80
|
-
if (aggregated.provides.length > 0)
|
|
81
|
-
summaryLines.push(`provides:\n${aggregated.provides.map(p => ` - ${p}`).join("\n")}`);
|
|
82
|
-
if (aggregated.key_files.length > 0)
|
|
83
|
-
summaryLines.push(`key_files:\n${aggregated.key_files.map(f => ` - ${f}`).join("\n")}`);
|
|
84
|
-
if (aggregated.key_decisions.length > 0)
|
|
85
|
-
summaryLines.push(`key_decisions:\n${aggregated.key_decisions.map(d => ` - ${d}`).join("\n")}`);
|
|
86
|
-
if (aggregated.patterns_established.length > 0)
|
|
87
|
-
summaryLines.push(`patterns_established:\n${aggregated.patterns_established.map(p => ` - ${p}`).join("\n")}`);
|
|
88
|
-
if (aggregated.affects.length > 0)
|
|
89
|
-
summaryLines.push(`affects:\n${aggregated.affects.map(a => ` - ${a}`).join("\n")}`);
|
|
90
|
-
if (aggregated.observability_surfaces.length > 0)
|
|
91
|
-
summaryLines.push(`observability_surfaces:\n${aggregated.observability_surfaces.map(o => ` - ${o}`).join("\n")}`);
|
|
92
|
-
const allPassed = taskSummaries.every(ts => ts.summary.frontmatter.verification_result === "passed");
|
|
93
|
-
summaryLines.push(`verification_result: ${allPassed ? "passed" : "mixed"}`);
|
|
94
|
-
summaryLines.push(`completed_at: ${new Date().toISOString()}`);
|
|
95
|
-
summaryLines.push("---");
|
|
96
|
-
summaryLines.push("");
|
|
97
|
-
summaryLines.push(`# ${sid}: Slice Summary`);
|
|
98
|
-
summaryLines.push("");
|
|
99
|
-
|
|
100
|
-
// Task one-liners
|
|
101
|
-
for (const { taskId, summary } of taskSummaries) {
|
|
102
|
-
const line = summary.oneLiner || summary.title || taskId;
|
|
103
|
-
summaryLines.push(`- **${taskId}**: ${line}`);
|
|
104
|
-
}
|
|
105
|
-
summaryLines.push("");
|
|
106
|
-
|
|
107
|
-
const sDir = resolveSlicePath(base, mid, sid);
|
|
108
|
-
if (!sDir) return false;
|
|
109
|
-
|
|
110
|
-
const summaryPath = join(sDir, `${sid}-SUMMARY.md`);
|
|
111
|
-
atomicWriteSync(summaryPath, summaryLines.join("\n"));
|
|
112
|
-
process.stderr.write(`gsd-mechanical: wrote ${summaryPath}\n`);
|
|
113
|
-
|
|
114
|
-
// Build UAT.md from plan's Verification section
|
|
115
|
-
const planPath = resolveSliceFile(base, mid, sid, "PLAN");
|
|
116
|
-
if (planPath) {
|
|
117
|
-
const planContent = readFileSync(planPath, "utf-8");
|
|
118
|
-
const verification = extractMarkdownSection(planContent, "Verification");
|
|
119
|
-
if (verification) {
|
|
120
|
-
const uatContent = [
|
|
121
|
-
"---",
|
|
122
|
-
`id: ${sid}`,
|
|
123
|
-
`parent: ${mid}`,
|
|
124
|
-
"type: artifact-driven",
|
|
125
|
-
"---",
|
|
126
|
-
"",
|
|
127
|
-
`# ${sid}: UAT`,
|
|
128
|
-
"",
|
|
129
|
-
verification,
|
|
130
|
-
"",
|
|
131
|
-
].join("\n");
|
|
132
|
-
const uatPath = join(sDir, `${sid}-UAT.md`);
|
|
133
|
-
atomicWriteSync(uatPath, uatContent);
|
|
134
|
-
process.stderr.write(`gsd-mechanical: wrote ${uatPath}\n`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Mark slice [x] in ROADMAP
|
|
139
|
-
await markSliceInRoadmap(base, mid, sid);
|
|
140
|
-
|
|
141
|
-
// Append new decisions if any
|
|
142
|
-
await appendNewDecisions(base, taskSummaries.map(ts => ts.summary));
|
|
143
|
-
|
|
144
|
-
// Update requirements if all passed
|
|
145
|
-
if (allPassed) {
|
|
146
|
-
await mechanicalRequirementsUpdate(base, mid, sid, taskSummaries.map(ts => ts.summary));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ─── Requirements Update ─────────────────────────────────────────────────────
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Conservative requirements update: mark requirements Validated only if
|
|
156
|
-
* all tasks' verification passed.
|
|
157
|
-
*/
|
|
158
|
-
export async function mechanicalRequirementsUpdate(
|
|
159
|
-
_base: string, _mid: string, _sid: string, _taskSummaries: Summary[],
|
|
160
|
-
): Promise<void> {
|
|
161
|
-
// Conservative: requirements validation requires human or LLM judgment
|
|
162
|
-
// about whether the requirement is truly met. Mechanical completion only
|
|
163
|
-
// marks the slice done — requirement status updates are left to the
|
|
164
|
-
// existing validation pipeline.
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ─── Decision Aggregation ────────────────────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Collect key_decisions from task summaries, deduplicate against existing
|
|
171
|
-
* DECISIONS.md, and append new ones.
|
|
172
|
-
*/
|
|
173
|
-
export async function appendNewDecisions(
|
|
174
|
-
base: string, taskSummaries: Summary[],
|
|
175
|
-
): Promise<void> {
|
|
176
|
-
const allDecisions = taskSummaries.flatMap(s => s.frontmatter.key_decisions);
|
|
177
|
-
if (allDecisions.length === 0) return;
|
|
178
|
-
|
|
179
|
-
const decisionsPath = resolveGsdRootFile(base, "DECISIONS");
|
|
180
|
-
const existing = existsSync(decisionsPath)
|
|
181
|
-
? readFileSync(decisionsPath, "utf-8")
|
|
182
|
-
: "";
|
|
183
|
-
|
|
184
|
-
// Deduplicate — skip decisions whose text already appears in the file
|
|
185
|
-
const newDecisions = allDecisions.filter(d =>
|
|
186
|
-
d.trim() && !existing.includes(d.trim()),
|
|
187
|
-
);
|
|
188
|
-
if (newDecisions.length === 0) return;
|
|
189
|
-
|
|
190
|
-
const entries = newDecisions
|
|
191
|
-
.map(d => `- ${d} _(auto-aggregated from task summaries)_`)
|
|
192
|
-
.join("\n");
|
|
193
|
-
|
|
194
|
-
const updated = existing.trimEnd() + "\n\n### Auto-aggregated Decisions\n\n" + entries + "\n";
|
|
195
|
-
atomicWriteSync(decisionsPath, updated);
|
|
196
|
-
process.stderr.write(`gsd-mechanical: appended ${newDecisions.length} decision(s) to DECISIONS.md\n`);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ─── Milestone Verification ──────────────────────────────────────────────────
|
|
200
|
-
|
|
201
|
-
export interface MilestoneVerificationResult {
|
|
202
|
-
verdict: "passed" | "failed" | "mixed";
|
|
203
|
-
checks: EvidenceJSON[];
|
|
204
|
-
uatResults: string[];
|
|
205
|
-
markdown: string;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Aggregate T##-VERIFY.json files and S##-UAT-RESULT.md files across all
|
|
210
|
-
* slices in a milestone to produce VALIDATION.md.
|
|
211
|
-
*/
|
|
212
|
-
export async function aggregateMilestoneVerification(
|
|
213
|
-
base: string, mid: string,
|
|
214
|
-
): Promise<MilestoneVerificationResult> {
|
|
215
|
-
const mDir = resolveMilestonePath(base, mid);
|
|
216
|
-
if (!mDir) return { verdict: "failed", checks: [], uatResults: [], markdown: "" };
|
|
217
|
-
|
|
218
|
-
const allChecks: EvidenceJSON[] = [];
|
|
219
|
-
const allUatResults: string[] = [];
|
|
220
|
-
|
|
221
|
-
// Scan all slices
|
|
222
|
-
const slicesDir = join(mDir, "slices");
|
|
223
|
-
if (!existsSync(slicesDir)) return { verdict: "failed", checks: [], uatResults: [], markdown: "" };
|
|
224
|
-
|
|
225
|
-
const sliceDirs = readdirSyncSafe(slicesDir).filter(name => /^S\d+/i.test(name)).sort();
|
|
226
|
-
|
|
227
|
-
for (const sliceName of sliceDirs) {
|
|
228
|
-
const sid = sliceName.match(/^(S\d+)/i)?.[1] ?? sliceName;
|
|
229
|
-
const tDir = resolveTasksDir(base, mid, sid);
|
|
230
|
-
if (tDir) {
|
|
231
|
-
const verifyFiles = resolveTaskJsonFiles(tDir, "VERIFY");
|
|
232
|
-
for (const vf of verifyFiles) {
|
|
233
|
-
try {
|
|
234
|
-
const content = readFileSync(join(tDir, vf), "utf-8");
|
|
235
|
-
const evidence = JSON.parse(content) as EvidenceJSON;
|
|
236
|
-
allChecks.push(evidence);
|
|
237
|
-
} catch {
|
|
238
|
-
// Skip malformed JSON
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Check for UAT result
|
|
244
|
-
const uatResultPath = resolveSliceFile(base, mid, sid, "UAT-RESULT");
|
|
245
|
-
if (uatResultPath) {
|
|
246
|
-
try {
|
|
247
|
-
const uatContent = readFileSync(uatResultPath, "utf-8");
|
|
248
|
-
allUatResults.push(`### ${sid}\n\n${uatContent}`);
|
|
249
|
-
} catch {
|
|
250
|
-
// Non-fatal
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Determine verdict
|
|
256
|
-
const allPassed = allChecks.length > 0 && allChecks.every(c => c.passed);
|
|
257
|
-
const anyFailed = allChecks.some(c => !c.passed);
|
|
258
|
-
const verdict: "passed" | "failed" | "mixed" = allPassed
|
|
259
|
-
? "passed"
|
|
260
|
-
: anyFailed
|
|
261
|
-
? (allChecks.some(c => c.passed) ? "mixed" : "failed")
|
|
262
|
-
: "passed"; // No checks = vacuously passed
|
|
263
|
-
|
|
264
|
-
// Build VALIDATION.md
|
|
265
|
-
const mdLines: string[] = [
|
|
266
|
-
"---",
|
|
267
|
-
`milestone: ${mid}`,
|
|
268
|
-
`verdict: ${verdict}`,
|
|
269
|
-
"remediation_round: 0",
|
|
270
|
-
`validated_at: ${new Date().toISOString()}`,
|
|
271
|
-
"---",
|
|
272
|
-
"",
|
|
273
|
-
`# ${mid}: Milestone Validation`,
|
|
274
|
-
"",
|
|
275
|
-
`**Verdict:** ${verdict}`,
|
|
276
|
-
"",
|
|
277
|
-
"## Verification Results",
|
|
278
|
-
"",
|
|
279
|
-
];
|
|
280
|
-
|
|
281
|
-
if (allChecks.length === 0) {
|
|
282
|
-
mdLines.push("_No verification evidence found._");
|
|
283
|
-
} else {
|
|
284
|
-
mdLines.push("| Task | Passed | Checks | Failed |");
|
|
285
|
-
mdLines.push("|------|--------|--------|--------|");
|
|
286
|
-
for (const check of allChecks) {
|
|
287
|
-
const failedCount = check.checks.filter(c => c.verdict === "fail").length;
|
|
288
|
-
mdLines.push(
|
|
289
|
-
`| ${check.taskId} | ${check.passed ? "yes" : "no"} | ${check.checks.length} | ${failedCount} |`,
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (allUatResults.length > 0) {
|
|
295
|
-
mdLines.push("");
|
|
296
|
-
mdLines.push("## UAT Results");
|
|
297
|
-
mdLines.push("");
|
|
298
|
-
mdLines.push(...allUatResults);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
mdLines.push("");
|
|
302
|
-
|
|
303
|
-
const markdown = mdLines.join("\n");
|
|
304
|
-
|
|
305
|
-
// Write VALIDATION.md
|
|
306
|
-
const validationPath = join(mDir, `${mid}-VALIDATION.md`);
|
|
307
|
-
atomicWriteSync(validationPath, markdown);
|
|
308
|
-
process.stderr.write(`gsd-mechanical: wrote ${validationPath}\n`);
|
|
309
|
-
|
|
310
|
-
return { verdict, checks: allChecks, uatResults: allUatResults, markdown };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ─── Milestone Summary ──────────────────────────────────────────────────────
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Read all S##-SUMMARY.md files and produce M##-SUMMARY.md.
|
|
317
|
-
*/
|
|
318
|
-
export async function generateMilestoneSummary(
|
|
319
|
-
base: string, mid: string,
|
|
320
|
-
): Promise<string> {
|
|
321
|
-
const mDir = resolveMilestonePath(base, mid);
|
|
322
|
-
if (!mDir) return "";
|
|
323
|
-
|
|
324
|
-
const slicesDir = join(mDir, "slices");
|
|
325
|
-
if (!existsSync(slicesDir)) return "";
|
|
326
|
-
|
|
327
|
-
const sliceDirs = readdirSyncSafe(slicesDir).filter(name => /^S\d+/i.test(name)).sort();
|
|
328
|
-
|
|
329
|
-
const aggregatedProvides: string[] = [];
|
|
330
|
-
const aggregatedKeyFiles: string[] = [];
|
|
331
|
-
const aggregatedKeyDecisions: string[] = [];
|
|
332
|
-
const aggregatedPatterns: string[] = [];
|
|
333
|
-
const sliceOneLinerList: string[] = [];
|
|
334
|
-
|
|
335
|
-
for (const sliceName of sliceDirs) {
|
|
336
|
-
const sid = sliceName.match(/^(S\d+)/i)?.[1] ?? sliceName;
|
|
337
|
-
const summaryPath = resolveSliceFile(base, mid, sid, "SUMMARY");
|
|
338
|
-
if (!summaryPath) continue;
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
const content = readFileSync(summaryPath, "utf-8");
|
|
342
|
-
const summary = parseSummary(content);
|
|
343
|
-
aggregatedProvides.push(...summary.frontmatter.provides);
|
|
344
|
-
aggregatedKeyFiles.push(...summary.frontmatter.key_files);
|
|
345
|
-
aggregatedKeyDecisions.push(...summary.frontmatter.key_decisions);
|
|
346
|
-
aggregatedPatterns.push(...summary.frontmatter.patterns_established);
|
|
347
|
-
sliceOneLinerList.push(`- **${sid}**: ${summary.oneLiner || summary.title || sid}`);
|
|
348
|
-
} catch {
|
|
349
|
-
sliceOneLinerList.push(`- **${sid}**: _(summary unavailable)_`);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const mdLines: string[] = [
|
|
354
|
-
"---",
|
|
355
|
-
`id: ${mid}`,
|
|
356
|
-
];
|
|
357
|
-
if (dedup(aggregatedProvides).length > 0)
|
|
358
|
-
mdLines.push(`provides:\n${dedup(aggregatedProvides).map(p => ` - ${p}`).join("\n")}`);
|
|
359
|
-
if (dedup(aggregatedKeyFiles).length > 0)
|
|
360
|
-
mdLines.push(`key_files:\n${dedup(aggregatedKeyFiles).map(f => ` - ${f}`).join("\n")}`);
|
|
361
|
-
if (dedup(aggregatedKeyDecisions).length > 0)
|
|
362
|
-
mdLines.push(`key_decisions:\n${dedup(aggregatedKeyDecisions).map(d => ` - ${d}`).join("\n")}`);
|
|
363
|
-
if (dedup(aggregatedPatterns).length > 0)
|
|
364
|
-
mdLines.push(`patterns_established:\n${dedup(aggregatedPatterns).map(p => ` - ${p}`).join("\n")}`);
|
|
365
|
-
mdLines.push(`completed_at: ${new Date().toISOString()}`);
|
|
366
|
-
mdLines.push("---");
|
|
367
|
-
mdLines.push("");
|
|
368
|
-
mdLines.push(`# ${mid}: Milestone Summary`);
|
|
369
|
-
mdLines.push("");
|
|
370
|
-
mdLines.push("## Slices");
|
|
371
|
-
mdLines.push("");
|
|
372
|
-
mdLines.push(...sliceOneLinerList);
|
|
373
|
-
mdLines.push("");
|
|
374
|
-
|
|
375
|
-
const content = mdLines.join("\n");
|
|
376
|
-
|
|
377
|
-
// Write M##-SUMMARY.md
|
|
378
|
-
const summaryPath = join(mDir, `${mid}-SUMMARY.md`);
|
|
379
|
-
atomicWriteSync(summaryPath, content);
|
|
380
|
-
process.stderr.write(`gsd-mechanical: wrote ${summaryPath}\n`);
|
|
381
|
-
|
|
382
|
-
return content;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
386
|
-
|
|
387
|
-
function aggregateFrontmatter(fms: SummaryFrontmatter[]): {
|
|
388
|
-
provides: string[];
|
|
389
|
-
key_files: string[];
|
|
390
|
-
key_decisions: string[];
|
|
391
|
-
patterns_established: string[];
|
|
392
|
-
affects: string[];
|
|
393
|
-
observability_surfaces: string[];
|
|
394
|
-
} {
|
|
395
|
-
return {
|
|
396
|
-
provides: dedup(fms.flatMap(f => f.provides)),
|
|
397
|
-
key_files: dedup(fms.flatMap(f => f.key_files)),
|
|
398
|
-
key_decisions: dedup(fms.flatMap(f => f.key_decisions)),
|
|
399
|
-
patterns_established: dedup(fms.flatMap(f => f.patterns_established)),
|
|
400
|
-
affects: dedup(fms.flatMap(f => f.affects)),
|
|
401
|
-
observability_surfaces: dedup(fms.flatMap(f => f.observability_surfaces)),
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function dedup(arr: string[]): string[] {
|
|
406
|
-
return [...new Set(arr.filter(s => s.trim()))];
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
async function markSliceInRoadmap(base: string, mid: string, sid: string): Promise<void> {
|
|
410
|
-
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
411
|
-
if (!roadmapPath) return;
|
|
412
|
-
const content = await loadFile(roadmapPath);
|
|
413
|
-
if (!content) return;
|
|
414
|
-
const updated = content.replace(
|
|
415
|
-
new RegExp(`^(\\s*-\\s+)\\[ \\]\\s+\\*\\*${sid}:`, "m"),
|
|
416
|
-
`$1[x] **${sid}:`,
|
|
417
|
-
);
|
|
418
|
-
if (updated !== content) {
|
|
419
|
-
atomicWriteSync(roadmapPath, updated);
|
|
420
|
-
process.stderr.write(`gsd-mechanical: marked ${sid} done in ROADMAP\n`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function readdirSyncSafe(dir: string): string[] {
|
|
425
|
-
try {
|
|
426
|
-
return readdirSync(dir);
|
|
427
|
-
} catch {
|
|
428
|
-
return [];
|
|
429
|
-
}
|
|
430
|
-
}
|