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,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-mode Recovery — artifact resolution, verification, blocker placeholders,
|
|
3
|
-
* skip artifacts,
|
|
3
|
+
* skip artifacts, merge state reconciliation,
|
|
4
4
|
* self-heal runtime records, and loop remediation steps.
|
|
5
5
|
*
|
|
6
6
|
* Pure functions that receive all needed state as parameters — no module-level
|
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
|
11
|
-
import {
|
|
12
|
-
clearUnitRuntimeRecord,
|
|
13
|
-
} from "./unit-runtime.js";
|
|
11
|
+
import { clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
14
12
|
import { clearParseCache, parseRoadmap, parsePlan } from "./files.js";
|
|
13
|
+
import { isValidationTerminal } from "./state.js";
|
|
15
14
|
import {
|
|
16
15
|
nativeConflictFiles,
|
|
17
16
|
nativeCommit,
|
|
@@ -35,22 +34,29 @@ import {
|
|
|
35
34
|
resolveMilestoneFile,
|
|
36
35
|
clearPathCache,
|
|
37
36
|
resolveGsdRootFile,
|
|
38
|
-
gsdRoot,
|
|
39
37
|
} from "./paths.js";
|
|
40
|
-
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
import {
|
|
39
|
+
existsSync,
|
|
40
|
+
mkdirSync,
|
|
41
|
+
readFileSync,
|
|
42
|
+
writeFileSync,
|
|
43
|
+
unlinkSync,
|
|
44
|
+
} from "node:fs";
|
|
44
45
|
import { dirname, join } from "node:path";
|
|
45
|
-
import { parseUnitId } from "./unit-id.js";
|
|
46
46
|
|
|
47
47
|
// ─── Artifact Resolution & Verification ───────────────────────────────────────
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Resolve the expected artifact for a unit to an absolute path.
|
|
51
51
|
*/
|
|
52
|
-
export function resolveExpectedArtifactPath(
|
|
53
|
-
|
|
52
|
+
export function resolveExpectedArtifactPath(
|
|
53
|
+
unitType: string,
|
|
54
|
+
unitId: string,
|
|
55
|
+
base: string,
|
|
56
|
+
): string | null {
|
|
57
|
+
const parts = unitId.split("/");
|
|
58
|
+
const mid = parts[0]!;
|
|
59
|
+
const sid = parts[1];
|
|
54
60
|
switch (unitType) {
|
|
55
61
|
case "research-milestone": {
|
|
56
62
|
const dir = resolveMilestonePath(base, mid);
|
|
@@ -77,8 +83,11 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
|
|
|
77
83
|
return dir ? join(dir, buildSliceFileName(sid!, "UAT-RESULT")) : null;
|
|
78
84
|
}
|
|
79
85
|
case "execute-task": {
|
|
86
|
+
const tid = parts[2];
|
|
80
87
|
const dir = resolveSlicePath(base, mid, sid!);
|
|
81
|
-
return dir && tid
|
|
88
|
+
return dir && tid
|
|
89
|
+
? join(dir, "tasks", buildTaskFileName(tid, "SUMMARY"))
|
|
90
|
+
: null;
|
|
82
91
|
}
|
|
83
92
|
case "complete-slice": {
|
|
84
93
|
const dir = resolveSlicePath(base, mid, sid!);
|
|
@@ -112,7 +121,11 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
|
|
|
112
121
|
* the summary allowed the unit to be marked complete when the LLM
|
|
113
122
|
* skipped writing the UAT file (see #176).
|
|
114
123
|
*/
|
|
115
|
-
export function verifyExpectedArtifact(
|
|
124
|
+
export function verifyExpectedArtifact(
|
|
125
|
+
unitType: string,
|
|
126
|
+
unitId: string,
|
|
127
|
+
base: string,
|
|
128
|
+
): boolean {
|
|
116
129
|
// Hook units have no standard artifact — always pass. Their lifecycle
|
|
117
130
|
// is managed by the hook engine, not the artifact verification system.
|
|
118
131
|
if (unitType.startsWith("hook/")) return true;
|
|
@@ -138,19 +151,9 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
138
151
|
if (!absPath) return false;
|
|
139
152
|
if (!existsSync(absPath)) return false;
|
|
140
153
|
|
|
141
|
-
// validate-milestone must have a VALIDATION file with a terminal verdict
|
|
142
|
-
// (pass, needs-attention, or needs-remediation). Without this check, a
|
|
143
|
-
// VALIDATION file with missing/malformed frontmatter or an unrecognized
|
|
144
|
-
// verdict is treated as "complete" by the artifact check but deriveState
|
|
145
|
-
// still returns phase:"validating-milestone" (because isValidationTerminal
|
|
146
|
-
// returns false), creating an infinite skip loop that hits the lifetime cap.
|
|
147
154
|
if (unitType === "validate-milestone") {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (!isValidationTerminal(validationContent)) return false;
|
|
151
|
-
} catch {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
155
|
+
const validationContent = readFileSync(absPath, "utf-8");
|
|
156
|
+
if (!isValidationTerminal(validationContent)) return false;
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
// plan-slice must produce a plan with actual task entries, not just a scaffold.
|
|
@@ -165,7 +168,10 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
165
168
|
|
|
166
169
|
// execute-task must also have its checkbox marked [x] in the slice plan
|
|
167
170
|
if (unitType === "execute-task") {
|
|
168
|
-
const
|
|
171
|
+
const parts = unitId.split("/");
|
|
172
|
+
const mid = parts[0];
|
|
173
|
+
const sid = parts[1];
|
|
174
|
+
const tid = parts[2];
|
|
169
175
|
if (mid && sid && tid) {
|
|
170
176
|
const planAbs = resolveSliceFile(base, mid, sid, "PLAN");
|
|
171
177
|
if (planAbs && existsSync(planAbs)) {
|
|
@@ -182,7 +188,9 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
182
188
|
// but omitted T{tid}-PLAN.md files would be marked complete, causing execute-task
|
|
183
189
|
// to dispatch with a missing task plan (see issue #739).
|
|
184
190
|
if (unitType === "plan-slice") {
|
|
185
|
-
const
|
|
191
|
+
const parts = unitId.split("/");
|
|
192
|
+
const mid = parts[0];
|
|
193
|
+
const sid = parts[1];
|
|
186
194
|
if (mid && sid) {
|
|
187
195
|
try {
|
|
188
196
|
const planContent = readFileSync(absPath, "utf-8");
|
|
@@ -206,8 +214,9 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
206
214
|
// state machine keeps returning the same complete-slice unit (roadmap still shows
|
|
207
215
|
// the slice incomplete), so dispatchNextUnit recurses forever.
|
|
208
216
|
if (unitType === "complete-slice") {
|
|
209
|
-
const
|
|
210
|
-
|
|
217
|
+
const parts = unitId.split("/");
|
|
218
|
+
const mid = parts[0];
|
|
219
|
+
const sid = parts[1];
|
|
211
220
|
if (mid && sid) {
|
|
212
221
|
const dir = resolveSlicePath(base, mid, sid);
|
|
213
222
|
if (dir) {
|
|
@@ -221,7 +230,7 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
221
230
|
try {
|
|
222
231
|
const roadmapContent = readFileSync(roadmapFile, "utf-8");
|
|
223
232
|
const roadmap = parseRoadmap(roadmapContent);
|
|
224
|
-
const slice =
|
|
233
|
+
const slice = roadmap.slices.find((s) => s.id === sid);
|
|
225
234
|
if (slice && !slice.done) return false;
|
|
226
235
|
} catch {
|
|
227
236
|
// Corrupt/unparseable roadmap — fail verification so the unit
|
|
@@ -240,7 +249,12 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
240
249
|
* Write a placeholder artifact so the pipeline can advance past a stuck unit.
|
|
241
250
|
* Returns the relative path written, or null if the path couldn't be resolved.
|
|
242
251
|
*/
|
|
243
|
-
export function writeBlockerPlaceholder(
|
|
252
|
+
export function writeBlockerPlaceholder(
|
|
253
|
+
unitType: string,
|
|
254
|
+
unitId: string,
|
|
255
|
+
base: string,
|
|
256
|
+
reason: string,
|
|
257
|
+
): string | null {
|
|
244
258
|
const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
|
|
245
259
|
if (!absPath) return null;
|
|
246
260
|
const dir = dirname(absPath);
|
|
@@ -259,8 +273,14 @@ export function writeBlockerPlaceholder(unitType: string, unitId: string, base:
|
|
|
259
273
|
return diagnoseExpectedArtifact(unitType, unitId, base);
|
|
260
274
|
}
|
|
261
275
|
|
|
262
|
-
export function diagnoseExpectedArtifact(
|
|
263
|
-
|
|
276
|
+
export function diagnoseExpectedArtifact(
|
|
277
|
+
unitType: string,
|
|
278
|
+
unitId: string,
|
|
279
|
+
base: string,
|
|
280
|
+
): string | null {
|
|
281
|
+
const parts = unitId.split("/");
|
|
282
|
+
const mid = parts[0];
|
|
283
|
+
const sid = parts[1];
|
|
264
284
|
switch (unitType) {
|
|
265
285
|
case "research-milestone":
|
|
266
286
|
return `${relMilestoneFile(base, mid!, "RESEARCH")} (milestone research)`;
|
|
@@ -271,6 +291,7 @@ export function diagnoseExpectedArtifact(unitType: string, unitId: string, base:
|
|
|
271
291
|
case "plan-slice":
|
|
272
292
|
return `${relSliceFile(base, mid!, sid!, "PLAN")} (slice plan)`;
|
|
273
293
|
case "execute-task": {
|
|
294
|
+
const tid = parts[2];
|
|
274
295
|
return `Task ${tid} marked [x] in ${relSliceFile(base, mid!, sid!, "PLAN")} + summary written`;
|
|
275
296
|
}
|
|
276
297
|
case "complete-slice":
|
|
@@ -299,9 +320,13 @@ export function diagnoseExpectedArtifact(unitType: string, unitId: string, base:
|
|
|
299
320
|
* the [x] checkbox in the slice plan. Returns true if artifacts were written.
|
|
300
321
|
*/
|
|
301
322
|
export function skipExecuteTask(
|
|
302
|
-
base: string,
|
|
323
|
+
base: string,
|
|
324
|
+
mid: string,
|
|
325
|
+
sid: string,
|
|
326
|
+
tid: string,
|
|
303
327
|
status: { summaryExists: boolean; taskChecked: boolean },
|
|
304
|
-
reason: string,
|
|
328
|
+
reason: string,
|
|
329
|
+
maxAttempts: number,
|
|
305
330
|
): boolean {
|
|
306
331
|
// Write a blocker task summary if missing.
|
|
307
332
|
if (!status.summaryExists) {
|
|
@@ -343,48 +368,6 @@ export function skipExecuteTask(
|
|
|
343
368
|
return true;
|
|
344
369
|
}
|
|
345
370
|
|
|
346
|
-
// ─── Disk-backed completed-unit helpers ───────────────────────────────────────
|
|
347
|
-
|
|
348
|
-
function isStringArray(data: unknown): data is string[] {
|
|
349
|
-
return Array.isArray(data) && data.every(item => typeof item === "string");
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/** Path to the persisted completed-unit keys file. */
|
|
353
|
-
export function completedKeysPath(base: string): string {
|
|
354
|
-
return join(gsdRoot(base), "completed-units.json");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/** Write a completed unit key to disk (read-modify-write append to set). */
|
|
358
|
-
export function persistCompletedKey(base: string, key: string): void {
|
|
359
|
-
const file = completedKeysPath(base);
|
|
360
|
-
const keys = loadJsonFileOrNull(file, isStringArray) ?? [];
|
|
361
|
-
const keySet = new Set(keys);
|
|
362
|
-
if (!keySet.has(key)) {
|
|
363
|
-
keys.push(key);
|
|
364
|
-
atomicWriteSync(file, JSON.stringify(keys));
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** Remove a stale completed unit key from disk. */
|
|
369
|
-
export function removePersistedKey(base: string, key: string): void {
|
|
370
|
-
const file = completedKeysPath(base);
|
|
371
|
-
const keys = loadJsonFileOrNull(file, isStringArray);
|
|
372
|
-
if (!keys) return;
|
|
373
|
-
const filtered = keys.filter(k => k !== key);
|
|
374
|
-
if (filtered.length !== keys.length) {
|
|
375
|
-
atomicWriteSync(file, JSON.stringify(filtered));
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/** Load all completed unit keys from disk into the in-memory set. */
|
|
380
|
-
export function loadPersistedKeys(base: string, target: Set<string>): void {
|
|
381
|
-
const file = completedKeysPath(base);
|
|
382
|
-
const keys = loadJsonFileOrNull(file, isStringArray);
|
|
383
|
-
if (keys) {
|
|
384
|
-
for (const k of keys) target.add(k);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
371
|
// ─── Merge State Reconciliation ───────────────────────────────────────────────
|
|
389
372
|
|
|
390
373
|
/**
|
|
@@ -394,7 +377,10 @@ export function loadPersistedKeys(base: string, target: Set<string>): void {
|
|
|
394
377
|
*
|
|
395
378
|
* Returns true if state was dirty and re-derivation is needed.
|
|
396
379
|
*/
|
|
397
|
-
export function reconcileMergeState(
|
|
380
|
+
export function reconcileMergeState(
|
|
381
|
+
basePath: string,
|
|
382
|
+
ctx: ExtensionContext,
|
|
383
|
+
): boolean {
|
|
398
384
|
const mergeHeadPath = join(basePath, ".git", "MERGE_HEAD");
|
|
399
385
|
const squashMsgPath = join(basePath, ".git", "SQUASH_MSG");
|
|
400
386
|
const hasMergeHead = existsSync(mergeHeadPath);
|
|
@@ -405,7 +391,7 @@ export function reconcileMergeState(basePath: string, ctx: ExtensionContext): bo
|
|
|
405
391
|
if (conflictedFiles.length === 0) {
|
|
406
392
|
// All conflicts resolved — finalize the merge/squash commit
|
|
407
393
|
try {
|
|
408
|
-
nativeCommit(basePath, "");
|
|
394
|
+
nativeCommit(basePath, ""); // --no-edit equivalent: use empty message placeholder
|
|
409
395
|
const mode = hasMergeHead ? "merge" : "squash commit";
|
|
410
396
|
ctx.ui.notify(`Finalized leftover ${mode} from prior session.`, "info");
|
|
411
397
|
} catch {
|
|
@@ -413,8 +399,8 @@ export function reconcileMergeState(basePath: string, ctx: ExtensionContext): bo
|
|
|
413
399
|
}
|
|
414
400
|
} else {
|
|
415
401
|
// Still conflicted — try auto-resolving .gsd/ state file conflicts (#530)
|
|
416
|
-
const gsdConflicts = conflictedFiles.filter(f => f.startsWith(".gsd/"));
|
|
417
|
-
const codeConflicts = conflictedFiles.filter(f => !f.startsWith(".gsd/"));
|
|
402
|
+
const gsdConflicts = conflictedFiles.filter((f) => f.startsWith(".gsd/"));
|
|
403
|
+
const codeConflicts = conflictedFiles.filter((f) => !f.startsWith(".gsd/"));
|
|
418
404
|
|
|
419
405
|
if (gsdConflicts.length > 0 && codeConflicts.length === 0) {
|
|
420
406
|
// All conflicts are in .gsd/ state files — auto-resolve by accepting theirs
|
|
@@ -427,7 +413,10 @@ export function reconcileMergeState(basePath: string, ctx: ExtensionContext): bo
|
|
|
427
413
|
}
|
|
428
414
|
if (resolved) {
|
|
429
415
|
try {
|
|
430
|
-
nativeCommit(
|
|
416
|
+
nativeCommit(
|
|
417
|
+
basePath,
|
|
418
|
+
"chore: auto-resolve .gsd/ state file conflicts",
|
|
419
|
+
);
|
|
431
420
|
ctx.ui.notify(
|
|
432
421
|
`Auto-resolved ${gsdConflicts.length} .gsd/ state file conflict(s) from prior merge.`,
|
|
433
422
|
"info",
|
|
@@ -438,11 +427,23 @@ export function reconcileMergeState(basePath: string, ctx: ExtensionContext): bo
|
|
|
438
427
|
}
|
|
439
428
|
if (!resolved) {
|
|
440
429
|
if (hasMergeHead) {
|
|
441
|
-
try {
|
|
430
|
+
try {
|
|
431
|
+
nativeMergeAbort(basePath);
|
|
432
|
+
} catch {
|
|
433
|
+
/* best-effort */
|
|
434
|
+
}
|
|
442
435
|
} else if (hasSquashMsg) {
|
|
443
|
-
try {
|
|
436
|
+
try {
|
|
437
|
+
unlinkSync(squashMsgPath);
|
|
438
|
+
} catch {
|
|
439
|
+
/* best-effort */
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
nativeResetHard(basePath);
|
|
444
|
+
} catch {
|
|
445
|
+
/* best-effort */
|
|
444
446
|
}
|
|
445
|
-
try { nativeResetHard(basePath); } catch { /* best-effort */ }
|
|
446
447
|
ctx.ui.notify(
|
|
447
448
|
"Detected leftover merge state — auto-resolve failed, cleaned up. Re-deriving state.",
|
|
448
449
|
"warning",
|
|
@@ -451,11 +452,23 @@ export function reconcileMergeState(basePath: string, ctx: ExtensionContext): bo
|
|
|
451
452
|
} else {
|
|
452
453
|
// Code conflicts present — abort and reset
|
|
453
454
|
if (hasMergeHead) {
|
|
454
|
-
try {
|
|
455
|
+
try {
|
|
456
|
+
nativeMergeAbort(basePath);
|
|
457
|
+
} catch {
|
|
458
|
+
/* best-effort */
|
|
459
|
+
}
|
|
455
460
|
} else if (hasSquashMsg) {
|
|
456
|
-
try {
|
|
461
|
+
try {
|
|
462
|
+
unlinkSync(squashMsgPath);
|
|
463
|
+
} catch {
|
|
464
|
+
/* best-effort */
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
nativeResetHard(basePath);
|
|
469
|
+
} catch {
|
|
470
|
+
/* best-effort */
|
|
457
471
|
}
|
|
458
|
-
try { nativeResetHard(basePath); } catch { /* best-effort */ }
|
|
459
472
|
ctx.ui.notify(
|
|
460
473
|
"Detected leftover merge state with unresolved conflicts — cleaned up. Re-deriving state.",
|
|
461
474
|
"warning",
|
|
@@ -468,14 +481,14 @@ export function reconcileMergeState(basePath: string, ctx: ExtensionContext): bo
|
|
|
468
481
|
// ─── Self-Heal Runtime Records ────────────────────────────────────────────────
|
|
469
482
|
|
|
470
483
|
/**
|
|
471
|
-
* Self-heal: scan runtime records in .gsd/ and clear
|
|
472
|
-
*
|
|
473
|
-
*
|
|
484
|
+
* Self-heal: scan runtime records in .gsd/ and clear stale ones.
|
|
485
|
+
* Clears dispatched records older than 1 hour (process crashed before
|
|
486
|
+
* completing the unit). deriveState() handles re-derivation — no need
|
|
487
|
+
* for completion key persistence here.
|
|
474
488
|
*/
|
|
475
489
|
export async function selfHealRuntimeRecords(
|
|
476
490
|
base: string,
|
|
477
491
|
ctx: ExtensionContext,
|
|
478
|
-
completedKeySet: Set<string>,
|
|
479
492
|
): Promise<void> {
|
|
480
493
|
try {
|
|
481
494
|
const { listUnitRuntimeRecords } = await import("./unit-runtime.js");
|
|
@@ -485,26 +498,8 @@ export async function selfHealRuntimeRecords(
|
|
|
485
498
|
const now = Date.now();
|
|
486
499
|
for (const record of records) {
|
|
487
500
|
const { unitType, unitId } = record;
|
|
488
|
-
const artifactPath = resolveExpectedArtifactPath(unitType, unitId, base);
|
|
489
501
|
|
|
490
|
-
//
|
|
491
|
-
// Use verifyExpectedArtifact (not just existsSync) so that execute-task
|
|
492
|
-
// also checks the plan checkbox is marked [x]. Without this, a task
|
|
493
|
-
// whose summary exists but checkbox is unchecked would be incorrectly
|
|
494
|
-
// marked as completed, causing deriveState to re-dispatch it endlessly.
|
|
495
|
-
if (artifactPath && existsSync(artifactPath) && verifyExpectedArtifact(unitType, unitId, base)) {
|
|
496
|
-
clearUnitRuntimeRecord(base, unitType, unitId);
|
|
497
|
-
// Also persist completion key if missing
|
|
498
|
-
const key = `${unitType}/${unitId}`;
|
|
499
|
-
if (!completedKeySet.has(key)) {
|
|
500
|
-
persistCompletedKey(base, key);
|
|
501
|
-
completedKeySet.add(key);
|
|
502
|
-
}
|
|
503
|
-
healed++;
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Case 2: No artifact but record is stale (dispatched > 1h ago, process crashed)
|
|
502
|
+
// Clear stale dispatched records (dispatched > 1h ago, process crashed)
|
|
508
503
|
const age = now - (record.startedAt ?? 0);
|
|
509
504
|
if (record.phase === "dispatched" && age > STALE_THRESHOLD_MS) {
|
|
510
505
|
clearUnitRuntimeRecord(base, unitType, unitId);
|
|
@@ -513,7 +508,10 @@ export async function selfHealRuntimeRecords(
|
|
|
513
508
|
}
|
|
514
509
|
}
|
|
515
510
|
if (healed > 0) {
|
|
516
|
-
ctx.ui.notify(
|
|
511
|
+
ctx.ui.notify(
|
|
512
|
+
`Self-heal: cleared ${healed} stale runtime record(s).`,
|
|
513
|
+
"info",
|
|
514
|
+
);
|
|
517
515
|
}
|
|
518
516
|
} catch (e) {
|
|
519
517
|
// Non-fatal — self-heal should never block auto-mode start
|
|
@@ -527,8 +525,15 @@ export async function selfHealRuntimeRecords(
|
|
|
527
525
|
* Build concrete, manual remediation steps for a loop-detected unit failure.
|
|
528
526
|
* These are shown when automatic reconciliation is not possible.
|
|
529
527
|
*/
|
|
530
|
-
export function buildLoopRemediationSteps(
|
|
531
|
-
|
|
528
|
+
export function buildLoopRemediationSteps(
|
|
529
|
+
unitType: string,
|
|
530
|
+
unitId: string,
|
|
531
|
+
base: string,
|
|
532
|
+
): string | null {
|
|
533
|
+
const parts = unitId.split("/");
|
|
534
|
+
const mid = parts[0];
|
|
535
|
+
const sid = parts[1];
|
|
536
|
+
const tid = parts[2];
|
|
532
537
|
switch (unitType) {
|
|
533
538
|
case "execute-task": {
|
|
534
539
|
if (!mid || !sid || !tid) break;
|
|
@@ -544,9 +549,10 @@ export function buildLoopRemediationSteps(unitType: string, unitId: string, base
|
|
|
544
549
|
case "plan-slice":
|
|
545
550
|
case "research-slice": {
|
|
546
551
|
if (!mid || !sid) break;
|
|
547
|
-
const artifactRel =
|
|
548
|
-
|
|
549
|
-
|
|
552
|
+
const artifactRel =
|
|
553
|
+
unitType === "plan-slice"
|
|
554
|
+
? relSliceFile(base, mid, sid, "PLAN")
|
|
555
|
+
: relSliceFile(base, mid, sid, "RESEARCH");
|
|
550
556
|
return [
|
|
551
557
|
` 1. Write ${artifactRel} manually (or with the LLM in interactive mode)`,
|
|
552
558
|
` 2. Run \`gsd doctor\` to reconcile .gsd/ state`,
|