gsd-pi 2.80.0-dev.0d763dc29 → 2.80.0-dev.1486c1899
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/README.md +4 -2
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +22 -15
- package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
- package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
- package/dist/resources/extensions/gsd/auto.js +52 -4
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
- package/dist/resources/extensions/gsd/guided-flow.js +19 -4
- package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/dist/resources/extensions/gsd/quick.js +34 -2
- package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
- package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
- package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
- package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +3 -3
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +22 -17
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
- package/packages/mcp-server/src/workflow-tools.ts +30 -16
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +8 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +8 -0
- package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
- package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/tui.js +1 -1
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +1 -1
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +28 -20
- package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
- package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
- package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
- package/src/resources/extensions/gsd/auto.ts +60 -3
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
- package/src/resources/extensions/gsd/guided-flow.ts +32 -4
- package/src/resources/extensions/gsd/preferences-types.ts +1 -1
- package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/src/resources/extensions/gsd/quick.ts +37 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
- package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
- package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
- package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
- package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
- package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
- package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
- /package/dist/web/standalone/.next/static/{1J8mL3ANp9jqY3SU5BbVW → O-2rnxSJki_W-2fkK3zIj}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{1J8mL3ANp9jqY3SU5BbVW → O-2rnxSJki_W-2fkK3zIj}/_ssgManifest.js +0 -0
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
isLockProcessAlive,
|
|
60
60
|
formatCrashInfo,
|
|
61
61
|
emitCrashRecoveredUnitEnd,
|
|
62
|
+
emitOpenUnitEndForUnit,
|
|
62
63
|
} from "./crash-recovery.js";
|
|
63
64
|
import {
|
|
64
65
|
acquireSessionLock,
|
|
@@ -200,6 +201,8 @@ import {
|
|
|
200
201
|
detectWorkingTreeActivity,
|
|
201
202
|
} from "./auto-supervisor.js";
|
|
202
203
|
import { isDbAvailable, getMilestone } from "./gsd-db.js";
|
|
204
|
+
import { markLatestActiveForWorkerCanceled } from "./db/unit-dispatches.js";
|
|
205
|
+
import { writeUnitRuntimeRecord } from "./unit-runtime.js";
|
|
203
206
|
import { countPendingCaptures } from "./captures.js";
|
|
204
207
|
import { CMUX_CHANNELS, type CmuxLogLevel } from "../shared/cmux-events.js";
|
|
205
208
|
import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
|
|
@@ -293,11 +296,15 @@ const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
|
|
|
293
296
|
* the DB is unavailable (e.g. fresh project before init) we skip registration
|
|
294
297
|
* silently rather than blocking session start.
|
|
295
298
|
*/
|
|
296
|
-
function registerAutoWorkerForSession(
|
|
299
|
+
function registerAutoWorkerForSession(
|
|
300
|
+
session: AutoSession,
|
|
301
|
+
projectRootOverride?: string,
|
|
302
|
+
): void {
|
|
297
303
|
if (session.workerId) return; // already registered (e.g. resume re-runs)
|
|
298
304
|
try {
|
|
299
305
|
const projectRootRealpath = normalizeRealPath(
|
|
300
|
-
|
|
306
|
+
projectRootOverride
|
|
307
|
+
?? session.scope?.workspace.projectRoot
|
|
301
308
|
?? (session.originalBasePath || session.basePath),
|
|
302
309
|
);
|
|
303
310
|
session.workerId = registerAutoWorker({ projectRootRealpath });
|
|
@@ -501,9 +508,54 @@ export {
|
|
|
501
508
|
getBudgetEnforcementAction,
|
|
502
509
|
} from "./auto-budget.js";
|
|
503
510
|
|
|
511
|
+
function closeOutSignalInterruptedUnit(currentBasePath: string): void {
|
|
512
|
+
const currentUnit = s.currentUnit;
|
|
513
|
+
if (!currentUnit) return;
|
|
514
|
+
|
|
515
|
+
const reason = "Auto-mode process received a termination signal";
|
|
516
|
+
const errorContext: ErrorContext = {
|
|
517
|
+
message: reason,
|
|
518
|
+
category: "aborted",
|
|
519
|
+
isTransient: false,
|
|
520
|
+
};
|
|
521
|
+
const basePath = s.basePath || currentBasePath;
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
emitOpenUnitEndForUnit(basePath, currentUnit.type, currentUnit.id, "cancelled", errorContext);
|
|
525
|
+
} catch (err) {
|
|
526
|
+
logWarning("engine", `signal unit-end cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
writeUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id, currentUnit.startedAt, {
|
|
531
|
+
phase: "crashed",
|
|
532
|
+
lastProgressAt: Date.now(),
|
|
533
|
+
lastProgressKind: "signal",
|
|
534
|
+
});
|
|
535
|
+
} catch (err) {
|
|
536
|
+
logWarning("engine", `signal runtime cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
if (s.workerId) markLatestActiveForWorkerCanceled(s.workerId, "signal-exit");
|
|
541
|
+
} catch (err) {
|
|
542
|
+
logWarning("engine", `signal dispatch cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
resolveAgentEndCancelled(errorContext);
|
|
547
|
+
} catch (err) {
|
|
548
|
+
logWarning("engine", `signal resolve cleanup failed: ${getErrorMessage(err)}`, { file: "auto.ts" });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
504
552
|
/** Wrapper: register SIGTERM handler and store reference. */
|
|
505
553
|
function registerSigtermHandler(currentBasePath: string): void {
|
|
506
|
-
s.sigtermHandler = _registerSigtermHandler(
|
|
554
|
+
s.sigtermHandler = _registerSigtermHandler(
|
|
555
|
+
currentBasePath,
|
|
556
|
+
s.sigtermHandler,
|
|
557
|
+
() => closeOutSignalInterruptedUnit(currentBasePath),
|
|
558
|
+
);
|
|
507
559
|
}
|
|
508
560
|
|
|
509
561
|
/** Wrapper: deregister SIGTERM handler and clear reference. */
|
|
@@ -2057,6 +2109,11 @@ export async function startAuto(
|
|
|
2057
2109
|
buildResolver,
|
|
2058
2110
|
};
|
|
2059
2111
|
|
|
2112
|
+
// Register the worker before bootstrap enters a milestone worktree.
|
|
2113
|
+
// This ensures enterMilestone can claim a lease and seed dispatch claims
|
|
2114
|
+
// for crash-recovery fidelity (#5405).
|
|
2115
|
+
registerAutoWorkerForSession(s, base);
|
|
2116
|
+
|
|
2060
2117
|
const ready = await bootstrapAutoSession(
|
|
2061
2118
|
s,
|
|
2062
2119
|
ctx,
|
|
@@ -153,9 +153,29 @@ export async function handleAgentEnd(
|
|
|
153
153
|
if (maybeHandleEmptyIntentTurn(event, isAutoActive())) return;
|
|
154
154
|
|
|
155
155
|
if (!isAutoActive()) return;
|
|
156
|
-
if (isSessionSwitchInFlight()) return;
|
|
157
156
|
|
|
158
157
|
const lastMsg = event.messages[event.messages.length - 1];
|
|
158
|
+
if (isSessionSwitchInFlight()) {
|
|
159
|
+
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
|
|
160
|
+
const rawErrorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
|
|
161
|
+
if (isUserInitiatedAbortMessage(rawErrorMsg)) {
|
|
162
|
+
resolveAgentEndCancelled({
|
|
163
|
+
message: rawErrorMsg,
|
|
164
|
+
category: "aborted",
|
|
165
|
+
isTransient: false,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} else if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
|
|
169
|
+
const content = "content" in lastMsg ? lastMsg.content : undefined;
|
|
170
|
+
const hasEmptyContent = Array.isArray(content) && content.length === 0;
|
|
171
|
+
const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
|
|
172
|
+
if (!hasEmptyContent || hasErrorMessage) {
|
|
173
|
+
resolveAgentEndCancelled(_buildAbortedPauseContext(lastMsg as { errorMessage?: unknown }));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
159
179
|
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
|
|
160
180
|
// Empty content with aborted stopReason is a non-fatal agent stop (the LLM
|
|
161
181
|
// chose to end without producing output). Only pause on genuine fatal aborts
|
|
@@ -6,23 +6,36 @@
|
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
7
|
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
8
8
|
|
|
9
|
+
async function loadContextModePreferences(baseDir: string) {
|
|
10
|
+
const [{ loadEffectiveGSDPreferences }, { logWarning }] = await Promise.all([
|
|
11
|
+
import("../preferences.js"),
|
|
12
|
+
import("../workflow-logger.js"),
|
|
13
|
+
]);
|
|
14
|
+
try {
|
|
15
|
+
return loadEffectiveGSDPreferences(baseDir)?.preferences ?? null;
|
|
16
|
+
} catch (err) {
|
|
17
|
+
logWarning("tool", `Context Mode tool could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
export function registerExecTools(pi: ExtensionAPI): void {
|
|
10
23
|
pi.registerTool({
|
|
11
24
|
name: "gsd_exec",
|
|
12
25
|
label: "Exec (Sandboxed)",
|
|
13
26
|
description:
|
|
14
|
-
"Run a short script (bash/node/python) in a subprocess.
|
|
27
|
+
"Run a short script (bash/node/python) in a subprocess. Capped stdout/stderr and metadata persist to " +
|
|
15
28
|
".gsd/exec/<id>.{stdout,stderr,meta.json}; only a short digest returns in context. Use " +
|
|
16
29
|
"this instead of reading many files or emitting large tool outputs — e.g. have the script " +
|
|
17
30
|
"count/grep/summarize and log the finding. Enabled by default; opt out via " +
|
|
18
31
|
"preferences.context_mode.enabled=false.",
|
|
19
32
|
promptSnippet:
|
|
20
|
-
"Run a bash/node/python script in a sandbox;
|
|
33
|
+
"Run a bash/node/python script in a sandbox; capped output is saved to disk and only a digest returns",
|
|
21
34
|
promptGuidelines: [
|
|
22
35
|
"Prefer gsd_exec for analyses that would otherwise read >3 files or produce large tool output.",
|
|
23
36
|
"Write scripts that log the finding (counts, matches, summaries) rather than raw dumps.",
|
|
24
37
|
"The digest is the last ~300 chars of stdout — size your log output accordingly.",
|
|
25
|
-
"Need
|
|
38
|
+
"Need persisted output? Read the stdout_path returned in details (file on local disk).",
|
|
26
39
|
],
|
|
27
40
|
parameters: Type.Object({
|
|
28
41
|
runtime: Type.Union(
|
|
@@ -40,20 +53,11 @@ export function registerExecTools(pi: ExtensionAPI): void {
|
|
|
40
53
|
),
|
|
41
54
|
}),
|
|
42
55
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
import("../preferences.js"),
|
|
46
|
-
import("../workflow-logger.js"),
|
|
47
|
-
]);
|
|
48
|
-
let prefs: ReturnType<typeof loadEffectiveGSDPreferences> | null = null;
|
|
49
|
-
try {
|
|
50
|
-
prefs = loadEffectiveGSDPreferences();
|
|
51
|
-
} catch (err) {
|
|
52
|
-
logWarning("tool", `gsd_exec could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
|
|
53
|
-
}
|
|
56
|
+
const { executeGsdExec } = await import("../tools/exec-tool.js");
|
|
57
|
+
const baseDir = process.cwd();
|
|
54
58
|
return executeGsdExec(params as Parameters<typeof executeGsdExec>[0], {
|
|
55
|
-
baseDir
|
|
56
|
-
preferences:
|
|
59
|
+
baseDir,
|
|
60
|
+
preferences: await loadContextModePreferences(baseDir),
|
|
57
61
|
});
|
|
58
62
|
},
|
|
59
63
|
});
|
|
@@ -67,7 +71,7 @@ export function registerExecTools(pi: ExtensionAPI): void {
|
|
|
67
71
|
promptSnippet: "Search prior gsd_exec runs by substring, runtime, or failing-only filter",
|
|
68
72
|
promptGuidelines: [
|
|
69
73
|
"Use this before re-running an expensive analysis — the prior run's stdout file may still answer.",
|
|
70
|
-
"The preview shows the trailing ~300 chars of stdout; read stdout_path for
|
|
74
|
+
"The preview shows the trailing ~300 chars of stdout; read stdout_path for persisted output.",
|
|
71
75
|
],
|
|
72
76
|
parameters: Type.Object({
|
|
73
77
|
query: Type.Optional(Type.String({ description: "Substring matched against id and purpose (case-insensitive)." })),
|
|
@@ -81,8 +85,10 @@ export function registerExecTools(pi: ExtensionAPI): void {
|
|
|
81
85
|
}),
|
|
82
86
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
83
87
|
const { executeExecSearch } = await import("../tools/exec-search-tool.js");
|
|
88
|
+
const baseDir = process.cwd();
|
|
84
89
|
return executeExecSearch(params as Parameters<typeof executeExecSearch>[0], {
|
|
85
|
-
baseDir
|
|
90
|
+
baseDir,
|
|
91
|
+
preferences: await loadContextModePreferences(baseDir),
|
|
86
92
|
});
|
|
87
93
|
},
|
|
88
94
|
});
|
|
@@ -102,8 +108,10 @@ export function registerExecTools(pi: ExtensionAPI): void {
|
|
|
102
108
|
parameters: Type.Object({}),
|
|
103
109
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
104
110
|
const { executeResume } = await import("../tools/resume-tool.js");
|
|
111
|
+
const baseDir = process.cwd();
|
|
105
112
|
return executeResume(params as Parameters<typeof executeResume>[0], {
|
|
106
|
-
baseDir
|
|
113
|
+
baseDir,
|
|
114
|
+
preferences: await loadContextModePreferences(baseDir),
|
|
107
115
|
});
|
|
108
116
|
},
|
|
109
117
|
});
|
|
@@ -65,6 +65,26 @@ async function applyDisabledModelProviderPolicy(ctx: ExtensionContext): Promise<
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Bridge `context_management.compaction_threshold_percent` from GSD preferences
|
|
70
|
+
* into the agent's runtime compaction settings (#5475). The preference is
|
|
71
|
+
* validated to (0.5, 0.95) at load time, but defense-in-depth normalization
|
|
72
|
+
* here protects against a stale or hand-edited prefs file. Calling with
|
|
73
|
+
* `undefined` clears any prior override so a removed preference does not leak.
|
|
74
|
+
*/
|
|
75
|
+
async function applyCompactionThresholdOverride(ctx: ExtensionContext): Promise<void> {
|
|
76
|
+
try {
|
|
77
|
+
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
78
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
79
|
+
const raw = prefs?.preferences.context_management?.compaction_threshold_percent;
|
|
80
|
+
const value =
|
|
81
|
+
typeof raw === "number" && Number.isFinite(raw) && raw > 0 && raw < 1 ? raw : undefined;
|
|
82
|
+
ctx.setCompactionThresholdOverride(value);
|
|
83
|
+
} catch {
|
|
84
|
+
// Non-fatal: leave any existing override in place.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
export function resolveNotificationStoreBasePath(cwd: string = process.cwd()): string {
|
|
69
89
|
return resolveWorktreeProjectRoot(cwd);
|
|
70
90
|
}
|
|
@@ -123,6 +143,7 @@ export function registerHooks(
|
|
|
123
143
|
await resetAskUserQuestionsTurnCache();
|
|
124
144
|
await syncServiceTierStatus(ctx);
|
|
125
145
|
await applyDisabledModelProviderPolicy(ctx);
|
|
146
|
+
await applyCompactionThresholdOverride(ctx);
|
|
126
147
|
// Skip MCP auto-prep when running inside an auto-worktree (see session_switch below).
|
|
127
148
|
const { isInAutoWorktree } = await import("../auto-worktree.js");
|
|
128
149
|
if (!isInAutoWorktree(process.cwd())) {
|
|
@@ -172,6 +193,7 @@ export function registerHooks(
|
|
|
172
193
|
clearDiscussionFlowState(process.cwd());
|
|
173
194
|
await syncServiceTierStatus(ctx);
|
|
174
195
|
await applyDisabledModelProviderPolicy(ctx);
|
|
196
|
+
await applyCompactionThresholdOverride(ctx);
|
|
175
197
|
// Skip MCP auto-prep when running inside an auto-worktree. The worktree
|
|
176
198
|
// already has .mcp.json from createAutoWorktree, and re-running the writer
|
|
177
199
|
// post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
|
|
@@ -359,6 +359,47 @@ export function markCanceled(dispatchId: number, reason: string): void {
|
|
|
359
359
|
).run({ ":id": dispatchId, ":ended_at": now, ":reason": reason });
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Best-effort signal/crash cleanup: cancel the latest active dispatch owned by
|
|
364
|
+
* a worker when the process is exiting before the normal loop can settle it.
|
|
365
|
+
*/
|
|
366
|
+
export function markLatestActiveForWorkerCanceled(workerId: string, reason: string): boolean {
|
|
367
|
+
if (!isDbAvailable()) return false;
|
|
368
|
+
const now = new Date().toISOString();
|
|
369
|
+
const db = _getAdapter()!;
|
|
370
|
+
const result = transaction(() => {
|
|
371
|
+
return db.prepare(
|
|
372
|
+
`UPDATE unit_dispatches
|
|
373
|
+
SET status = 'canceled', ended_at = :ended_at, exit_reason = :reason
|
|
374
|
+
WHERE id = (
|
|
375
|
+
SELECT id FROM unit_dispatches
|
|
376
|
+
WHERE worker_id = :worker_id
|
|
377
|
+
AND status IN ('pending','claimed','running')
|
|
378
|
+
ORDER BY id DESC
|
|
379
|
+
LIMIT 1
|
|
380
|
+
)`,
|
|
381
|
+
).run({
|
|
382
|
+
":ended_at": now,
|
|
383
|
+
":reason": reason,
|
|
384
|
+
":worker_id": workerId,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
const changes =
|
|
388
|
+
typeof (result as { changes?: unknown }).changes === "number"
|
|
389
|
+
? (result as { changes: number }).changes
|
|
390
|
+
: 0;
|
|
391
|
+
if (changes <= 0) return false;
|
|
392
|
+
insertAuditEvent({
|
|
393
|
+
eventId: randomUUID(),
|
|
394
|
+
traceId: workerId,
|
|
395
|
+
category: "orchestration",
|
|
396
|
+
type: "dispatch-canceled",
|
|
397
|
+
ts: now,
|
|
398
|
+
payload: { workerId, reason },
|
|
399
|
+
});
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
|
|
362
403
|
/**
|
|
363
404
|
* Fetch the most recent N dispatches for a unit. Used by recordDispatchClaim
|
|
364
405
|
* callers to compute attempt_n and by detect-stuck.ts (B3) to consult
|
|
@@ -87,6 +87,34 @@ import { logWarning } from "./workflow-logger.js";
|
|
|
87
87
|
import { deleteRuntimeKv } from "./db/runtime-kv.js";
|
|
88
88
|
import { PAUSED_SESSION_KV_KEY } from "./interrupted-session.js";
|
|
89
89
|
|
|
90
|
+
type AutoStartOptions = Parameters<typeof startAutoDetached>[4];
|
|
91
|
+
type AutoStartLauncher = typeof startAutoDetached;
|
|
92
|
+
|
|
93
|
+
function scheduleAutoStartAfterIdle(
|
|
94
|
+
ctx: ExtensionCommandContext,
|
|
95
|
+
pi: ExtensionAPI,
|
|
96
|
+
basePath: string,
|
|
97
|
+
verboseMode: boolean,
|
|
98
|
+
options?: AutoStartOptions,
|
|
99
|
+
launch: AutoStartLauncher = startAutoDetached,
|
|
100
|
+
): void {
|
|
101
|
+
const waitForIdle =
|
|
102
|
+
typeof (ctx as { waitForIdle?: unknown }).waitForIdle === "function"
|
|
103
|
+
? ctx.waitForIdle.bind(ctx)
|
|
104
|
+
: async () => {};
|
|
105
|
+
void waitForIdle()
|
|
106
|
+
.then(() => {
|
|
107
|
+
setTimeout(() => launch(ctx, pi, basePath, verboseMode, options), 0);
|
|
108
|
+
})
|
|
109
|
+
.catch((err) => {
|
|
110
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
111
|
+
ctx.ui.notify(`Auto-start failed while waiting for the prior turn to settle: ${message}`, "error");
|
|
112
|
+
logWarning("guided", `auto-start idle wait failed: ${message}`);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const _scheduleAutoStartAfterIdleForTest = scheduleAutoStartAfterIdle;
|
|
117
|
+
|
|
90
118
|
// ─── Scope-based validator wrappers ──────────────────────────────────────────
|
|
91
119
|
// These thin wrappers accept a MilestoneScope so callers that already hold a
|
|
92
120
|
// pinned scope never have to re-derive (basePath, milestoneId) separately.
|
|
@@ -446,7 +474,7 @@ async function dispatchNextDeepProjectSetupStage(entry: PendingDeepProjectSetupE
|
|
|
446
474
|
|
|
447
475
|
if (!hasPendingDeepStage(prefs, entry.basePath)) {
|
|
448
476
|
pendingDeepProjectSetupMap.delete(entry.basePath);
|
|
449
|
-
|
|
477
|
+
scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
|
|
450
478
|
return true;
|
|
451
479
|
}
|
|
452
480
|
|
|
@@ -479,7 +507,7 @@ async function dispatchNextDeepProjectSetupStage(entry: PendingDeepProjectSetupE
|
|
|
479
507
|
entry.ctx.ui.notify(result.reason, result.level);
|
|
480
508
|
} else if (hasPendingDeepStage(prefs, entry.basePath)) {
|
|
481
509
|
pendingDeepProjectSetupMap.delete(entry.basePath);
|
|
482
|
-
|
|
510
|
+
scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
|
|
483
511
|
return true;
|
|
484
512
|
}
|
|
485
513
|
return false;
|
|
@@ -487,7 +515,7 @@ async function dispatchNextDeepProjectSetupStage(entry: PendingDeepProjectSetupE
|
|
|
487
515
|
|
|
488
516
|
if (!USER_DRIVEN_DEEP_SETUP_UNITS.has(result.unitType)) {
|
|
489
517
|
pendingDeepProjectSetupMap.delete(entry.basePath);
|
|
490
|
-
|
|
518
|
+
scheduleAutoStartAfterIdle(entry.ctx, entry.pi, entry.basePath, false, { step: entry.step });
|
|
491
519
|
return true;
|
|
492
520
|
}
|
|
493
521
|
|
|
@@ -693,7 +721,7 @@ export function checkAutoStartAfterDiscuss(): boolean {
|
|
|
693
721
|
|
|
694
722
|
pendingAutoStartMap.delete(basePath);
|
|
695
723
|
ctx.ui.notify(`Milestone ${milestoneId} ready.`, "success");
|
|
696
|
-
|
|
724
|
+
scheduleAutoStartAfterIdle(ctx, pi, basePath, false, { step });
|
|
697
725
|
return true;
|
|
698
726
|
}
|
|
699
727
|
|
|
@@ -32,7 +32,7 @@ export interface ContextManagementConfig {
|
|
|
32
32
|
/**
|
|
33
33
|
* Opt-in tool-output sandboxing for sub-sessions. When enabled, the gsd_exec
|
|
34
34
|
* MCP tool runs scripts in an isolated subprocess and returns only a short
|
|
35
|
-
* digest to the calling agent's context window;
|
|
35
|
+
* digest to the calling agent's context window; capped stdout/stderr persist
|
|
36
36
|
* in the project memory store and can be retrieved by id later.
|
|
37
37
|
*
|
|
38
38
|
* Inspired by mksglu/context-mode (Elastic License 2.0). This is an
|
|
@@ -16,7 +16,7 @@ Dispatch ALL slices simultaneously using the `subagent` tool in **parallel mode*
|
|
|
16
16
|
|
|
17
17
|
1. Call `subagent` with `tasks: [...]` containing one entry per slice below
|
|
18
18
|
2. Wait for ALL subagents to complete
|
|
19
|
-
3. Verify each slice's RESEARCH file was written (check
|
|
19
|
+
3. Verify each slice's RESEARCH file was written (check `.gsd/milestones/{{mid}}/slices/<slice-id>/`)
|
|
20
20
|
4. If a subagent failed to write its RESEARCH file, retry it **once** individually
|
|
21
21
|
5. If it fails a second time, write a partial RESEARCH file for that slice with a `## BLOCKER` section explaining the failure — do NOT retry again
|
|
22
22
|
6. Report which slices completed research and which (if any) needed a blocker note
|
|
@@ -17,11 +17,7 @@ You are executing a GSD quick task — a lightweight, focused unit of work outsi
|
|
|
17
17
|
5. Verify your work:
|
|
18
18
|
- Run tests if applicable.
|
|
19
19
|
- Verify both happy path and failure modes for non-trivial changes.
|
|
20
|
-
6.
|
|
21
|
-
- Use conventional commit messages (feat:, fix:, refactor:, etc.)
|
|
22
|
-
- Stage only relevant files — never commit secrets or runtime files.
|
|
23
|
-
- Commit logical units separately if the task involves distinct changes.
|
|
24
|
-
- Quick tasks run outside the auto-mode lifecycle — there is no system auto-commit, so commit directly here.
|
|
20
|
+
6. {{commitInstruction}}
|
|
25
21
|
7. Write a brief summary to `{{summaryPath}}`:
|
|
26
22
|
- Quick tasks operate outside the milestone/slice/task DB structure, so `gsd_summary_save` (which requires a `milestone_id`) cannot be used here. Write the file directly.
|
|
27
23
|
|
|
@@ -27,13 +27,13 @@ Roadmap, slice summaries, assessments, requirements, decisions, and project cont
|
|
|
27
27
|
Call `subagent` with `tasks: [...]` containing ALL THREE reviewers simultaneously:
|
|
28
28
|
|
|
29
29
|
**Reviewer A - Requirements Coverage**
|
|
30
|
-
Prompt: "Review milestone {{milestoneId}} requirements coverage. Working directory: {{workingDirectory}}. Read `.gsd/
|
|
30
|
+
Prompt: "Review milestone {{milestoneId}} requirements coverage. Working directory: {{workingDirectory}}. Read `.gsd/REQUIREMENTS.md` or use the inlined requirements context. For each requirement, check slice SUMMARY files under `.gsd/milestones/{{milestoneId}}/slices/` and mark COVERED, PARTIAL, or MISSING. Output table: Requirement | Status | Evidence. End with one-line verdict: PASS if all covered, NEEDS-ATTENTION if partials exist, FAIL if any missing."
|
|
31
31
|
|
|
32
32
|
**Reviewer B - Cross-Slice Integration**
|
|
33
33
|
Prompt: "Review milestone {{milestoneId}} cross-slice integration. Working directory: {{workingDirectory}}. Read `{{roadmapPath}}` and find the boundary map (produces/consumes contracts). For each boundary, confirm producer SUMMARY produced the artifact and consumer SUMMARY consumed it. Output table: Boundary | Producer Summary | Consumer Summary | Status. End with one-line verdict: PASS if all boundaries honored, NEEDS-ATTENTION if any gaps."
|
|
34
34
|
|
|
35
35
|
**Reviewer C - Assessment & Acceptance Criteria**
|
|
36
|
-
Prompt: "Review milestone {{milestoneId}} assessment evidence and acceptance criteria. Working directory: {{workingDirectory}}. Read `.gsd/{{milestoneId}}/CONTEXT.md` for criteria. Check slice
|
|
36
|
+
Prompt: "Review milestone {{milestoneId}} assessment evidence and acceptance criteria. Working directory: {{workingDirectory}}. Read `.gsd/milestones/{{milestoneId}}/{{milestoneId}}-CONTEXT.md` for criteria. Check slice SUMMARY/UAT files under `.gsd/milestones/{{milestoneId}}/slices/`. Verify each criterion maps to passing evidence. Then review inlined milestone verification classes. For each non-empty planned class, output table: Class | Planned Check | Evidence | Verdict. Use the exact class names `Contract`, `Integration`, `Operational`, and `UAT` whenever those classes are present. If no verification classes were planned, say that explicitly. Output sections `Acceptance Criteria` with checklist `[ ] Criterion | Evidence`, and `Verification Classes` with the table. End with one-line verdict: PASS if all criteria and classes are covered, NEEDS-ATTENTION if gaps exist."
|
|
37
37
|
|
|
38
38
|
### Step 2 - Synthesize Findings
|
|
39
39
|
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
-
import { join } from "node:path";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
15
15
|
import { loadPrompt } from "./prompt-loader.js";
|
|
16
16
|
import { gsdRoot } from "./paths.js";
|
|
17
17
|
import { GitServiceImpl, runGit } from "./git-service.js";
|
|
@@ -77,6 +77,40 @@ function ensureQuickDir(basePath: string, taskNum: number, slug: string): string
|
|
|
77
77
|
return taskDir;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function isPathInside(parent: string, child: string): boolean {
|
|
81
|
+
const rel = relative(parent, child);
|
|
82
|
+
return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isExternalGsdRoot(basePath: string, root: string): boolean {
|
|
86
|
+
try {
|
|
87
|
+
return !isPathInside(realpathSync(basePath), realpathSync(root));
|
|
88
|
+
} catch {
|
|
89
|
+
return !isPathInside(basePath, root);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildQuickCommitInstruction(basePath: string, root: string): string {
|
|
94
|
+
const externalState = isExternalGsdRoot(basePath, root);
|
|
95
|
+
if (externalState) {
|
|
96
|
+
return [
|
|
97
|
+
"Commit repo changes atomically, but do not stage or commit `.gsd/quick/...`:",
|
|
98
|
+
" - `.gsd/` resolves outside this git repository, so Git cannot stage quick-task summary files from the project repo.",
|
|
99
|
+
" - Write the quick summary file directly at the requested path; that file is persisted by GSD external state.",
|
|
100
|
+
" - Stage and commit only implementation/test/docs files that live inside the repository.",
|
|
101
|
+
" - If the task only writes quick-task research/summary files and no repository files changed, do not run `git commit`; report that there was nothing in the project repo to commit.",
|
|
102
|
+
].join("\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
"Commit your changes atomically:",
|
|
107
|
+
" - Use conventional commit messages (feat:, fix:, refactor:, etc.)",
|
|
108
|
+
" - Stage only relevant files — never commit secrets or runtime files.",
|
|
109
|
+
" - Commit logical units separately if the task involves distinct changes.",
|
|
110
|
+
" - Quick tasks run outside the auto-mode lifecycle — there is no system auto-commit, so commit directly here.",
|
|
111
|
+
].join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
80
114
|
function quickReturnStatePath(basePath: string): string {
|
|
81
115
|
return join(gsdRoot(basePath), "runtime", "quick-return.json");
|
|
82
116
|
}
|
|
@@ -246,6 +280,7 @@ export async function handleQuick(
|
|
|
246
280
|
taskDir: taskDirRel,
|
|
247
281
|
branch: actualBranch,
|
|
248
282
|
summaryPath,
|
|
283
|
+
commitInstruction: buildQuickCommitInstruction(basePath, root),
|
|
249
284
|
date,
|
|
250
285
|
taskNum: String(taskNum),
|
|
251
286
|
slug,
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
_resetPendingResolve,
|
|
11
11
|
_hasPendingResolveForTest,
|
|
12
12
|
_setActiveSession,
|
|
13
|
+
_setSessionSwitchInFlight,
|
|
14
|
+
_consumePendingSwitchCancellation,
|
|
13
15
|
isSessionSwitchInFlight,
|
|
14
16
|
} from "../auto/resolve.js";
|
|
15
17
|
import { runUnit } from "../auto/run-unit.js";
|
|
@@ -206,6 +208,28 @@ test("runUnit returns cancelled when session creation fails", async () => {
|
|
|
206
208
|
assert.equal(pi.calls.length, 0);
|
|
207
209
|
});
|
|
208
210
|
|
|
211
|
+
test("runUnit clears queued switch cancellation when session creation fails", async () => {
|
|
212
|
+
_resetPendingResolve();
|
|
213
|
+
|
|
214
|
+
const ctx = makeMockCtx();
|
|
215
|
+
const pi = makeMockPi();
|
|
216
|
+
const s = makeMockSession({
|
|
217
|
+
newSessionThrows: "connection refused",
|
|
218
|
+
onNewSessionStart: () => {
|
|
219
|
+
resolveAgentEndCancelled({
|
|
220
|
+
message: "Claude Code process aborted by user",
|
|
221
|
+
category: "aborted",
|
|
222
|
+
isTransient: false,
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
228
|
+
|
|
229
|
+
assert.equal(result.status, "cancelled");
|
|
230
|
+
assert.equal(_consumePendingSwitchCancellation(), null);
|
|
231
|
+
});
|
|
232
|
+
|
|
209
233
|
test("runUnit returns cancelled when session creation times out", async () => {
|
|
210
234
|
_resetPendingResolve();
|
|
211
235
|
|
|
@@ -221,6 +245,34 @@ test("runUnit returns cancelled when session creation times out", async () => {
|
|
|
221
245
|
assert.equal(pi.calls.length, 0);
|
|
222
246
|
});
|
|
223
247
|
|
|
248
|
+
test("runUnit consumes a cancellation queued during session switch before dispatch", async () => {
|
|
249
|
+
_resetPendingResolve();
|
|
250
|
+
|
|
251
|
+
const ctx = makeMockCtx();
|
|
252
|
+
const pi = makeMockPi();
|
|
253
|
+
let cancellationQueued = false;
|
|
254
|
+
const s = makeMockSession({
|
|
255
|
+
newSessionDelayMs: 10,
|
|
256
|
+
onNewSessionStart: () => {
|
|
257
|
+
setTimeout(() => {
|
|
258
|
+
cancellationQueued = !resolveAgentEndCancelled({
|
|
259
|
+
message: "Claude Code process aborted by user",
|
|
260
|
+
category: "aborted",
|
|
261
|
+
isTransient: false,
|
|
262
|
+
});
|
|
263
|
+
}, 0);
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const result = await runUnit(ctx, pi, s, "plan-slice", "M009/S01", "prompt");
|
|
268
|
+
|
|
269
|
+
assert.equal(cancellationQueued, true);
|
|
270
|
+
assert.equal(result.status, "cancelled");
|
|
271
|
+
assert.equal(result.errorContext?.category, "aborted");
|
|
272
|
+
assert.equal(result.errorContext?.message, "Claude Code process aborted by user");
|
|
273
|
+
assert.equal(pi.calls.length, 0, "queued switch cancellation must prevent prompt dispatch");
|
|
274
|
+
});
|
|
275
|
+
|
|
224
276
|
test("runUnit keeps the session-switch guard across a late newSession settlement", async () => {
|
|
225
277
|
_resetPendingResolve();
|
|
226
278
|
mock.timers.enable();
|
|
@@ -2089,6 +2141,25 @@ test("resolveAgentEndCancelled without args produces no errorContext field", asy
|
|
|
2089
2141
|
assert.equal(resolved.errorContext, undefined, "errorContext must not be present when no args passed");
|
|
2090
2142
|
});
|
|
2091
2143
|
|
|
2144
|
+
test("resolveAgentEndCancelled queues cancellation that arrives during session switch", () => {
|
|
2145
|
+
_resetPendingResolve();
|
|
2146
|
+
|
|
2147
|
+
_setSessionSwitchInFlight(true);
|
|
2148
|
+
const resolved = resolveAgentEndCancelled({
|
|
2149
|
+
message: "Claude Code process aborted by user",
|
|
2150
|
+
category: "aborted",
|
|
2151
|
+
isTransient: false,
|
|
2152
|
+
});
|
|
2153
|
+
|
|
2154
|
+
assert.equal(resolved, false);
|
|
2155
|
+
const pending = _consumePendingSwitchCancellation();
|
|
2156
|
+
assert.ok(pending?.errorContext, "queued cancellation should preserve errorContext");
|
|
2157
|
+
assert.equal(pending.errorContext.category, "aborted");
|
|
2158
|
+
assert.equal(pending.errorContext.message, "Claude Code process aborted by user");
|
|
2159
|
+
assert.equal(_consumePendingSwitchCancellation(), null);
|
|
2160
|
+
_resetPendingResolve();
|
|
2161
|
+
});
|
|
2162
|
+
|
|
2092
2163
|
// ─── #1571: artifact verification retry ──────────────────────────────────────
|
|
2093
2164
|
|
|
2094
2165
|
test("autoLoop re-iterates when postUnitPreVerification returns retry (#1571)", async () => {
|
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
|
|
8
|
-
import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps, writeBlockerPlaceholder } from "../auto-recovery.ts";
|
|
8
|
+
import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps, writeBlockerPlaceholder, refreshRecoveryDbForArtifact } from "../auto-recovery.ts";
|
|
9
9
|
import { resolveMilestoneFile } from "../paths.ts";
|
|
10
10
|
import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow, insertTask, getMilestoneCommitAttributionShas } from "../gsd-db.ts";
|
|
11
11
|
import { clearParseCache } from "../files.ts";
|
|
@@ -175,6 +175,19 @@ test("resolveExpectedArtifactPath returns correct path for all slice-level types
|
|
|
175
175
|
}
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
+
test("refreshRecoveryDbForArtifact treats missing execute-task DB rows as fatal mismatches", () => {
|
|
179
|
+
makeTmpProject();
|
|
180
|
+
|
|
181
|
+
const result = refreshRecoveryDbForArtifact("execute-task", "M001/S01/T01");
|
|
182
|
+
|
|
183
|
+
assert.deepEqual(result, {
|
|
184
|
+
ok: false,
|
|
185
|
+
fatal: true,
|
|
186
|
+
reason: "execute-task-artifact-db-missing",
|
|
187
|
+
message: "Stuck recovery found execute-task M001/S01/T01 artifacts, but no matching DB task row exists after refresh.",
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
178
191
|
// ─── diagnoseExpectedArtifact ─────────────────────────────────────────────
|
|
179
192
|
|
|
180
193
|
test("diagnoseExpectedArtifact returns description for known types", () => {
|
|
@@ -121,3 +121,14 @@ test('executeResume: reports friendly empty state when no snapshot exists', () =
|
|
|
121
121
|
cleanup(base);
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
|
+
|
|
125
|
+
test('executeResume: returns disabled error when context_mode.enabled=false', () => {
|
|
126
|
+
const base = freshBase();
|
|
127
|
+
try {
|
|
128
|
+
const result = executeResume({}, { baseDir: base, preferences: { context_mode: { enabled: false } } });
|
|
129
|
+
assert.equal(result.isError, true);
|
|
130
|
+
assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
|
|
131
|
+
} finally {
|
|
132
|
+
cleanup(base);
|
|
133
|
+
}
|
|
134
|
+
});
|