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,17 +1,16 @@
1
1
  /**
2
- * Auto-mode Supervisor — signal handling and working-tree activity detection.
2
+ * Auto-mode Supervisor — SIGTERM handling and working-tree activity detection.
3
3
  *
4
4
  * Pure functions — no module-level globals or AutoContext dependency.
5
5
  */
6
6
 
7
7
  import { clearLock } from "./crash-recovery.js";
8
- import { releaseSessionLock } from "./session-lock.js";
9
8
  import { nativeHasChanges } from "./native-git-bridge.js";
10
9
 
11
- // ─── Signal Handling ──────────────────────────────────────────────────────────
10
+ // ─── SIGTERM Handling ─────────────────────────────────────────────────────────
12
11
 
13
12
  /**
14
- * Register SIGTERM and SIGINT handlers that clear lock files and exit cleanly.
13
+ * Register a SIGTERM handler that clears the lock file and exits cleanly.
15
14
  * Captures the active base path at registration time so the handler
16
15
  * always references the correct path even if the module variable changes.
17
16
  * Removes any previously registered handler before installing the new one.
@@ -22,25 +21,19 @@ export function registerSigtermHandler(
22
21
  currentBasePath: string,
23
22
  previousHandler: (() => void) | null,
24
23
  ): () => void {
25
- if (previousHandler) {
26
- process.off("SIGTERM", previousHandler);
27
- process.off("SIGINT", previousHandler);
28
- }
24
+ if (previousHandler) process.off("SIGTERM", previousHandler);
29
25
  const handler = () => {
30
- releaseSessionLock(currentBasePath);
31
26
  clearLock(currentBasePath);
32
27
  process.exit(0);
33
28
  };
34
29
  process.on("SIGTERM", handler);
35
- process.on("SIGINT", handler);
36
30
  return handler;
37
31
  }
38
32
 
39
- /** Deregister signal handlers (called on stop/pause). */
33
+ /** Deregister the SIGTERM handler (called on stop/pause). */
40
34
  export function deregisterSigtermHandler(handler: (() => void) | null): void {
41
35
  if (handler) {
42
36
  process.off("SIGTERM", handler);
43
- process.off("SIGINT", handler);
44
37
  }
45
38
  }
46
39
 
@@ -18,14 +18,14 @@ import {
18
18
  writeBlockerPlaceholder,
19
19
  } from "./auto-recovery.js";
20
20
  import { existsSync } from "node:fs";
21
- import { parseUnitId } from "./unit-id.js";
21
+
22
+ import { resolveAgentEnd } from "./auto-loop.js";
22
23
 
23
24
  export interface RecoveryContext {
24
25
  basePath: string;
25
26
  verbose: boolean;
26
27
  currentUnitStartedAt: number;
27
28
  unitRecoveryCount: Map<string, number>;
28
- dispatchNextUnit: (ctx: ExtensionContext, pi: ExtensionAPI) => Promise<void>;
29
29
  }
30
30
 
31
31
  export async function recoverTimedOutUnit(
@@ -36,7 +36,7 @@ export async function recoverTimedOutUnit(
36
36
  reason: "idle" | "hard",
37
37
  rctx: RecoveryContext,
38
38
  ): Promise<"recovered" | "paused"> {
39
- const { basePath, verbose, currentUnitStartedAt, unitRecoveryCount, dispatchNextUnit } = rctx;
39
+ const { basePath, verbose, currentUnitStartedAt, unitRecoveryCount } = rctx;
40
40
 
41
41
  const runtime = readUnitRuntimeRecord(basePath, unitType, unitId);
42
42
  const recoveryAttempts = runtime?.recoveryAttempts ?? 0;
@@ -75,7 +75,7 @@ export async function recoverTimedOutUnit(
75
75
  "info",
76
76
  );
77
77
  unitRecoveryCount.delete(recoveryKey);
78
- await dispatchNextUnit(ctx, pi);
78
+ resolveAgentEnd({ messages: [], _synthetic: "timeout-recovery" } as any);
79
79
  return "recovered";
80
80
  }
81
81
 
@@ -129,7 +129,7 @@ export async function recoverTimedOutUnit(
129
129
 
130
130
  // Retries exhausted — write missing durable artifacts and advance.
131
131
  const diagnostic = formatExecuteTaskRecoveryStatus(status);
132
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
132
+ const [mid, sid, tid] = unitId.split("/");
133
133
  const skipped = mid && sid && tid
134
134
  ? skipExecuteTask(basePath, mid, sid, tid, status, reason, maxRecoveryAttempts)
135
135
  : false;
@@ -146,7 +146,7 @@ export async function recoverTimedOutUnit(
146
146
  "warning",
147
147
  );
148
148
  unitRecoveryCount.delete(recoveryKey);
149
- await dispatchNextUnit(ctx, pi);
149
+ resolveAgentEnd({ messages: [], _synthetic: "timeout-recovery" } as any);
150
150
  return "recovered";
151
151
  }
152
152
 
@@ -180,7 +180,7 @@ export async function recoverTimedOutUnit(
180
180
  "info",
181
181
  );
182
182
  unitRecoveryCount.delete(recoveryKey);
183
- await dispatchNextUnit(ctx, pi);
183
+ resolveAgentEnd({ messages: [], _synthetic: "timeout-recovery" } as any);
184
184
  return "recovered";
185
185
  }
186
186
 
@@ -249,7 +249,7 @@ export async function recoverTimedOutUnit(
249
249
  "warning",
250
250
  );
251
251
  unitRecoveryCount.delete(recoveryKey);
252
- await dispatchNextUnit(ctx, pi);
252
+ resolveAgentEnd({ messages: [], _synthetic: "timeout-recovery" } as any);
253
253
  return "recovered";
254
254
  }
255
255
 
@@ -2,7 +2,7 @@
2
2
  * Unit supervision timers — soft timeout warning, idle watchdog,
3
3
  * hard timeout, and context-pressure monitor.
4
4
  *
5
- * Extracted from dispatchNextUnit() in auto.ts. All timers are set up
5
+ * Originally extracted from dispatchNextUnit() in auto.ts (now deleted replaced by autoLoop).
6
6
  * via startUnitSupervision() and torn down by the caller via clearUnitTimeout().
7
7
  */
8
8
 
@@ -20,7 +20,6 @@ import { closeoutUnit, type CloseoutOptions } from "./auto-unit-closeout.js";
20
20
  import { saveActivityLog } from "./activity-log.js";
21
21
  import { recoverTimedOutUnit, type RecoveryContext } from "./auto-timeout-recovery.js";
22
22
  import type { AutoSession } from "./auto/session.js";
23
- import { getErrorMessage } from "./error-utils.js";
24
23
 
25
24
  export interface SupervisionContext {
26
25
  s: AutoSession;
@@ -128,7 +127,7 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
128
127
  );
129
128
  await pauseAuto(ctx, pi);
130
129
  } catch (err) {
131
- const message = getErrorMessage(err);
130
+ const message = err instanceof Error ? err.message : String(err);
132
131
  console.error(`[idle-watchdog] Unhandled error: ${message}`);
133
132
  try {
134
133
  ctx.ui.notify(`Idle watchdog error: ${message}`, "warning");
@@ -160,7 +159,7 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
160
159
  );
161
160
  await pauseAuto(ctx, pi);
162
161
  } catch (err) {
163
- const message = getErrorMessage(err);
162
+ const message = err instanceof Error ? err.message : String(err);
164
163
  console.error(`[hard-timeout] Unhandled error: ${message}`);
165
164
  try {
166
165
  ctx.ui.notify(`Hard timeout error: ${message}`, "warning");
@@ -21,11 +21,8 @@ import {
21
21
  runDependencyAudit,
22
22
  } from "./verification-gate.js";
23
23
  import { writeVerificationJSON } from "./verification-evidence.js";
24
- import { removePersistedKey } from "./auto-recovery.js";
25
- import type { AutoSession, PendingVerificationRetry } from "./auto/session.js";
24
+ import type { AutoSession } from "./auto/session.js";
26
25
  import { join } from "node:path";
27
- import { getErrorMessage } from "./error-utils.js";
28
- import { parseUnitId } from "./unit-id.js";
29
26
 
30
27
  export interface VerificationContext {
31
28
  s: AutoSession;
@@ -35,17 +32,21 @@ export interface VerificationContext {
35
32
 
36
33
  export type VerificationResult = "continue" | "retry" | "pause";
37
34
 
35
+ function isInfraVerificationFailure(stderr: string): boolean {
36
+ return /\b(ENOENT|ENOTFOUND|ETIMEDOUT|ECONNRESET|EAI_AGAIN|spawn\s+\S+\s+ENOENT|command not found)\b/i.test(
37
+ stderr,
38
+ );
39
+ }
40
+
38
41
  /**
39
42
  * Run the verification gate for the current execute-task unit.
40
43
  * Returns:
41
44
  * - "continue" — gate passed (or no checks configured), proceed normally
42
- * - "retry" — gate failed with retries remaining, dispatchNextUnit already called
45
+ * - "retry" — gate failed with retries remaining, s.pendingVerificationRetry set for loop re-iteration
43
46
  * - "pause" — gate failed with retries exhausted, pauseAuto already called
44
47
  */
45
48
  export async function runPostUnitVerification(
46
49
  vctx: VerificationContext,
47
- dispatchNextUnit: (ctx: ExtensionContext, pi: ExtensionAPI) => Promise<void>,
48
- startDispatchGapWatchdog: (ctx: ExtensionContext, pi: ExtensionAPI) => void,
49
50
  pauseAuto: (ctx?: ExtensionContext, pi?: ExtensionAPI) => Promise<void>,
50
51
  ): Promise<VerificationResult> {
51
52
  const { s, ctx, pi } = vctx;
@@ -59,15 +60,16 @@ export async function runPostUnitVerification(
59
60
  const prefs = effectivePrefs?.preferences;
60
61
 
61
62
  // Read task plan verify field
62
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(s.currentUnit.id);
63
+ const parts = s.currentUnit.id.split("/");
63
64
  let taskPlanVerify: string | undefined;
64
- if (mid && sid && tid) {
65
+ if (parts.length >= 3) {
66
+ const [mid, sid, tid] = parts;
65
67
  const planFile = resolveSliceFile(s.basePath, mid, sid, "PLAN");
66
68
  if (planFile) {
67
69
  const planContent = await loadFile(planFile);
68
70
  if (planContent) {
69
71
  const slicePlan = parsePlan(planContent);
70
- const taskEntry = slicePlan?.tasks?.find(t => t.id === tid);
72
+ const taskEntry = slicePlan?.tasks?.find((t) => t.id === tid);
71
73
  taskPlanVerify = taskEntry?.verify;
72
74
  }
73
75
  }
@@ -85,7 +87,7 @@ export async function runPostUnitVerification(
85
87
  const runtimeErrors = await captureRuntimeErrors();
86
88
  if (runtimeErrors.length > 0) {
87
89
  result.runtimeErrors = runtimeErrors;
88
- if (runtimeErrors.some(e => e.blocking)) {
90
+ if (runtimeErrors.some((e) => e.blocking)) {
89
91
  result.passed = false;
90
92
  }
91
93
  }
@@ -94,7 +96,9 @@ export async function runPostUnitVerification(
94
96
  const auditWarnings = runDependencyAudit(s.basePath);
95
97
  if (auditWarnings.length > 0) {
96
98
  result.auditWarnings = auditWarnings;
97
- process.stderr.write(`verification-gate: ${auditWarnings.length} audit warning(s)\n`);
99
+ process.stderr.write(
100
+ `verification-gate: ${auditWarnings.length} audit warning(s)\n`,
101
+ );
98
102
  for (const w of auditWarnings) {
99
103
  process.stderr.write(` [${w.severity}] ${w.name}: ${w.title}\n`);
100
104
  }
@@ -102,59 +106,49 @@ export async function runPostUnitVerification(
102
106
 
103
107
  // Auto-fix retry preferences
104
108
  const autoFixEnabled = prefs?.verification_auto_fix !== false;
105
- const maxRetries = typeof prefs?.verification_max_retries === "number" ? prefs.verification_max_retries : 2;
106
- const completionKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
109
+ const maxRetries =
110
+ typeof prefs?.verification_max_retries === "number"
111
+ ? prefs.verification_max_retries
112
+ : 2;
107
113
 
108
114
  if (result.checks.length > 0) {
109
- const blockingChecks = result.checks.filter(c => c.blocking);
110
- const advisoryChecks = result.checks.filter(c => !c.blocking);
111
- const blockingPassCount = blockingChecks.filter(c => c.exitCode === 0).length;
112
- const advisoryFailCount = advisoryChecks.filter(c => c.exitCode !== 0).length;
113
-
115
+ const passCount = result.checks.filter((c) => c.exitCode === 0).length;
116
+ const total = result.checks.length;
114
117
  if (result.passed) {
115
- let msg = blockingChecks.length > 0
116
- ? `Verification gate: ${blockingPassCount}/${blockingChecks.length} blocking checks passed`
117
- : `Verification gate: passed (no blocking checks)`;
118
- if (advisoryFailCount > 0) {
119
- msg += ` (${advisoryFailCount} advisory warning${advisoryFailCount > 1 ? "s" : ""})`;
120
- }
121
- ctx.ui.notify(msg);
122
- // Log advisory warnings to stderr for visibility
123
- if (advisoryFailCount > 0) {
124
- const advisoryFailures = advisoryChecks.filter(c => c.exitCode !== 0);
125
- process.stderr.write(`verification-gate: ${advisoryFailCount} advisory (non-blocking) failure(s)\n`);
126
- for (const f of advisoryFailures) {
127
- process.stderr.write(` [advisory] ${f.command} exited ${f.exitCode}\n`);
128
- }
129
- }
118
+ ctx.ui.notify(`Verification gate: ${passCount}/${total} checks passed`);
130
119
  } else {
131
- const blockingFailures = blockingChecks.filter(c => c.exitCode !== 0);
132
- const failNames = blockingFailures.map(f => f.command).join(", ");
120
+ const failures = result.checks.filter((c) => c.exitCode !== 0);
121
+ const failNames = failures.map((f) => f.command).join(", ");
133
122
  ctx.ui.notify(`Verification gate: FAILED — ${failNames}`);
134
- process.stderr.write(`verification-gate: ${blockingFailures.length}/${blockingChecks.length} blocking checks failed\n`);
135
- for (const f of blockingFailures) {
123
+ process.stderr.write(
124
+ `verification-gate: ${total - passCount}/${total} checks failed\n`,
125
+ );
126
+ for (const f of failures) {
136
127
  process.stderr.write(` ${f.command} exited ${f.exitCode}\n`);
137
- if (f.stderr) process.stderr.write(` stderr: ${f.stderr.slice(0, 500)}\n`);
138
- }
139
- if (advisoryFailCount > 0) {
140
- process.stderr.write(`verification-gate: ${advisoryFailCount} additional advisory (non-blocking) failure(s)\n`);
128
+ if (f.stderr)
129
+ process.stderr.write(` stderr: ${f.stderr.slice(0, 500)}\n`);
141
130
  }
142
131
  }
143
132
  }
144
133
 
145
134
  // Log blocking runtime errors
146
- if (result.runtimeErrors?.some(e => e.blocking)) {
147
- const blockingErrors = result.runtimeErrors.filter(e => e.blocking);
148
- process.stderr.write(`verification-gate: ${blockingErrors.length} blocking runtime error(s) detected\n`);
135
+ if (result.runtimeErrors?.some((e) => e.blocking)) {
136
+ const blockingErrors = result.runtimeErrors.filter((e) => e.blocking);
137
+ process.stderr.write(
138
+ `verification-gate: ${blockingErrors.length} blocking runtime error(s) detected\n`,
139
+ );
149
140
  for (const err of blockingErrors) {
150
- process.stderr.write(` [${err.source}] ${err.severity}: ${err.message.slice(0, 200)}\n`);
141
+ process.stderr.write(
142
+ ` [${err.source}] ${err.severity}: ${err.message.slice(0, 200)}\n`,
143
+ );
151
144
  }
152
145
  }
153
146
 
154
147
  // Write verification evidence JSON
155
148
  const attempt = s.verificationRetryCount.get(s.currentUnit.id) ?? 0;
156
- if (mid && sid && tid) {
149
+ if (parts.length >= 3) {
157
150
  try {
151
+ const [mid, sid, tid] = parts;
158
152
  const sDir = resolveSlicePath(s.basePath, mid, sid);
159
153
  if (sDir) {
160
154
  const tasksDir = join(sDir, "tasks");
@@ -162,52 +156,48 @@ export async function runPostUnitVerification(
162
156
  writeVerificationJSON(result, tasksDir, tid, s.currentUnit.id);
163
157
  } else {
164
158
  const nextAttempt = attempt + 1;
165
- writeVerificationJSON(result, tasksDir, tid, s.currentUnit.id, nextAttempt, maxRetries);
159
+ writeVerificationJSON(
160
+ result,
161
+ tasksDir,
162
+ tid,
163
+ s.currentUnit.id,
164
+ nextAttempt,
165
+ maxRetries,
166
+ );
166
167
  }
167
168
  }
168
169
  } catch (evidenceErr) {
169
- process.stderr.write(`verification-evidence: write error — ${(evidenceErr as Error).message}\n`);
170
+ process.stderr.write(
171
+ `verification-evidence: write error — ${(evidenceErr as Error).message}\n`,
172
+ );
170
173
  }
171
174
  }
172
175
 
173
- // ── Auto-fix retry logic ──
174
- if (result.passed) {
175
- s.verificationRetryCount.delete(s.currentUnit.id);
176
- s.pendingVerificationRetry = null;
177
- return "continue";
178
- }
176
+ const advisoryFailure =
177
+ !result.passed &&
178
+ (result.discoverySource === "package-json" ||
179
+ result.checks.some((check) =>
180
+ isInfraVerificationFailure(check.stderr),
181
+ ));
179
182
 
180
- // Check if all failures are infra errors (ETIMEDOUT, ENOENT, etc.).
181
- // Infra errors are transient OS-level problems the agent cannot fix —
182
- // retrying the entire task is wasteful and creates phantom failures.
183
- const failedChecks = result.checks.filter(c => c.exitCode !== 0);
184
- const allInfraErrors = failedChecks.length > 0 && failedChecks.every(c => c.infraError === true);
185
- if (allInfraErrors) {
186
- const infraNames = failedChecks.map(f => f.command).join(", ");
187
- ctx.ui.notify(`Verification gate: infra error (${infraNames}) — skipping retry, not a code issue`, "warning");
188
- process.stderr.write(`verification-gate: all ${failedChecks.length} failure(s) are infra errors — treating as transient, no retry\n`);
183
+ if (advisoryFailure) {
189
184
  s.verificationRetryCount.delete(s.currentUnit.id);
190
185
  s.pendingVerificationRetry = null;
191
- return "continue";
192
- }
193
-
194
- if (result.discoverySource === "package-json") {
195
- // Auto-discovered checks from package.json may fail on pre-existing errors
196
- // that the current task didn't introduce. Don't trigger the retry loop —
197
- // log a warning and let the task proceed (#1186).
198
- process.stderr.write(
199
- `verification-gate: auto-discovered checks failed (source: package-json) — treating as advisory, not blocking\n`,
200
- );
201
186
  ctx.ui.notify(
202
- `Verification: auto-discovered checks failed (pre-existing errors likely). Continuing without retry.`,
187
+ result.discoverySource === "package-json"
188
+ ? "Verification failed in auto-discovered package.json checks — treating as advisory."
189
+ : "Verification failed due to infrastructure/runtime environment issues — treating as advisory.",
203
190
  "warning",
204
191
  );
205
- s.verificationRetryCount.delete(s.currentUnit.id);
206
- s.pendingVerificationRetry = null;
207
192
  return "continue";
208
193
  }
209
194
 
210
- if (autoFixEnabled && attempt + 1 <= maxRetries) {
195
+ // ── Auto-fix retry logic ──
196
+ if (result.passed) {
197
+ s.verificationRetryCount.delete(s.currentUnit.id);
198
+ s.pendingVerificationRetry = null;
199
+ return "continue";
200
+ } else if (autoFixEnabled && attempt + 1 <= maxRetries) {
211
201
  const nextAttempt = attempt + 1;
212
202
  s.verificationRetryCount.set(s.currentUnit.id, nextAttempt);
213
203
  s.pendingVerificationRetry = {
@@ -215,17 +205,11 @@ export async function runPostUnitVerification(
215
205
  failureContext: formatFailureContext(result),
216
206
  attempt: nextAttempt,
217
207
  };
218
- ctx.ui.notify(`Verification failed — auto-fix attempt ${nextAttempt}/${maxRetries}`, "warning");
219
- s.completedKeySet.delete(completionKey);
220
- removePersistedKey(s.basePath, completionKey);
221
- // Dispatch retry immediately
222
- try {
223
- await dispatchNextUnit(ctx, pi);
224
- } catch (retryDispatchErr) {
225
- const msg = getErrorMessage(retryDispatchErr);
226
- ctx.ui.notify(`Verification retry dispatch error: ${msg}`, "error");
227
- startDispatchGapWatchdog(ctx, pi);
228
- }
208
+ ctx.ui.notify(
209
+ `Verification failed — auto-fix attempt ${nextAttempt}/${maxRetries}`,
210
+ "warning",
211
+ );
212
+ // Return "retry" — the autoLoop while loop will re-iterate with the retry context
229
213
  return "retry";
230
214
  } else {
231
215
  // Gate failed, retries exhausted
@@ -241,7 +225,9 @@ export async function runPostUnitVerification(
241
225
  }
242
226
  } catch (err) {
243
227
  // Gate errors are non-fatal
244
- process.stderr.write(`verification-gate: error — ${(err as Error).message}\n`);
228
+ process.stderr.write(
229
+ `verification-gate: error — ${(err as Error).message}\n`,
230
+ );
245
231
  return "continue";
246
232
  }
247
233
  }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Worktree ↔ project root state synchronization for auto-mode.
3
+ *
4
+ * When auto-mode runs inside a worktree, dispatch-critical state files
5
+ * (.gsd/ metadata) diverge between the worktree (where work happens)
6
+ * and the project root (where startAutoMode reads initial state on restart).
7
+ * Without syncing, restarting auto-mode reads stale state from the project
8
+ * root and re-dispatches already-completed units.
9
+ *
10
+ * Also contains resource staleness detection and stale worktree escape.
11
+ */
12
+
13
+ import {
14
+ existsSync,
15
+ mkdirSync,
16
+ readFileSync,
17
+ cpSync,
18
+ unlinkSync,
19
+ readdirSync,
20
+ } from "node:fs";
21
+ import { join, sep as pathSep } from "node:path";
22
+ import { homedir } from "node:os";
23
+ import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
24
+
25
+ // ─── Project Root → Worktree Sync ─────────────────────────────────────────
26
+
27
+ /**
28
+ * Sync milestone artifacts from project root INTO worktree before deriveState.
29
+ * Covers the case where the LLM wrote artifacts to the main repo filesystem
30
+ * (e.g. via absolute paths) but the worktree has stale data. Also deletes
31
+ * gsd.db in the worktree so it rebuilds from fresh disk state (#853).
32
+ * Non-fatal — sync failure should never block dispatch.
33
+ */
34
+ export function syncProjectRootToWorktree(
35
+ projectRoot: string,
36
+ worktreePath: string,
37
+ milestoneId: string | null,
38
+ ): void {
39
+ if (!worktreePath || !projectRoot || worktreePath === projectRoot) return;
40
+ if (!milestoneId) return;
41
+
42
+ const prGsd = join(projectRoot, ".gsd");
43
+ const wtGsd = join(worktreePath, ".gsd");
44
+
45
+ // Copy milestone directory from project root to worktree if the project root
46
+ // has newer artifacts (e.g. slices that don't exist in the worktree yet)
47
+ safeCopyRecursive(
48
+ join(prGsd, "milestones", milestoneId),
49
+ join(wtGsd, "milestones", milestoneId),
50
+ );
51
+
52
+ // Delete worktree gsd.db so it rebuilds from the freshly synced files.
53
+ // Stale DB rows are the root cause of the infinite skip loop (#853).
54
+ try {
55
+ const wtDb = join(wtGsd, "gsd.db");
56
+ if (existsSync(wtDb)) {
57
+ unlinkSync(wtDb);
58
+ }
59
+ } catch {
60
+ /* non-fatal */
61
+ }
62
+ }
63
+
64
+ // ─── Worktree → Project Root Sync ─────────────────────────────────────────
65
+
66
+ /**
67
+ * Sync dispatch-critical .gsd/ state files from worktree to project root.
68
+ * Only runs when inside an auto-worktree (worktreePath differs from projectRoot).
69
+ * Copies: STATE.md + active milestone directory (roadmap, slice plans, task summaries).
70
+ * Non-fatal — sync failure should never block dispatch.
71
+ */
72
+ export function syncStateToProjectRoot(
73
+ worktreePath: string,
74
+ projectRoot: string,
75
+ milestoneId: string | null,
76
+ ): void {
77
+ if (!worktreePath || !projectRoot || worktreePath === projectRoot) return;
78
+ if (!milestoneId) return;
79
+
80
+ const wtGsd = join(worktreePath, ".gsd");
81
+ const prGsd = join(projectRoot, ".gsd");
82
+
83
+ // 1. STATE.md — the quick-glance status used by initial deriveState()
84
+ safeCopy(join(wtGsd, "STATE.md"), join(prGsd, "STATE.md"), { force: true });
85
+
86
+ // 2. Milestone directory — ROADMAP, slice PLANs, task summaries
87
+ // Copy the entire milestone .gsd subtree so deriveState reads current checkboxes
88
+ safeCopyRecursive(
89
+ join(wtGsd, "milestones", milestoneId),
90
+ join(prGsd, "milestones", milestoneId),
91
+ { force: true },
92
+ );
93
+
94
+ // 4. Runtime records — unit dispatch state used by selfHealRuntimeRecords().
95
+ // Without this, a crash during a unit leaves the runtime record only in the
96
+ // worktree. If the next session resolves basePath before worktree re-entry,
97
+ // selfHeal can't find or clear the stale record (#769).
98
+ safeCopyRecursive(
99
+ join(wtGsd, "runtime", "units"),
100
+ join(prGsd, "runtime", "units"),
101
+ { force: true },
102
+ );
103
+ }
104
+
105
+ // ─── Resource Staleness ───────────────────────────────────────────────────
106
+
107
+ /**
108
+ * Read the resource version (semver) from the managed-resources manifest.
109
+ * Uses gsdVersion instead of syncedAt so that launching a second session
110
+ * doesn't falsely trigger staleness (#804).
111
+ */
112
+ export function readResourceVersion(): string | null {
113
+ const agentDir =
114
+ process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
115
+ const manifestPath = join(agentDir, "managed-resources.json");
116
+ try {
117
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
118
+ return typeof manifest?.gsdVersion === "string"
119
+ ? manifest.gsdVersion
120
+ : null;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Check if managed resources have been updated since session start.
128
+ * Returns a warning message if stale, null otherwise.
129
+ */
130
+ export function checkResourcesStale(
131
+ versionOnStart: string | null,
132
+ ): string | null {
133
+ if (versionOnStart === null) return null;
134
+ const current = readResourceVersion();
135
+ if (current === null) return null;
136
+ if (current !== versionOnStart) {
137
+ return "GSD resources were updated since this session started. Restart gsd to load the new code.";
138
+ }
139
+ return null;
140
+ }
141
+
142
+ // ─── Stale Worktree Escape ────────────────────────────────────────────────
143
+
144
+ /**
145
+ * Detect and escape a stale worktree cwd (#608).
146
+ *
147
+ * After milestone completion + merge, the worktree directory is removed but
148
+ * the process cwd may still point inside `.gsd/worktrees/<MID>/`.
149
+ * When a new session starts, `process.cwd()` is passed as `base` to startAuto
150
+ * and all subsequent writes land in the wrong directory. This function detects
151
+ * that scenario and chdir back to the project root.
152
+ *
153
+ * Returns the corrected base path.
154
+ */
155
+ export function escapeStaleWorktree(base: string): string {
156
+ const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
157
+ const idx = base.indexOf(marker);
158
+ if (idx === -1) return base;
159
+
160
+ // base is inside .gsd/worktrees/<something> — extract the project root
161
+ const projectRoot = base.slice(0, idx);
162
+ try {
163
+ process.chdir(projectRoot);
164
+ } catch {
165
+ // If chdir fails, return the original — caller will handle errors downstream
166
+ return base;
167
+ }
168
+ return projectRoot;
169
+ }
170
+
171
+ /**
172
+ * Clean stale runtime unit files for completed milestones.
173
+ *
174
+ * After restart, stale runtime/units/*.json from prior milestones can
175
+ * cause deriveState to resume the wrong milestone (#887). Removes files
176
+ * for milestones that have a SUMMARY (fully complete).
177
+ */
178
+ export function cleanStaleRuntimeUnits(
179
+ gsdRootPath: string,
180
+ hasMilestoneSummary: (mid: string) => boolean,
181
+ ): number {
182
+ const runtimeUnitsDir = join(gsdRootPath, "runtime", "units");
183
+ if (!existsSync(runtimeUnitsDir)) return 0;
184
+
185
+ let cleaned = 0;
186
+ try {
187
+ for (const file of readdirSync(runtimeUnitsDir)) {
188
+ if (!file.endsWith(".json")) continue;
189
+ const midMatch = file.match(/(M\d+(?:-[a-z0-9]{6})?)/);
190
+ if (!midMatch) continue;
191
+ if (hasMilestoneSummary(midMatch[1])) {
192
+ try {
193
+ unlinkSync(join(runtimeUnitsDir, file));
194
+ cleaned++;
195
+ } catch {
196
+ /* non-fatal */
197
+ }
198
+ }
199
+ }
200
+ } catch {
201
+ /* non-fatal */
202
+ }
203
+ return cleaned;
204
+ }