gsd-pi 2.80.0-dev.cf9433f56 → 2.80.0-dev.d4fc28e6b
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/cli.js +0 -19
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +29 -0
- package/dist/resources/extensions/gsd/auto/loop.js +71 -8
- package/dist/resources/extensions/gsd/auto/phases.js +150 -94
- package/dist/resources/extensions/gsd/auto/resolve.js +12 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +10 -30
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto/workflow-dispatch-claim.js +33 -1
- package/dist/resources/extensions/gsd/auto/workflow-worker-heartbeat.js +9 -1
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +5 -32
- package/dist/resources/extensions/gsd/auto-dispatch.js +16 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +17 -4
- package/dist/resources/extensions/gsd/auto-prompts.js +90 -15
- package/dist/resources/extensions/gsd/auto-start.js +197 -6
- package/dist/resources/extensions/gsd/auto-worktree.js +111 -1
- package/dist/resources/extensions/gsd/auto.js +18 -22
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +86 -19
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +49 -36
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +15 -5
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +9 -3
- package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +7 -1
- package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +9 -3
- package/dist/resources/extensions/gsd/bootstrap/query-tools.js +8 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +298 -54
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +82 -23
- package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
- package/dist/resources/extensions/gsd/commands-handlers.js +23 -9
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +53 -0
- package/dist/resources/extensions/gsd/ecosystem/gsd-extension-api.js +2 -0
- package/dist/resources/extensions/gsd/guided-flow.js +47 -28
- package/dist/resources/extensions/gsd/native-git-bridge.js +32 -8
- package/dist/resources/extensions/gsd/orphan-stash-audit.js +101 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +13 -3
- package/dist/resources/extensions/gsd/pre-execution-checks.js +15 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +2 -2
- package/dist/resources/extensions/gsd/workflow-protocol.js +131 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +35 -4
- 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 +12 -12
- 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 +12 -12
- 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/dist/welcome-screen.d.ts +2 -0
- package/dist/welcome-screen.js +9 -7
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +4 -1
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts +5 -0
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +2 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/index.d.ts +1 -0
- package/packages/pi-agent-core/dist/index.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/index.js +2 -0
- package/packages/pi-agent-core/dist/index.js.map +1 -1
- package/packages/pi-agent-core/dist/token-audit.d.ts +47 -0
- package/packages/pi-agent-core/dist/token-audit.d.ts.map +1 -0
- package/packages/pi-agent-core/dist/token-audit.js +221 -0
- package/packages/pi-agent-core/dist/token-audit.js.map +1 -0
- package/packages/pi-agent-core/dist/types.d.ts +9 -0
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.test.ts +128 -0
- package/packages/pi-agent-core/src/agent-loop.ts +4 -1
- package/packages/pi-agent-core/src/agent.ts +8 -0
- package/packages/pi-agent-core/src/index.ts +2 -0
- package/packages/pi-agent-core/src/token-audit.test.ts +189 -0
- package/packages/pi-agent-core/src/token-audit.ts +287 -0
- package/packages/pi-agent-core/src/types.ts +14 -0
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +18 -0
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +36 -7
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -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 -6
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +3 -3
- 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 +32 -1
- 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/hooks-runner.test.js +2 -0
- package/packages/pi-coding-agent/dist/core/hooks-runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js +46 -0
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -2
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +74 -2
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js +22 -0
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +6 -7
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -3
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +25 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +40 -7
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +10 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +3 -3
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -5
- package/packages/pi-coding-agent/src/core/extensions/types.ts +35 -1
- package/packages/pi-coding-agent/src/core/hooks-runner.test.ts +2 -0
- package/packages/pi-coding-agent/src/core/sdk-tool-filter.test.ts +60 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +85 -3
- package/packages/pi-coding-agent/src/core/skill-tool.test.ts +28 -0
- package/packages/pi-coding-agent/src/core/system-prompt.ts +8 -10
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +30 -0
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +26 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
- package/src/resources/extensions/gsd/auto/loop.ts +84 -8
- package/src/resources/extensions/gsd/auto/phases.ts +218 -154
- package/src/resources/extensions/gsd/auto/resolve.ts +19 -0
- package/src/resources/extensions/gsd/auto/run-unit.ts +10 -29
- package/src/resources/extensions/gsd/auto/session.ts +8 -0
- package/src/resources/extensions/gsd/auto/workflow-dispatch-claim.ts +63 -1
- package/src/resources/extensions/gsd/auto/workflow-worker-heartbeat.ts +14 -1
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +8 -34
- package/src/resources/extensions/gsd/auto-dispatch.ts +16 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +18 -4
- package/src/resources/extensions/gsd/auto-prompts.ts +95 -14
- package/src/resources/extensions/gsd/auto-start.ts +230 -9
- package/src/resources/extensions/gsd/auto-worktree.ts +123 -0
- package/src/resources/extensions/gsd/auto.ts +18 -18
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +100 -18
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +50 -36
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +16 -5
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +10 -3
- package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +8 -1
- package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +10 -3
- package/src/resources/extensions/gsd/bootstrap/query-tools.ts +9 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +347 -54
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +90 -22
- package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
- package/src/resources/extensions/gsd/commands-handlers.ts +34 -15
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +66 -0
- package/src/resources/extensions/gsd/ecosystem/gsd-extension-api.ts +3 -0
- package/src/resources/extensions/gsd/guided-flow.ts +52 -35
- package/src/resources/extensions/gsd/native-git-bridge.ts +39 -6
- package/src/resources/extensions/gsd/orphan-stash-audit.ts +117 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +13 -3
- package/src/resources/extensions/gsd/pre-execution-checks.ts +16 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
- package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +361 -10
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +168 -6
- package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
- package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/complete-slice-composer.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/context-store.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/execute-task-rendering.test.ts +5 -2
- package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +219 -0
- package/src/resources/extensions/gsd/tests/finalize-survivor-branch.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +242 -0
- package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +34 -2
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/orphan-merge-bootstrap.test.ts +133 -0
- package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +201 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestrator-fast-forward.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +7 -5
- package/src/resources/extensions/gsd/tests/prompt-duplication-cuts.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/query-tools-db-open.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +38 -17
- package/src/resources/extensions/gsd/tests/select-resumable-milestone.test.ts +96 -0
- package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +77 -0
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +166 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/system-context-memory.test.ts +112 -0
- package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +7 -9
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +291 -0
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +50 -1
- package/src/resources/extensions/gsd/tests/unstructured-continue-context-injection.test.ts +5 -4
- package/src/resources/extensions/gsd/tests/workflow-dispatch-claim.test.ts +142 -0
- package/src/resources/extensions/gsd/tests/workflow-protocol-excerpt.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/workflow-worker-heartbeat.test.ts +32 -1
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +22 -19
- package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +104 -3
- package/src/resources/extensions/gsd/workflow-protocol.ts +160 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +49 -4
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +0 -97
- /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_ssgManifest.js +0 -0
|
@@ -11,11 +11,15 @@ import {
|
|
|
11
11
|
_hasPendingResolveForTest,
|
|
12
12
|
_setActiveSession,
|
|
13
13
|
_setSessionSwitchInFlight,
|
|
14
|
+
_markSessionSwitchAbortGraceWindow,
|
|
15
|
+
_clearSessionSwitchAbortGraceWindow,
|
|
14
16
|
_consumePendingSwitchCancellation,
|
|
15
17
|
isSessionSwitchInFlight,
|
|
18
|
+
isSessionSwitchAbortGraceActive,
|
|
16
19
|
} from "../auto/resolve.js";
|
|
17
20
|
import { runUnit } from "../auto/run-unit.js";
|
|
18
21
|
import { autoLoop } from "../auto/loop.js";
|
|
22
|
+
import { runDispatch } from "../auto/phases.js";
|
|
19
23
|
import { detectStuck } from "../auto/detect-stuck.js";
|
|
20
24
|
import type { UnitResult, AgentEndEvent } from "../auto/types.js";
|
|
21
25
|
import type { LoopDeps } from "../auto/loop-deps.js";
|
|
@@ -66,7 +70,7 @@ function makeMockSession(opts?: {
|
|
|
66
70
|
verbose: false,
|
|
67
71
|
basePath: process.cwd(),
|
|
68
72
|
cmdCtx: {
|
|
69
|
-
newSession: (options?: { abortSignal?: AbortSignal }) => {
|
|
73
|
+
newSession: (options?: { abortSignal?: AbortSignal; workspaceRoot?: string }) => {
|
|
70
74
|
opts?.onNewSessionStart?.(session);
|
|
71
75
|
if (opts?.newSessionThrows) {
|
|
72
76
|
return Promise.reject(new Error(opts.newSessionThrows));
|
|
@@ -78,7 +82,7 @@ function makeMockSession(opts?: {
|
|
|
78
82
|
setTimeout(() => {
|
|
79
83
|
// Simulate AgentSession.newSession() checking abortSignal after
|
|
80
84
|
// its internal async work (abort()) completes — this is where the
|
|
81
|
-
// real code
|
|
85
|
+
// real code selects a workspace root and rebuilds the tool runtime.
|
|
82
86
|
// If the signal is aborted, the real code discards the session.
|
|
83
87
|
opts?.onSignalCheck?.(options?.abortSignal?.aborted ?? false);
|
|
84
88
|
opts?.onNewSessionSettle?.(session);
|
|
@@ -548,10 +552,9 @@ test("runUnit proceeds when isProviderRequestReady throws (defensive) (#4555)",
|
|
|
548
552
|
assert.equal(pi.calls.length, 0);
|
|
549
553
|
});
|
|
550
554
|
|
|
551
|
-
test("late-resolving newSession() after timeout receives aborted signal so tool runtime is not configured with root
|
|
552
|
-
// When newSession() times out in runUnit(),
|
|
553
|
-
//
|
|
554
|
-
// configure the tool runtime (which would give it root cwd, not worktree cwd).
|
|
555
|
+
test("late-resolving newSession() after timeout receives aborted signal so tool runtime is not configured with stale workspace root (#3731)", async () => {
|
|
556
|
+
// When newSession() times out in runUnit(), a late resolution must not
|
|
557
|
+
// configure the tool runtime against a stale workspace root.
|
|
555
558
|
//
|
|
556
559
|
// The fix: runUnit creates an AbortController, aborts it on timeout, and passes
|
|
557
560
|
// the signal to newSession(). AgentSession.newSession() checks the signal after
|
|
@@ -566,8 +569,8 @@ test("late-resolving newSession() after timeout receives aborted signal so tool
|
|
|
566
569
|
|
|
567
570
|
// newSession mock simulates AgentSession.newSession() behavior:
|
|
568
571
|
// after an internal delay (representing await this.abort()), it checks the
|
|
569
|
-
// abortSignal
|
|
570
|
-
//
|
|
572
|
+
// abortSignal before selecting the workspace root and calling _buildRuntime.
|
|
573
|
+
// If aborted, the real code must discard the session.
|
|
571
574
|
const s = makeMockSession({
|
|
572
575
|
newSessionDelayMs: 200_000, // longer than NEW_SESSION_TIMEOUT_MS (120s)
|
|
573
576
|
onSignalCheck: (aborted) => {
|
|
@@ -600,7 +603,7 @@ test("late-resolving newSession() after timeout receives aborted signal so tool
|
|
|
600
603
|
abortedWhenLateSessionSettled,
|
|
601
604
|
true,
|
|
602
605
|
"runUnit must pass an aborted AbortSignal to newSession() when it resolves after the session-creation timeout (#3731). " +
|
|
603
|
-
"Without this, AgentSession.newSession()
|
|
606
|
+
"Without this, AgentSession.newSession() can rebuild the tool runtime with a stale workspace root.",
|
|
604
607
|
);
|
|
605
608
|
} finally {
|
|
606
609
|
mock.timers.reset();
|
|
@@ -689,7 +692,11 @@ function makeMockDeps(
|
|
|
689
692
|
resolveMilestoneFile: () => null,
|
|
690
693
|
reconcileMergeState: () => "clean",
|
|
691
694
|
preflightCleanRoot: () => ({ stashPushed: false, summary: "" }),
|
|
692
|
-
postflightPopStash: () => {
|
|
695
|
+
postflightPopStash: () => ({
|
|
696
|
+
restored: true,
|
|
697
|
+
needsManualRecovery: false,
|
|
698
|
+
message: "restored",
|
|
699
|
+
}),
|
|
693
700
|
getLedger: () => null,
|
|
694
701
|
getProjectTotals: () => ({ cost: 0 }),
|
|
695
702
|
formatCost: (c: number) => `$${c.toFixed(2)}`,
|
|
@@ -857,6 +864,145 @@ test("autoLoop exits on terminal complete state", async (t) => {
|
|
|
857
864
|
);
|
|
858
865
|
});
|
|
859
866
|
|
|
867
|
+
test("autoLoop stops before success notification when postflight stash restore needs recovery", async () => {
|
|
868
|
+
_resetPendingResolve();
|
|
869
|
+
|
|
870
|
+
const notifications: Array<{ msg: string; level: string }> = [];
|
|
871
|
+
const ctx = makeMockCtx();
|
|
872
|
+
ctx.ui.setStatus = () => {};
|
|
873
|
+
ctx.ui.notify = (msg: string, level: string) => {
|
|
874
|
+
notifications.push({ msg, level });
|
|
875
|
+
};
|
|
876
|
+
const pi = makeMockPi();
|
|
877
|
+
const s = makeLoopSession();
|
|
878
|
+
let stopReason = "";
|
|
879
|
+
|
|
880
|
+
const deps = makeMockDeps({
|
|
881
|
+
deriveState: async () => {
|
|
882
|
+
deps.callLog.push("deriveState");
|
|
883
|
+
return {
|
|
884
|
+
phase: "complete",
|
|
885
|
+
activeMilestone: { id: "M001", title: "Test", status: "complete" },
|
|
886
|
+
activeSlice: null,
|
|
887
|
+
activeTask: null,
|
|
888
|
+
registry: [{ id: "M001", status: "complete" }],
|
|
889
|
+
blockers: [],
|
|
890
|
+
} as any;
|
|
891
|
+
},
|
|
892
|
+
preflightCleanRoot: () => ({
|
|
893
|
+
stashPushed: true,
|
|
894
|
+
stashMarker: "gsd-preflight-stash:M001:test",
|
|
895
|
+
summary: "stashed",
|
|
896
|
+
}),
|
|
897
|
+
postflightPopStash: () => ({
|
|
898
|
+
restored: false,
|
|
899
|
+
needsManualRecovery: true,
|
|
900
|
+
message: "git stash pop stash@{0} failed after merge of milestone M001",
|
|
901
|
+
stashRef: "stash@{0}",
|
|
902
|
+
}),
|
|
903
|
+
sendDesktopNotification: () => {
|
|
904
|
+
deps.callLog.push("sendDesktopNotification");
|
|
905
|
+
},
|
|
906
|
+
logCmuxEvent: () => {
|
|
907
|
+
deps.callLog.push("logCmuxEvent");
|
|
908
|
+
},
|
|
909
|
+
stopAuto: async (_ctx, _pi, reason) => {
|
|
910
|
+
deps.callLog.push("stopAuto");
|
|
911
|
+
stopReason = reason ?? "";
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
await autoLoop(ctx, pi, s, deps);
|
|
916
|
+
|
|
917
|
+
assert.equal(stopReason, "Post-merge stash restore failed for milestone M001");
|
|
918
|
+
assert.ok(
|
|
919
|
+
notifications.some(
|
|
920
|
+
(n) => n.level === "error" && n.msg.includes("Post-merge stash restore failed for milestone M001"),
|
|
921
|
+
),
|
|
922
|
+
"failed postflight restore must be surfaced as an error",
|
|
923
|
+
);
|
|
924
|
+
assert.ok(
|
|
925
|
+
!deps.callLog.includes("sendDesktopNotification"),
|
|
926
|
+
"must not emit milestone success desktop notification after stash restore failure",
|
|
927
|
+
);
|
|
928
|
+
assert.ok(
|
|
929
|
+
!deps.callLog.includes("logCmuxEvent"),
|
|
930
|
+
"must not emit milestone success cmux event after stash restore failure",
|
|
931
|
+
);
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
test("autoLoop marks transition merge complete before postflight recovery stop", async () => {
|
|
935
|
+
_resetPendingResolve();
|
|
936
|
+
|
|
937
|
+
const ctx = makeMockCtx();
|
|
938
|
+
ctx.ui.setStatus = () => {};
|
|
939
|
+
ctx.ui.notify = () => {};
|
|
940
|
+
const pi = makeMockPi();
|
|
941
|
+
const s = makeLoopSession();
|
|
942
|
+
let mergeCalls = 0;
|
|
943
|
+
let stopReason = "";
|
|
944
|
+
|
|
945
|
+
const deps = makeMockDeps({
|
|
946
|
+
deriveState: async () => {
|
|
947
|
+
deps.callLog.push("deriveState");
|
|
948
|
+
return {
|
|
949
|
+
phase: "executing",
|
|
950
|
+
activeMilestone: { id: "M002", title: "Next", status: "active" },
|
|
951
|
+
activeSlice: null,
|
|
952
|
+
activeTask: null,
|
|
953
|
+
registry: [
|
|
954
|
+
{ id: "M001", title: "Done", status: "complete" },
|
|
955
|
+
{ id: "M002", title: "Next", status: "active" },
|
|
956
|
+
],
|
|
957
|
+
blockers: [],
|
|
958
|
+
} as any;
|
|
959
|
+
},
|
|
960
|
+
preflightCleanRoot: () => ({
|
|
961
|
+
stashPushed: true,
|
|
962
|
+
stashMarker: "gsd-preflight-stash:M001:test",
|
|
963
|
+
summary: "stashed",
|
|
964
|
+
}),
|
|
965
|
+
postflightPopStash: () => ({
|
|
966
|
+
restored: false,
|
|
967
|
+
needsManualRecovery: true,
|
|
968
|
+
message: "git stash pop stash@{0} failed after merge of milestone M001",
|
|
969
|
+
stashRef: "stash@{0}",
|
|
970
|
+
}),
|
|
971
|
+
resolver: {
|
|
972
|
+
get workPath() {
|
|
973
|
+
return "/tmp/project";
|
|
974
|
+
},
|
|
975
|
+
get projectRoot() {
|
|
976
|
+
return "/tmp/project";
|
|
977
|
+
},
|
|
978
|
+
get lockPath() {
|
|
979
|
+
return "/tmp/project";
|
|
980
|
+
},
|
|
981
|
+
enterMilestone: () => {
|
|
982
|
+
assert.fail("must not enter the next milestone after postflight recovery fails");
|
|
983
|
+
},
|
|
984
|
+
exitMilestone: () => {},
|
|
985
|
+
mergeAndExit: () => {
|
|
986
|
+
mergeCalls += 1;
|
|
987
|
+
},
|
|
988
|
+
mergeAndEnterNext: () => {},
|
|
989
|
+
} as any,
|
|
990
|
+
stopAuto: async (_ctx, _pi, reason) => {
|
|
991
|
+
deps.callLog.push("stopAuto");
|
|
992
|
+
stopReason = reason ?? "";
|
|
993
|
+
if (!s.milestoneMergedInPhases) {
|
|
994
|
+
deps.resolver.mergeAndExit("M001", ctx.ui);
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
await autoLoop(ctx, pi, s, deps);
|
|
1000
|
+
|
|
1001
|
+
assert.equal(stopReason, "Post-merge stash restore failed for milestone M001");
|
|
1002
|
+
assert.equal(s.milestoneMergedInPhases, true);
|
|
1003
|
+
assert.equal(mergeCalls, 1, "postflight recovery stop must not re-run an already completed transition merge");
|
|
1004
|
+
});
|
|
1005
|
+
|
|
860
1006
|
test("autoLoop pauses when provider readiness cancels before dispatch", async () => {
|
|
861
1007
|
_resetPendingResolve();
|
|
862
1008
|
|
|
@@ -2160,6 +2306,18 @@ test("resolveAgentEndCancelled queues cancellation that arrives during session s
|
|
|
2160
2306
|
_resetPendingResolve();
|
|
2161
2307
|
});
|
|
2162
2308
|
|
|
2309
|
+
test("session-switch abort grace window is short-lived and resettable", () => {
|
|
2310
|
+
_resetPendingResolve();
|
|
2311
|
+
|
|
2312
|
+
_markSessionSwitchAbortGraceWindow(1_000);
|
|
2313
|
+
|
|
2314
|
+
assert.equal(isSessionSwitchAbortGraceActive(Date.now()), true);
|
|
2315
|
+
assert.equal(isSessionSwitchAbortGraceActive(Date.now() + 10_000), false);
|
|
2316
|
+
|
|
2317
|
+
_clearSessionSwitchAbortGraceWindow();
|
|
2318
|
+
assert.equal(isSessionSwitchAbortGraceActive(), false);
|
|
2319
|
+
});
|
|
2320
|
+
|
|
2163
2321
|
// ─── #1571: artifact verification retry ──────────────────────────────────────
|
|
2164
2322
|
|
|
2165
2323
|
test("autoLoop re-iterates when postUnitPreVerification returns retry (#1571)", async () => {
|
|
@@ -2580,6 +2738,199 @@ test("autoLoop stops when worktree has no .git for execute-task (#1833)", async
|
|
|
2580
2738
|
);
|
|
2581
2739
|
});
|
|
2582
2740
|
|
|
2741
|
+
test("dispatch health check wins before stuck detection for execute-task without .git", async () => {
|
|
2742
|
+
_resetPendingResolve();
|
|
2743
|
+
|
|
2744
|
+
const ctx = makeMockCtx();
|
|
2745
|
+
const pi = makeMockPi();
|
|
2746
|
+
const notifications: string[] = [];
|
|
2747
|
+
ctx.ui.notify = (msg: string) => { notifications.push(msg); };
|
|
2748
|
+
|
|
2749
|
+
const s = makeLoopSession({ basePath: "/tmp/broken-worktree" });
|
|
2750
|
+
const deps = makeMockDeps({
|
|
2751
|
+
existsSync: (p: string) => !p.endsWith(".git"),
|
|
2752
|
+
});
|
|
2753
|
+
const result = await runDispatch(
|
|
2754
|
+
{
|
|
2755
|
+
ctx,
|
|
2756
|
+
pi,
|
|
2757
|
+
s,
|
|
2758
|
+
deps,
|
|
2759
|
+
prefs: undefined,
|
|
2760
|
+
iteration: 1,
|
|
2761
|
+
flowId: "test-flow",
|
|
2762
|
+
nextSeq: () => 1,
|
|
2763
|
+
},
|
|
2764
|
+
{
|
|
2765
|
+
state: {
|
|
2766
|
+
phase: "executing",
|
|
2767
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
2768
|
+
activeSlice: { id: "S01", title: "Slice 1" },
|
|
2769
|
+
activeTask: { id: "T01" },
|
|
2770
|
+
registry: [{ id: "M001", status: "active" }],
|
|
2771
|
+
blockers: [],
|
|
2772
|
+
} as any,
|
|
2773
|
+
mid: "M001",
|
|
2774
|
+
midTitle: "Test",
|
|
2775
|
+
},
|
|
2776
|
+
{
|
|
2777
|
+
recentUnits: [
|
|
2778
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
2779
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
2780
|
+
],
|
|
2781
|
+
stuckRecoveryAttempts: 1,
|
|
2782
|
+
consecutiveFinalizeTimeouts: 0,
|
|
2783
|
+
},
|
|
2784
|
+
);
|
|
2785
|
+
|
|
2786
|
+
assert.equal(result.action, "break");
|
|
2787
|
+
assert.equal(result.reason, "worktree-invalid");
|
|
2788
|
+
assert.ok(deps.callLog.includes("stopAuto"), "should stop through worktree health check");
|
|
2789
|
+
assert.ok(
|
|
2790
|
+
notifications.some((n) => n.includes("Worktree health check failed") && n.includes("no .git")),
|
|
2791
|
+
"should notify about missing .git",
|
|
2792
|
+
);
|
|
2793
|
+
assert.ok(
|
|
2794
|
+
!notifications.some((n) => n.includes("Stuck on execute-task")),
|
|
2795
|
+
"stuck-loop message must not mask the worktree health failure",
|
|
2796
|
+
);
|
|
2797
|
+
});
|
|
2798
|
+
|
|
2799
|
+
test("pre-dispatch skip resolves before dispatch health and stuck accounting", async () => {
|
|
2800
|
+
_resetPendingResolve();
|
|
2801
|
+
|
|
2802
|
+
const ctx = makeMockCtx();
|
|
2803
|
+
const pi = makeMockPi();
|
|
2804
|
+
const notifications: string[] = [];
|
|
2805
|
+
ctx.ui.notify = (msg: string) => { notifications.push(msg); };
|
|
2806
|
+
|
|
2807
|
+
const s = makeLoopSession({ basePath: "/tmp/broken-worktree" });
|
|
2808
|
+
const deps = makeMockDeps({
|
|
2809
|
+
existsSync: (p: string) => !p.endsWith(".git"),
|
|
2810
|
+
runPreDispatchHooks: () => ({ firedHooks: ["skip-execute"], action: "skip" }),
|
|
2811
|
+
});
|
|
2812
|
+
const loopState = {
|
|
2813
|
+
recentUnits: [
|
|
2814
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
2815
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
2816
|
+
],
|
|
2817
|
+
stuckRecoveryAttempts: 1,
|
|
2818
|
+
consecutiveFinalizeTimeouts: 0,
|
|
2819
|
+
};
|
|
2820
|
+
|
|
2821
|
+
const result = await runDispatch(
|
|
2822
|
+
{
|
|
2823
|
+
ctx,
|
|
2824
|
+
pi,
|
|
2825
|
+
s,
|
|
2826
|
+
deps,
|
|
2827
|
+
prefs: undefined,
|
|
2828
|
+
iteration: 1,
|
|
2829
|
+
flowId: "test-flow",
|
|
2830
|
+
nextSeq: () => 1,
|
|
2831
|
+
},
|
|
2832
|
+
{
|
|
2833
|
+
state: {
|
|
2834
|
+
phase: "executing",
|
|
2835
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
2836
|
+
activeSlice: { id: "S01", title: "Slice 1" },
|
|
2837
|
+
activeTask: { id: "T01" },
|
|
2838
|
+
registry: [{ id: "M001", status: "active" }],
|
|
2839
|
+
blockers: [],
|
|
2840
|
+
} as any,
|
|
2841
|
+
mid: "M001",
|
|
2842
|
+
midTitle: "Test",
|
|
2843
|
+
},
|
|
2844
|
+
loopState,
|
|
2845
|
+
);
|
|
2846
|
+
|
|
2847
|
+
assert.equal(result.action, "continue");
|
|
2848
|
+
assert.ok(!deps.callLog.includes("stopAuto"), "skip hook should not stop on worktree health");
|
|
2849
|
+
assert.equal(loopState.recentUnits.length, 2, "skip hook should not update stuck accounting");
|
|
2850
|
+
assert.ok(
|
|
2851
|
+
notifications.some((n) => n.includes("Skipping execute-task M001/S01/T01")),
|
|
2852
|
+
"should notify about the skip hook",
|
|
2853
|
+
);
|
|
2854
|
+
assert.ok(
|
|
2855
|
+
!notifications.some((n) => n.includes("Worktree health check failed") || n.includes("Stuck on execute-task")),
|
|
2856
|
+
"health and stuck notifications must not run before skip hook resolution",
|
|
2857
|
+
);
|
|
2858
|
+
});
|
|
2859
|
+
|
|
2860
|
+
test("pre-dispatch replace resolves final unit before dispatch health and stuck accounting", async () => {
|
|
2861
|
+
_resetPendingResolve();
|
|
2862
|
+
|
|
2863
|
+
const ctx = makeMockCtx();
|
|
2864
|
+
const pi = makeMockPi();
|
|
2865
|
+
const notifications: string[] = [];
|
|
2866
|
+
ctx.ui.notify = (msg: string) => { notifications.push(msg); };
|
|
2867
|
+
|
|
2868
|
+
const s = makeLoopSession({ basePath: "/tmp/broken-worktree" });
|
|
2869
|
+
const deps = makeMockDeps({
|
|
2870
|
+
existsSync: (p: string) => !p.endsWith(".git"),
|
|
2871
|
+
runPreDispatchHooks: () => ({
|
|
2872
|
+
firedHooks: ["review"],
|
|
2873
|
+
action: "replace",
|
|
2874
|
+
unitType: "hook/review",
|
|
2875
|
+
prompt: "review before executing",
|
|
2876
|
+
model: "review-model",
|
|
2877
|
+
}),
|
|
2878
|
+
});
|
|
2879
|
+
const loopState = {
|
|
2880
|
+
recentUnits: [
|
|
2881
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
2882
|
+
{ key: "execute-task/M001/S01/T01" },
|
|
2883
|
+
],
|
|
2884
|
+
stuckRecoveryAttempts: 1,
|
|
2885
|
+
consecutiveFinalizeTimeouts: 0,
|
|
2886
|
+
};
|
|
2887
|
+
|
|
2888
|
+
const result = await runDispatch(
|
|
2889
|
+
{
|
|
2890
|
+
ctx,
|
|
2891
|
+
pi,
|
|
2892
|
+
s,
|
|
2893
|
+
deps,
|
|
2894
|
+
prefs: undefined,
|
|
2895
|
+
iteration: 1,
|
|
2896
|
+
flowId: "test-flow",
|
|
2897
|
+
nextSeq: () => 1,
|
|
2898
|
+
},
|
|
2899
|
+
{
|
|
2900
|
+
state: {
|
|
2901
|
+
phase: "executing",
|
|
2902
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
2903
|
+
activeSlice: { id: "S01", title: "Slice 1" },
|
|
2904
|
+
activeTask: { id: "T01" },
|
|
2905
|
+
registry: [{ id: "M001", status: "active" }],
|
|
2906
|
+
blockers: [],
|
|
2907
|
+
} as any,
|
|
2908
|
+
mid: "M001",
|
|
2909
|
+
midTitle: "Test",
|
|
2910
|
+
},
|
|
2911
|
+
loopState,
|
|
2912
|
+
);
|
|
2913
|
+
|
|
2914
|
+
assert.equal(result.action, "next");
|
|
2915
|
+
assert.equal(result.data?.unitType, "hook/review");
|
|
2916
|
+
assert.equal(result.data?.finalPrompt, "review before executing");
|
|
2917
|
+
assert.equal(result.data?.hookModelOverride, "review-model");
|
|
2918
|
+
assert.ok(!deps.callLog.includes("stopAuto"), "replace hook should not stop on execute-task health");
|
|
2919
|
+
assert.deepEqual(
|
|
2920
|
+
loopState.recentUnits.map((u) => u.key),
|
|
2921
|
+
[
|
|
2922
|
+
"execute-task/M001/S01/T01",
|
|
2923
|
+
"execute-task/M001/S01/T01",
|
|
2924
|
+
"hook/review/M001/S01/T01",
|
|
2925
|
+
],
|
|
2926
|
+
"stuck accounting should record the final replaced unit",
|
|
2927
|
+
);
|
|
2928
|
+
assert.ok(
|
|
2929
|
+
!notifications.some((n) => n.includes("Worktree health check failed") || n.includes("Stuck on execute-task")),
|
|
2930
|
+
"health and stuck notifications must use the final replaced unit",
|
|
2931
|
+
);
|
|
2932
|
+
});
|
|
2933
|
+
|
|
2583
2934
|
test("autoLoop warns but proceeds for greenfield project (no project files) (#1833)", async () => {
|
|
2584
2935
|
_resetPendingResolve();
|
|
2585
2936
|
|
|
@@ -3,8 +3,14 @@
|
|
|
3
3
|
|
|
4
4
|
import { describe, test } from "node:test";
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
|
-
import { readFileSync } from "node:fs";
|
|
6
|
+
import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
|
|
10
|
+
import { autoSession } from "../auto-runtime-state.ts";
|
|
11
|
+
import { dispatchHookUnit } from "../auto.ts";
|
|
12
|
+
import { registerHooks } from "../bootstrap/register-hooks.ts";
|
|
13
|
+
import { clearDiscussionFlowState, getPendingGate } from "../bootstrap/write-gate.ts";
|
|
8
14
|
|
|
9
15
|
const autoTimersPath = join(import.meta.dirname, "..", "auto-timers.ts");
|
|
10
16
|
const autoTimersSrc = readFileSync(autoTimersPath, "utf-8");
|
|
@@ -18,6 +24,37 @@ const runUnitSrc = readFileSync(runUnitPath, "utf-8");
|
|
|
18
24
|
const registerHooksPath = join(import.meta.dirname, "..", "bootstrap", "register-hooks.ts");
|
|
19
25
|
const registerHooksSrc = readFileSync(registerHooksPath, "utf-8");
|
|
20
26
|
|
|
27
|
+
function makeHookHarness() {
|
|
28
|
+
const handlers = new Map<string, Array<(event: any, ctx: any) => Promise<any>>>();
|
|
29
|
+
const pi = {
|
|
30
|
+
on(name: string, handler: (event: any, ctx: any) => Promise<any>) {
|
|
31
|
+
const current = handlers.get(name) ?? [];
|
|
32
|
+
current.push(handler);
|
|
33
|
+
handlers.set(name, current);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const ctx = {
|
|
37
|
+
ui: {
|
|
38
|
+
notify: () => {},
|
|
39
|
+
setStatus: () => {},
|
|
40
|
+
setWidget: () => {},
|
|
41
|
+
},
|
|
42
|
+
modelRegistry: {
|
|
43
|
+
setDisabledModelProviders: () => {},
|
|
44
|
+
},
|
|
45
|
+
setCompactionThresholdOverride: () => {},
|
|
46
|
+
};
|
|
47
|
+
async function emit(name: string, event: any): Promise<any> {
|
|
48
|
+
for (const handler of handlers.get(name) ?? []) {
|
|
49
|
+
const result = await handler(event, ctx);
|
|
50
|
+
if (result?.block) return result;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
registerHooks(pi as any, []);
|
|
55
|
+
return { emit };
|
|
56
|
+
}
|
|
57
|
+
|
|
21
58
|
describe("#3512: gsd-auto-wrapup must not interrupt in-flight tool calls", () => {
|
|
22
59
|
test("soft timeout wrapup gates triggerTurn on getInFlightToolCount() === 0", () => {
|
|
23
60
|
// The soft timeout sendMessage must NOT use a hardcoded `triggerTurn: true`.
|
|
@@ -73,10 +110,65 @@ describe("#3512: gsd-auto-wrapup must not interrupt in-flight tool calls", () =>
|
|
|
73
110
|
});
|
|
74
111
|
});
|
|
75
112
|
|
|
113
|
+
describe("hook dispatch session workspace root", () => {
|
|
114
|
+
test("dispatchHookUnit passes basePath explicitly to newSession", async (t) => {
|
|
115
|
+
const originalCwd = process.cwd();
|
|
116
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-hook-cwd-"));
|
|
117
|
+
mkdirSync(join(basePath, ".gsd"), { recursive: true });
|
|
118
|
+
autoSession.reset();
|
|
119
|
+
t.after(() => {
|
|
120
|
+
try {
|
|
121
|
+
process.chdir(originalCwd);
|
|
122
|
+
} catch {
|
|
123
|
+
// best effort cleanup after cwd-sensitive dispatch tests
|
|
124
|
+
}
|
|
125
|
+
autoSession.reset();
|
|
126
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
let newSessionOptions: unknown;
|
|
130
|
+
const ctx = {
|
|
131
|
+
ui: {
|
|
132
|
+
notify: () => {},
|
|
133
|
+
setStatus: () => {},
|
|
134
|
+
setWidget: () => {},
|
|
135
|
+
},
|
|
136
|
+
modelRegistry: {
|
|
137
|
+
getAvailable: () => [],
|
|
138
|
+
},
|
|
139
|
+
sessionManager: {
|
|
140
|
+
getSessionFile: () => join(basePath, "session.jsonl"),
|
|
141
|
+
},
|
|
142
|
+
newSession: async (options?: unknown) => {
|
|
143
|
+
newSessionOptions = options;
|
|
144
|
+
return { cancelled: false };
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
const pi = {
|
|
148
|
+
sendMessage: () => {},
|
|
149
|
+
setModel: async () => true,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const dispatched = await dispatchHookUnit(
|
|
153
|
+
ctx as any,
|
|
154
|
+
pi as any,
|
|
155
|
+
"review",
|
|
156
|
+
"execute-task",
|
|
157
|
+
"M001/S01/T01",
|
|
158
|
+
"review the completed unit",
|
|
159
|
+
undefined,
|
|
160
|
+
basePath,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
assert.equal(dispatched, true);
|
|
164
|
+
assert.deepEqual(newSessionOptions, { workspaceRoot: basePath });
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
76
168
|
describe("#4276: pending/skipped tools stay visible to auto-mode hooks", () => {
|
|
77
169
|
test("tool_call handler marks GSD tools in-flight before execution_start", () => {
|
|
78
170
|
const startMarker = 'pi.on("tool_call", async (event, ctx) => {';
|
|
79
|
-
const endMarker = 'pi.on("tool_result", async (event) => {';
|
|
171
|
+
const endMarker = 'pi.on("tool_result", async (event, ctx) => {';
|
|
80
172
|
const toolCallSection = registerHooksSrc.slice(
|
|
81
173
|
registerHooksSrc.indexOf(startMarker),
|
|
82
174
|
registerHooksSrc.indexOf(endMarker),
|
|
@@ -90,7 +182,7 @@ describe("#4276: pending/skipped tools stay visible to auto-mode hooks", () => {
|
|
|
90
182
|
});
|
|
91
183
|
|
|
92
184
|
test("tool_result handler clears pending tools and records queued-skip errors", () => {
|
|
93
|
-
const startMarker = 'pi.on("tool_result", async (event) => {';
|
|
185
|
+
const startMarker = 'pi.on("tool_result", async (event, ctx) => {';
|
|
94
186
|
const endMarker = 'pi.on("tool_execution_start", async (event) => {';
|
|
95
187
|
const toolResultSection = registerHooksSrc.slice(
|
|
96
188
|
registerHooksSrc.indexOf(startMarker),
|
|
@@ -193,7 +285,7 @@ describe("#4365: tool_execution_start hook must pass toolName to markToolStart",
|
|
|
193
285
|
});
|
|
194
286
|
|
|
195
287
|
describe("deep setup approval questions pause immediately", () => {
|
|
196
|
-
test("register-hooks
|
|
288
|
+
test("register-hooks defers the pending gate during message_update without aborting the stream", () => {
|
|
197
289
|
const startMarker = 'pi.on("message_update"';
|
|
198
290
|
const endMarker = 'pi.on("session_shutdown"';
|
|
199
291
|
const messageUpdateSection = registerHooksSrc.slice(
|
|
@@ -210,8 +302,8 @@ describe("deep setup approval questions pause immediately", () => {
|
|
|
210
302
|
"message_update must detect approval/question boundaries",
|
|
211
303
|
);
|
|
212
304
|
assert.ok(
|
|
213
|
-
messageUpdateSection.includes("approvalGateIdForUnit") && messageUpdateSection.includes("
|
|
214
|
-
"plain-text approval questions must
|
|
305
|
+
messageUpdateSection.includes("approvalGateIdForUnit") && messageUpdateSection.includes("deferApprovalGate"),
|
|
306
|
+
"plain-text approval questions must defer the durable write gate until same-turn draft persistence can finish",
|
|
215
307
|
);
|
|
216
308
|
assert.ok(
|
|
217
309
|
messageUpdateSection.includes("getDiscussionMilestoneIdFor") && messageUpdateSection.includes('"discuss-milestone"'),
|
|
@@ -222,4 +314,74 @@ describe("deep setup approval questions pause immediately", () => {
|
|
|
222
314
|
"message_update must NOT abort the stream — aborting eats the model's question text on external CLI providers; the pending gate set above blocks subsequent tool calls instead",
|
|
223
315
|
);
|
|
224
316
|
});
|
|
317
|
+
|
|
318
|
+
test("plain-text approval boundary defers durable gate until same-turn CONTEXT-DRAFT can save", async () => {
|
|
319
|
+
const base = realpathSync(mkdtempSync(join(tmpdir(), "gsd-deferred-approval-")));
|
|
320
|
+
const previousCwd = process.cwd();
|
|
321
|
+
try {
|
|
322
|
+
mkdirSync(join(base, ".gsd", "milestones", "M003"), { recursive: true });
|
|
323
|
+
process.chdir(base);
|
|
324
|
+
clearDiscussionFlowState(base);
|
|
325
|
+
autoSession.reset();
|
|
326
|
+
autoSession.basePath = base;
|
|
327
|
+
autoSession.currentUnit = {
|
|
328
|
+
type: "discuss-milestone",
|
|
329
|
+
id: "M003",
|
|
330
|
+
startedAt: Date.now(),
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const { emit } = makeHookHarness();
|
|
334
|
+
await emit("message_update", {
|
|
335
|
+
message: {
|
|
336
|
+
role: "assistant",
|
|
337
|
+
content: [{ type: "text", text: "Did I capture that correctly? If not, tell me what I missed." }],
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
assert.equal(
|
|
342
|
+
getPendingGate(base),
|
|
343
|
+
null,
|
|
344
|
+
"approval text should not install the durable pending gate until the assistant turn ends",
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const draftResult = await emit("tool_call", {
|
|
348
|
+
toolCallId: "draft-save",
|
|
349
|
+
toolName: "gsd_summary_save",
|
|
350
|
+
input: {
|
|
351
|
+
milestone_id: "M003",
|
|
352
|
+
artifact_type: "CONTEXT-DRAFT",
|
|
353
|
+
content: "# M003 Draft\n",
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
assert.equal(
|
|
357
|
+
draftResult?.block,
|
|
358
|
+
undefined,
|
|
359
|
+
"same-turn CONTEXT-DRAFT persistence should remain allowed after the approval text streams",
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const finalContextResult = await emit("tool_call", {
|
|
363
|
+
toolCallId: "final-context",
|
|
364
|
+
toolName: "gsd_summary_save",
|
|
365
|
+
input: {
|
|
366
|
+
milestone_id: "M003",
|
|
367
|
+
artifact_type: "CONTEXT",
|
|
368
|
+
content: "# M003 Context\n",
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
assert.equal(finalContextResult?.block, true, "final CONTEXT must still wait for approval");
|
|
372
|
+
assert.match(finalContextResult.reason, /Approval question "depth_verification_M003_confirm"/);
|
|
373
|
+
|
|
374
|
+
await emit("agent_end", { messages: [] });
|
|
375
|
+
assert.equal(
|
|
376
|
+
getPendingGate(base),
|
|
377
|
+
"depth_verification_M003_confirm",
|
|
378
|
+
"agent_end should activate the durable pending gate for the next turn",
|
|
379
|
+
);
|
|
380
|
+
} finally {
|
|
381
|
+
process.chdir(previousCwd);
|
|
382
|
+
autoSession.reset();
|
|
383
|
+
clearDiscussionFlowState(base);
|
|
384
|
+
rmSync(base, { recursive: true, force: true });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
225
387
|
});
|