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,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auto-mode Supervisor —
|
|
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
|
-
// ───
|
|
10
|
+
// ─── SIGTERM Handling ─────────────────────────────────────────────────────────
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
|
-
* Register SIGTERM
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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,
|
|
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
|
|
63
|
+
const parts = s.currentUnit.id.split("/");
|
|
63
64
|
let taskPlanVerify: string | undefined;
|
|
64
|
-
if (
|
|
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(
|
|
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 =
|
|
106
|
-
|
|
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
|
|
110
|
-
const
|
|
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
|
-
|
|
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
|
|
132
|
-
const failNames =
|
|
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(
|
|
135
|
-
|
|
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)
|
|
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(
|
|
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(
|
|
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 (
|
|
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(
|
|
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(
|
|
170
|
+
process.stderr.write(
|
|
171
|
+
`verification-evidence: write error — ${(evidenceErr as Error).message}\n`,
|
|
172
|
+
);
|
|
170
173
|
}
|
|
171
174
|
}
|
|
172
175
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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(
|
|
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
|
+
}
|