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.
Files changed (135) hide show
  1. package/dist/bundled-resource-path.d.ts +8 -0
  2. package/dist/bundled-resource-path.js +14 -0
  3. package/dist/headless-query.js +6 -6
  4. package/dist/resources/extensions/gsd/auto/session.js +27 -32
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
  8. package/dist/resources/extensions/gsd/auto-loop.js +956 -0
  9. package/dist/resources/extensions/gsd/auto-observability.js +4 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
  11. package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
  12. package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
  13. package/dist/resources/extensions/gsd/auto-start.js +330 -309
  14. package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
  16. package/dist/resources/extensions/gsd/auto-timers.js +3 -4
  17. package/dist/resources/extensions/gsd/auto-verification.js +35 -73
  18. package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
  19. package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
  20. package/dist/resources/extensions/gsd/auto.js +283 -1013
  21. package/dist/resources/extensions/gsd/captures.js +10 -4
  22. package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
  23. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  24. package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
  25. package/dist/resources/extensions/gsd/git-service.js +1 -1
  26. package/dist/resources/extensions/gsd/gsd-db.js +296 -151
  27. package/dist/resources/extensions/gsd/index.js +92 -228
  28. package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
  29. package/dist/resources/extensions/gsd/progress-score.js +61 -156
  30. package/dist/resources/extensions/gsd/quick.js +98 -122
  31. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  32. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  33. package/dist/resources/extensions/gsd/undo.js +43 -48
  34. package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
  35. package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
  36. package/dist/resources/extensions/gsd/verification-gate.js +6 -35
  37. package/dist/resources/extensions/gsd/worktree-command.js +30 -24
  38. package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
  40. package/dist/resources/extensions/gsd/worktree.js +7 -44
  41. package/dist/tool-bootstrap.js +59 -11
  42. package/dist/worktree-cli.js +7 -7
  43. package/package.json +1 -1
  44. package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
  45. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/models.generated.js +735 -2588
  47. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  48. package/packages/pi-ai/src/models.generated.ts +1039 -2892
  49. package/packages/pi-coding-agent/package.json +1 -1
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/gsd/auto/session.ts +47 -30
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
  53. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
  54. package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
  55. package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
  56. package/src/resources/extensions/gsd/auto-observability.ts +4 -2
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
  58. package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
  59. package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
  60. package/src/resources/extensions/gsd/auto-start.ts +440 -354
  61. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
  62. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
  63. package/src/resources/extensions/gsd/auto-timers.ts +3 -4
  64. package/src/resources/extensions/gsd/auto-verification.ts +76 -90
  65. package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
  66. package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
  67. package/src/resources/extensions/gsd/auto.ts +515 -1199
  68. package/src/resources/extensions/gsd/captures.ts +10 -4
  69. package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
  70. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  71. package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
  72. package/src/resources/extensions/gsd/git-service.ts +8 -1
  73. package/src/resources/extensions/gsd/gitignore.ts +4 -2
  74. package/src/resources/extensions/gsd/gsd-db.ts +375 -180
  75. package/src/resources/extensions/gsd/index.ts +104 -263
  76. package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
  77. package/src/resources/extensions/gsd/progress-score.ts +65 -200
  78. package/src/resources/extensions/gsd/quick.ts +121 -125
  79. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  80. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
  82. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
  83. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  84. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  85. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
  87. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
  88. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
  89. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
  90. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  91. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
  92. package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
  93. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
  94. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
  95. package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
  96. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
  97. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
  98. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
  99. package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
  100. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
  101. package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
  102. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
  103. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
  104. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  105. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  106. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
  107. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
  108. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
  109. package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
  110. package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
  111. package/src/resources/extensions/gsd/types.ts +90 -81
  112. package/src/resources/extensions/gsd/undo.ts +42 -46
  113. package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
  114. package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
  115. package/src/resources/extensions/gsd/verification-gate.ts +6 -39
  116. package/src/resources/extensions/gsd/worktree-command.ts +36 -24
  117. package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
  119. package/src/resources/extensions/gsd/worktree.ts +7 -44
  120. package/dist/resources/extensions/gsd/auto-constants.js +0 -5
  121. package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
  122. package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
  123. package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
  124. package/src/resources/extensions/gsd/auto-constants.ts +0 -6
  125. package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
  126. package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
  127. package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
  128. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  129. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
  130. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
  131. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
  132. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
  133. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
  134. package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
  135. 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, completed-unit persistence, merge state reconciliation,
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 { isValidationTerminal } from "./state.js";
41
- import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
42
- import { atomicWriteSync } from "./atomic-write.js";
43
- import { loadJsonFileOrNull } from "./json-persistence.js";
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(unitType: string, unitId: string, base: string): string | null {
53
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
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 ? join(dir, "tasks", buildTaskFileName(tid, "SUMMARY")) : null;
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(unitType: string, unitId: string, base: string): boolean {
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
- try {
149
- const validationContent = readFileSync(absPath, "utf-8");
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 { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
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 { milestone: mid, slice: sid } = parseUnitId(unitId);
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 { milestone: mid, slice: sid } = parseUnitId(unitId);
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 = (roadmap.slices ?? []).find(s => s.id === sid);
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(unitType: string, unitId: string, base: string, reason: string): string | null {
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(unitType: string, unitId: string, base: string): string | null {
263
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
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, mid: string, sid: string, tid: string,
323
+ base: string,
324
+ mid: string,
325
+ sid: string,
326
+ tid: string,
303
327
  status: { summaryExists: boolean; taskChecked: boolean },
304
- reason: string, maxAttempts: number,
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(basePath: string, ctx: ExtensionContext): boolean {
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, ""); // --no-edit equivalent: use empty message placeholder
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(basePath, "chore: auto-resolve .gsd/ state file conflicts");
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 { nativeMergeAbort(basePath); } catch { /* best-effort */ }
430
+ try {
431
+ nativeMergeAbort(basePath);
432
+ } catch {
433
+ /* best-effort */
434
+ }
442
435
  } else if (hasSquashMsg) {
443
- try { unlinkSync(squashMsgPath); } catch { /* best-effort */ }
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 { nativeMergeAbort(basePath); } catch { /* best-effort */ }
455
+ try {
456
+ nativeMergeAbort(basePath);
457
+ } catch {
458
+ /* best-effort */
459
+ }
455
460
  } else if (hasSquashMsg) {
456
- try { unlinkSync(squashMsgPath); } catch { /* best-effort */ }
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 any where the expected
472
- * artifact already exists on disk. This repairs incomplete closeouts from
473
- * prior crashes preventing spurious re-dispatch of already-completed units.
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
- // Case 1: Artifact exists unit completed but closeout didn't finish.
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(`Self-heal: cleared ${healed} stale runtime record(s).`, "info");
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(unitType: string, unitId: string, base: string): string | null {
531
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
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 = unitType === "plan-slice"
548
- ? relSliceFile(base, mid, sid, "PLAN")
549
- : relSliceFile(base, mid, sid, "RESEARCH");
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`,