gsd-pi 2.81.0 → 2.82.0
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 +36 -24
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +111 -8
- package/dist/resources/extensions/gsd/auto/phases.js +190 -97
- package/dist/resources/extensions/gsd/auto/run-unit.js +66 -3
- package/dist/resources/extensions/gsd/auto/session.js +9 -0
- package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +43 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +182 -178
- package/dist/resources/extensions/gsd/auto-dispatch.js +14 -11
- package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +6 -181
- package/dist/resources/extensions/gsd/auto-runtime-state.js +5 -0
- package/dist/resources/extensions/gsd/auto-start.js +20 -23
- package/dist/resources/extensions/gsd/auto-unit-closeout.js +33 -5
- package/dist/resources/extensions/gsd/auto-verification.js +12 -6
- package/dist/resources/extensions/gsd/auto-worktree.js +8 -0
- package/dist/resources/extensions/gsd/auto.js +265 -76
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +13 -6
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -2
- package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +4 -8
- package/dist/resources/extensions/gsd/commands/handlers/notifications-handler.js +4 -10
- package/dist/resources/extensions/gsd/commands/handlers/parallel.js +9 -0
- package/dist/resources/extensions/gsd/git-service.js +2 -1
- package/dist/resources/extensions/gsd/gsd-db.js +7 -23
- package/dist/resources/extensions/gsd/health-widget-core.js +1 -1
- package/dist/resources/extensions/gsd/health-widget.js +4 -10
- package/dist/resources/extensions/gsd/markdown-renderer.js +0 -95
- package/dist/resources/extensions/gsd/native-git-bridge.js +14 -14
- package/dist/resources/extensions/gsd/notification-overlay.js +35 -40
- package/dist/resources/extensions/gsd/parallel-merge.js +53 -30
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +25 -33
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +14 -12
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +20 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +20 -2
- package/dist/resources/extensions/gsd/recovery-classification.js +15 -1
- package/dist/resources/extensions/gsd/session-lock.js +40 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +131 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/merge-state.js +247 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +50 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +87 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +50 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +124 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-worker.js +32 -0
- package/dist/resources/extensions/gsd/state-reconciliation/errors.js +41 -0
- package/dist/resources/extensions/gsd/state-reconciliation/index.js +99 -0
- package/dist/resources/extensions/gsd/state-reconciliation/registry.js +24 -0
- package/dist/resources/extensions/gsd/state-reconciliation/spawn-gate.js +43 -0
- package/dist/resources/extensions/gsd/state-reconciliation/types.js +3 -0
- package/dist/resources/extensions/gsd/state-reconciliation.js +5 -26
- package/dist/resources/extensions/gsd/tui/render-kit.js +74 -0
- package/dist/resources/extensions/gsd/watch/header-renderer.js +92 -69
- package/dist/resources/extensions/gsd/watch/splash-palette.js +10 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +722 -316
- package/dist/resources/extensions/gsd/worktree-telemetry.js +3 -1
- 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 +9 -9
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- 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 +9 -9
- 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/welcome-screen.d.ts +0 -7
- package/dist/welcome-screen.js +60 -69
- package/package.json +1 -1
- package/packages/daemon/package.json +2 -2
- package/packages/mcp-server/package.json +2 -2
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.js +47 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +76 -9
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.js +40 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.d.ts +0 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js +30 -29
- package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.test.js +10 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +13 -13
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +1 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +58 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts +2 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +12 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +14 -41
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +0 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +86 -82
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.d.ts +35 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.js +152 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.d.ts +16 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.js +73 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +12 -8
- package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.js +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +105 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +27 -26
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js +9 -6
- package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/assistant-message-design.test.ts +56 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +113 -9
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/user-message-design.test.ts +48 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/adaptive-layout.test.ts +10 -3
- package/packages/pi-coding-agent/src/modes/interactive/components/adaptive-layout.ts +43 -42
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +14 -14
- package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +64 -3
- package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +13 -7
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +15 -42
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +84 -104
- package/packages/pi-coding-agent/src/modes/interactive/components/transcript-design.ts +196 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/tui-style-kit.ts +94 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +14 -9
- package/packages/pi-coding-agent/src/modes/interactive/theme/theme-highlight.test.ts +23 -0
- package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +106 -1
- package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +27 -26
- package/packages/pi-coding-agent/src/modes/interactive/tui-mode.test.ts +9 -6
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/__tests__/overlay-layout.test.js +14 -1
- package/packages/pi-tui/dist/__tests__/overlay-layout.test.js.map +1 -1
- package/packages/pi-tui/dist/overlay-layout.d.ts.map +1 -1
- package/packages/pi-tui/dist/overlay-layout.js +9 -6
- package/packages/pi-tui/dist/overlay-layout.js.map +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/pi-tui/src/__tests__/overlay-layout.test.ts +20 -1
- package/packages/pi-tui/src/overlay-layout.ts +10 -7
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/packages/rpc-client/package.json +1 -1
- package/pkg/dist/modes/interactive/theme/theme-highlight.test.d.ts +2 -0
- package/pkg/dist/modes/interactive/theme/theme-highlight.test.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme-highlight.test.js +17 -0
- package/pkg/dist/modes/interactive/theme/theme-highlight.test.js.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/pkg/dist/modes/interactive/theme/theme.js +105 -1
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
- package/pkg/dist/modes/interactive/theme/themes.d.ts.map +1 -1
- package/pkg/dist/modes/interactive/theme/themes.js +27 -26
- package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/loop-deps.ts +9 -5
- package/src/resources/extensions/gsd/auto/loop.ts +113 -9
- package/src/resources/extensions/gsd/auto/phases.ts +144 -19
- package/src/resources/extensions/gsd/auto/run-unit.ts +69 -4
- package/src/resources/extensions/gsd/auto/session.ts +10 -0
- package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +82 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +230 -183
- package/src/resources/extensions/gsd/auto-dispatch.ts +15 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +7 -1
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -209
- package/src/resources/extensions/gsd/auto-runtime-state.ts +5 -0
- package/src/resources/extensions/gsd/auto-start.ts +22 -22
- package/src/resources/extensions/gsd/auto-unit-closeout.ts +51 -0
- package/src/resources/extensions/gsd/auto-verification.ts +12 -6
- package/src/resources/extensions/gsd/auto-worktree.ts +8 -0
- package/src/resources/extensions/gsd/auto.ts +295 -75
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -6
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +6 -2
- package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +5 -8
- package/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +4 -10
- package/src/resources/extensions/gsd/commands/handlers/parallel.ts +12 -0
- package/src/resources/extensions/gsd/git-service.ts +2 -0
- package/src/resources/extensions/gsd/gsd-db.ts +7 -23
- package/src/resources/extensions/gsd/health-widget-core.ts +1 -1
- package/src/resources/extensions/gsd/health-widget.ts +6 -10
- package/src/resources/extensions/gsd/journal.ts +2 -0
- package/src/resources/extensions/gsd/markdown-renderer.ts +4 -95
- package/src/resources/extensions/gsd/native-git-bridge.ts +14 -13
- package/src/resources/extensions/gsd/notification-overlay.ts +50 -46
- package/src/resources/extensions/gsd/parallel-merge.ts +61 -34
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +33 -35
- package/src/resources/extensions/gsd/prompts/complete-slice.md +14 -12
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +20 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +20 -2
- package/src/resources/extensions/gsd/recovery-classification.ts +18 -1
- package/src/resources/extensions/gsd/session-lock.ts +41 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +172 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/merge-state.ts +337 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +69 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +109 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +68 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +185 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-worker.ts +46 -0
- package/src/resources/extensions/gsd/state-reconciliation/errors.ts +67 -0
- package/src/resources/extensions/gsd/state-reconciliation/index.ts +142 -0
- package/src/resources/extensions/gsd/state-reconciliation/registry.ts +27 -0
- package/src/resources/extensions/gsd/state-reconciliation/spawn-gate.ts +60 -0
- package/src/resources/extensions/gsd/state-reconciliation/types.ts +83 -0
- package/src/resources/extensions/gsd/state-reconciliation.ts +21 -53
- package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +654 -176
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +291 -4
- package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +16 -1
- package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/auto-unit-closeout.test.ts +68 -0
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +28 -1
- package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +20 -2
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/header-renderer.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/headless-milestone-parity.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +14 -4
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/integration/integration-proof.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +116 -24
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +46 -11
- package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +78 -41
- package/src/resources/extensions/gsd/tests/notifications-handler.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +12 -217
- package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +38 -6
- package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +24 -1
- package/src/resources/extensions/gsd/tests/resume-dispatch-worktree.test.ts +7 -3
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +65 -58
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +952 -0
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/tui-header-lifecycle.test.ts +121 -1
- package/src/resources/extensions/gsd/tests/tui-render-kit.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +83 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +158 -58
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +572 -118
- package/src/resources/extensions/gsd/tests/worktree-telemetry.test.ts +59 -2
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +18 -0
- package/src/resources/extensions/gsd/tui/render-kit.ts +109 -0
- package/src/resources/extensions/gsd/watch/header-renderer.ts +121 -79
- package/src/resources/extensions/gsd/watch/splash-palette.ts +11 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +1151 -524
- package/src/resources/extensions/gsd/worktree-telemetry.ts +7 -2
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +0 -1544
- /package/dist/web/standalone/.next/static/{drLMkgfHQ8lzS229_HWYR → S44UQTFCUdA44dkjfYt6S}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{drLMkgfHQ8lzS229_HWYR → S44UQTFCUdA44dkjfYt6S}/_ssgManifest.js +0 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Project/App: GSD-2
|
|
2
|
+
// File Purpose: Auto-loop execution, dispatch, recovery, and cancellation regression tests.
|
|
3
|
+
|
|
1
4
|
import test, { mock } from "node:test";
|
|
2
5
|
import assert from "node:assert/strict";
|
|
3
6
|
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
@@ -17,9 +20,10 @@ import {
|
|
|
17
20
|
isSessionSwitchInFlight,
|
|
18
21
|
isSessionSwitchAbortGraceActive,
|
|
19
22
|
} from "../auto/resolve.js";
|
|
20
|
-
import { runUnit } from "../auto/run-unit.js";
|
|
23
|
+
import { runUnit, shouldDeferUnitFailsafeTimeout } from "../auto/run-unit.js";
|
|
24
|
+
import { writeUnitRuntimeRecord, readUnitRuntimeRecord } from "../unit-runtime.js";
|
|
21
25
|
import { autoLoop } from "../auto/loop.js";
|
|
22
|
-
import { runDispatch } from "../auto/phases.js";
|
|
26
|
+
import { runDispatch, runUnitPhase } from "../auto/phases.js";
|
|
23
27
|
import { detectStuck } from "../auto/detect-stuck.js";
|
|
24
28
|
import type { UnitResult, AgentEndEvent } from "../auto/types.js";
|
|
25
29
|
import type { LoopDeps } from "../auto/loop-deps.js";
|
|
@@ -162,6 +166,109 @@ test("resolveAgentEnd resolves a pending runUnit promise", async () => {
|
|
|
162
166
|
assert.deepEqual(result.event, event);
|
|
163
167
|
});
|
|
164
168
|
|
|
169
|
+
test("runUnit failsafe defers cancellation while timeout recovery is making fresh progress", async () => {
|
|
170
|
+
_resetPendingResolve();
|
|
171
|
+
mock.timers.enable();
|
|
172
|
+
const originalCwd = process.cwd();
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
mock.timers.setTime(10_000);
|
|
176
|
+
const ctx = makeMockCtx();
|
|
177
|
+
const pi = makeMockPi();
|
|
178
|
+
const s = makeMockSession();
|
|
179
|
+
s.basePath = mkdtempSync(join(tmpdir(), "gsd-rununit-recovery-"));
|
|
180
|
+
s.currentUnit = { type: "task", id: "T01", startedAt: 1234 };
|
|
181
|
+
|
|
182
|
+
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
183
|
+
await waitForMicrotasks(() => pi.calls.length === 1, "unit dispatch");
|
|
184
|
+
|
|
185
|
+
writeUnitRuntimeRecord(s.basePath, "task", "T01", 1234, {
|
|
186
|
+
phase: "recovered",
|
|
187
|
+
recoveryAttempts: 1,
|
|
188
|
+
lastProgressKind: "hard-recovery-retry",
|
|
189
|
+
lastProgressAt: Date.now(),
|
|
190
|
+
});
|
|
191
|
+
assert.equal(
|
|
192
|
+
shouldDeferUnitFailsafeTimeout(readUnitRuntimeRecord(s.basePath, "task", "T01"), {
|
|
193
|
+
nowMs: Date.now(),
|
|
194
|
+
currentUnitStartedAt: s.currentUnit.startedAt,
|
|
195
|
+
freshProgressMs: 30_000,
|
|
196
|
+
}),
|
|
197
|
+
true,
|
|
198
|
+
"fresh recovery runtime should defer the failsafe",
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
writeUnitRuntimeRecord(s.basePath, "task", "T01", 1234, {
|
|
203
|
+
phase: "recovered",
|
|
204
|
+
recoveryAttempts: 1,
|
|
205
|
+
lastProgressKind: "hard-recovery-retry",
|
|
206
|
+
lastProgressAt: Date.now(),
|
|
207
|
+
});
|
|
208
|
+
}, (30 * 60 * 1000) + 29_000);
|
|
209
|
+
|
|
210
|
+
mock.timers.tick((30 * 60 * 1000) + 31_000);
|
|
211
|
+
await Promise.resolve();
|
|
212
|
+
|
|
213
|
+
resolveAgentEnd(makeEvent());
|
|
214
|
+
const result = await resultPromise;
|
|
215
|
+
assert.equal(result.status, "completed");
|
|
216
|
+
} finally {
|
|
217
|
+
mock.timers.reset();
|
|
218
|
+
process.chdir(originalCwd);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("shouldDeferUnitFailsafeTimeout rejects stale runtime progress", () => {
|
|
223
|
+
assert.equal(
|
|
224
|
+
shouldDeferUnitFailsafeTimeout({
|
|
225
|
+
version: 1,
|
|
226
|
+
unitType: "task",
|
|
227
|
+
unitId: "T01",
|
|
228
|
+
startedAt: 1234,
|
|
229
|
+
updatedAt: 1,
|
|
230
|
+
phase: "recovered",
|
|
231
|
+
wrapupWarningSent: false,
|
|
232
|
+
continueHereFired: false,
|
|
233
|
+
timeoutAt: 1,
|
|
234
|
+
lastProgressAt: 1,
|
|
235
|
+
progressCount: 1,
|
|
236
|
+
lastProgressKind: "hard-recovery-retry",
|
|
237
|
+
recoveryAttempts: 1,
|
|
238
|
+
}, {
|
|
239
|
+
nowMs: 120_000,
|
|
240
|
+
currentUnitStartedAt: 1234,
|
|
241
|
+
freshProgressMs: 30_000,
|
|
242
|
+
}),
|
|
243
|
+
false,
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("shouldDeferUnitFailsafeTimeout rejects future runtime progress", () => {
|
|
248
|
+
assert.equal(
|
|
249
|
+
shouldDeferUnitFailsafeTimeout({
|
|
250
|
+
version: 1,
|
|
251
|
+
unitType: "task",
|
|
252
|
+
unitId: "T01",
|
|
253
|
+
startedAt: 1234,
|
|
254
|
+
updatedAt: 1,
|
|
255
|
+
phase: "recovered",
|
|
256
|
+
wrapupWarningSent: false,
|
|
257
|
+
continueHereFired: false,
|
|
258
|
+
timeoutAt: 1,
|
|
259
|
+
lastProgressAt: 150_000,
|
|
260
|
+
progressCount: 1,
|
|
261
|
+
lastProgressKind: "hard-recovery-retry",
|
|
262
|
+
recoveryAttempts: 1,
|
|
263
|
+
}, {
|
|
264
|
+
nowMs: 120_000,
|
|
265
|
+
currentUnitStartedAt: 1234,
|
|
266
|
+
freshProgressMs: 30_000,
|
|
267
|
+
}),
|
|
268
|
+
false,
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
165
272
|
test("resolveAgentEnd drops event when no promise is pending", () => {
|
|
166
273
|
_resetPendingResolve();
|
|
167
274
|
|
|
@@ -682,7 +789,6 @@ function makeMockDeps(
|
|
|
682
789
|
pruneQueueOrder: () => {},
|
|
683
790
|
isInAutoWorktree: () => false,
|
|
684
791
|
shouldUseWorktreeIsolation: () => false,
|
|
685
|
-
mergeMilestoneToMain: () => ({ pushed: false, codeFilesChanged: true }),
|
|
686
792
|
teardownAutoWorktree: () => {},
|
|
687
793
|
createAutoWorktree: () => "/tmp/wt",
|
|
688
794
|
captureIntegrationBranch: () => {},
|
|
@@ -785,6 +891,7 @@ function makeLoopSession(overrides?: Partial<Record<string, unknown>>) {
|
|
|
785
891
|
lastBudgetAlertLevel: 0,
|
|
786
892
|
pendingVerificationRetry: null,
|
|
787
893
|
pendingCrashRecovery: null,
|
|
894
|
+
verificationRetryFailureHashes: new Map<string, string>(),
|
|
788
895
|
pendingQuickTasks: [],
|
|
789
896
|
sidecarQueue: [],
|
|
790
897
|
autoModeStartModel: null,
|
|
@@ -1294,6 +1401,107 @@ test("autoLoop calls deriveState → resolveDispatch → runUnit in sequence", a
|
|
|
1294
1401
|
);
|
|
1295
1402
|
});
|
|
1296
1403
|
|
|
1404
|
+
test("autoLoop journals post-unit finalize stop after completed unit", async () => {
|
|
1405
|
+
_resetPendingResolve();
|
|
1406
|
+
|
|
1407
|
+
const ctx = makeMockCtx();
|
|
1408
|
+
ctx.ui.setStatus = () => {};
|
|
1409
|
+
ctx.sessionManager = { getSessionFile: () => "/tmp/session.json" };
|
|
1410
|
+
const pi = makeMockPi();
|
|
1411
|
+
const s = makeLoopSession();
|
|
1412
|
+
const journalEvents: Array<{ eventType: string; data?: any }> = [];
|
|
1413
|
+
|
|
1414
|
+
const deps = makeMockDeps({
|
|
1415
|
+
postUnitPreVerification: async () => {
|
|
1416
|
+
deps.callLog.push("postUnitPreVerification");
|
|
1417
|
+
s.lastGitActionFailure = "commit failed";
|
|
1418
|
+
return "dispatched" as const;
|
|
1419
|
+
},
|
|
1420
|
+
emitJournalEvent: (entry: any) => {
|
|
1421
|
+
journalEvents.push(entry);
|
|
1422
|
+
},
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
1426
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1427
|
+
resolveAgentEnd(makeEvent());
|
|
1428
|
+
await loopPromise;
|
|
1429
|
+
|
|
1430
|
+
assert.ok(
|
|
1431
|
+
deps.callLog.includes("postUnitPreVerification"),
|
|
1432
|
+
"completed units must enter post-unit pre-verification before stopping",
|
|
1433
|
+
);
|
|
1434
|
+
assert.ok(
|
|
1435
|
+
!deps.callLog.includes("runPostUnitVerification"),
|
|
1436
|
+
"git-closeout stop should not run later verification phases",
|
|
1437
|
+
);
|
|
1438
|
+
|
|
1439
|
+
const unitEndIndex = journalEvents.findIndex((e) => e.eventType === "unit-end");
|
|
1440
|
+
const finalizeStartIndex = journalEvents.findIndex((e) => e.eventType === "post-unit-finalize-start");
|
|
1441
|
+
const finalizeEndIndex = journalEvents.findIndex((e) => e.eventType === "post-unit-finalize-end");
|
|
1442
|
+
const iterationEndIndex = journalEvents.findIndex((e) => e.eventType === "iteration-end");
|
|
1443
|
+
|
|
1444
|
+
assert.ok(unitEndIndex >= 0, "unit-end should be journaled after agent completion");
|
|
1445
|
+
assert.ok(finalizeStartIndex > unitEndIndex, "post-unit finalize must start after unit-end");
|
|
1446
|
+
assert.ok(finalizeEndIndex > finalizeStartIndex, "post-unit finalize must journal its stop result");
|
|
1447
|
+
assert.ok(iterationEndIndex > finalizeEndIndex, "iteration-end must be emitted even when finalize stops");
|
|
1448
|
+
|
|
1449
|
+
assert.deepEqual(journalEvents[finalizeEndIndex]!.data, {
|
|
1450
|
+
iteration: 1,
|
|
1451
|
+
unitType: "execute-task",
|
|
1452
|
+
unitId: "M001/S01/T01",
|
|
1453
|
+
status: "stopped",
|
|
1454
|
+
action: "break",
|
|
1455
|
+
reason: "git-closeout-failure",
|
|
1456
|
+
});
|
|
1457
|
+
assert.deepEqual(journalEvents[iterationEndIndex]!.data, {
|
|
1458
|
+
iteration: 1,
|
|
1459
|
+
status: "stopped",
|
|
1460
|
+
reason: "git-closeout-failure",
|
|
1461
|
+
unitType: "execute-task",
|
|
1462
|
+
unitId: "M001/S01/T01",
|
|
1463
|
+
failureClass: "git",
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
test("autoLoop journals iteration-end when unit phase breaks after cancelled unit", async () => {
|
|
1468
|
+
_resetPendingResolve();
|
|
1469
|
+
|
|
1470
|
+
const ctx = makeMockCtx();
|
|
1471
|
+
ctx.ui.setStatus = () => {};
|
|
1472
|
+
ctx.sessionManager = { getSessionFile: () => "/tmp/session.json" };
|
|
1473
|
+
const pi = makeMockPi();
|
|
1474
|
+
const s = makeLoopSession();
|
|
1475
|
+
const journalEvents: Array<{ eventType: string; data?: any }> = [];
|
|
1476
|
+
|
|
1477
|
+
const deps = makeMockDeps({
|
|
1478
|
+
emitJournalEvent: (entry: any) => {
|
|
1479
|
+
journalEvents.push(entry);
|
|
1480
|
+
},
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
1484
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1485
|
+
resolveAgentEndCancelled();
|
|
1486
|
+
await loopPromise;
|
|
1487
|
+
|
|
1488
|
+
const unitEndIndex = journalEvents.findIndex(
|
|
1489
|
+
(e) => e.eventType === "unit-end" && e.data?.status === "cancelled",
|
|
1490
|
+
);
|
|
1491
|
+
const iterationEndIndex = journalEvents.findIndex((e) => e.eventType === "iteration-end");
|
|
1492
|
+
|
|
1493
|
+
assert.ok(unitEndIndex >= 0, "cancelled unit should still emit unit-end");
|
|
1494
|
+
assert.ok(iterationEndIndex > unitEndIndex, "unit-phase break must close the iteration after unit-end");
|
|
1495
|
+
assert.deepEqual(journalEvents[iterationEndIndex]!.data, {
|
|
1496
|
+
iteration: 1,
|
|
1497
|
+
status: "stopped",
|
|
1498
|
+
reason: "unit-aborted",
|
|
1499
|
+
unitType: "execute-task",
|
|
1500
|
+
unitId: "M001/S01/T01",
|
|
1501
|
+
failureClass: "execution",
|
|
1502
|
+
});
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1297
1505
|
test("crash lock records session file from AFTER newSession, not before (#1710)", async (t) => {
|
|
1298
1506
|
_resetPendingResolve();
|
|
1299
1507
|
|
|
@@ -1404,86 +1612,152 @@ test("crash lock records session file from AFTER newSession, not before (#1710)"
|
|
|
1404
1612
|
|
|
1405
1613
|
test("autoLoop handles verification retry by continuing loop", async (t) => {
|
|
1406
1614
|
_resetPendingResolve();
|
|
1615
|
+
mock.timers.enable({ apis: ["Date", "setTimeout"], now: 10_000 });
|
|
1407
1616
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1617
|
+
try {
|
|
1618
|
+
const ctx = makeMockCtx();
|
|
1619
|
+
ctx.ui.setStatus = () => {};
|
|
1620
|
+
ctx.sessionManager = { getSessionFile: () => "/tmp/session.json" };
|
|
1621
|
+
const pi = makeMockPi();
|
|
1412
1622
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1623
|
+
let verifyCallCount = 0;
|
|
1624
|
+
let deriveCallCount = 0;
|
|
1625
|
+
const s = makeLoopSession();
|
|
1416
1626
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1627
|
+
// Pre-queued verification actions: each entry provides a side-effect + return value
|
|
1628
|
+
type VerifyAction = { sideEffect?: () => void; response: "retry" | "continue" };
|
|
1629
|
+
const verificationActions: VerifyAction[] = [
|
|
1630
|
+
{
|
|
1631
|
+
sideEffect: () => {
|
|
1632
|
+
// Simulate retry — set pendingVerificationRetry on session
|
|
1633
|
+
s.pendingVerificationRetry = {
|
|
1634
|
+
unitId: "M001/S01/T01",
|
|
1635
|
+
failureContext: "test failed: expected X got Y",
|
|
1636
|
+
attempt: 1,
|
|
1637
|
+
};
|
|
1638
|
+
},
|
|
1639
|
+
response: "retry",
|
|
1640
|
+
},
|
|
1641
|
+
{ response: "continue" },
|
|
1642
|
+
];
|
|
1643
|
+
|
|
1644
|
+
const deps = makeMockDeps({
|
|
1645
|
+
deriveState: async () => {
|
|
1646
|
+
deriveCallCount++;
|
|
1647
|
+
deps.callLog.push("deriveState");
|
|
1648
|
+
return {
|
|
1649
|
+
phase: "executing",
|
|
1650
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
1651
|
+
activeSlice: { id: "S01", title: "Slice 1" },
|
|
1652
|
+
activeTask: { id: "T01" },
|
|
1653
|
+
registry: [{ id: "M001", status: "active" }],
|
|
1654
|
+
blockers: [],
|
|
1655
|
+
} as any;
|
|
1656
|
+
},
|
|
1657
|
+
runPostUnitVerification: async () => {
|
|
1658
|
+
const action = verificationActions[verifyCallCount] ?? { response: "continue" as const };
|
|
1659
|
+
verifyCallCount++;
|
|
1660
|
+
deps.callLog.push("runPostUnitVerification");
|
|
1661
|
+
action.sideEffect?.();
|
|
1662
|
+
return action.response;
|
|
1663
|
+
},
|
|
1664
|
+
postUnitPostVerification: async () => {
|
|
1665
|
+
deps.callLog.push("postUnitPostVerification");
|
|
1666
|
+
// After the retry cycle completes, deactivate
|
|
1667
|
+
s.active = false;
|
|
1668
|
+
return "continue" as const;
|
|
1669
|
+
},
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
1673
|
+
|
|
1674
|
+
// First iteration: runUnit → verification returns "retry" → loop continues
|
|
1675
|
+
await waitForMicrotasks(() => pi.calls.length === 1, "first dispatch");
|
|
1676
|
+
resolveAgentEnd(makeEvent()); // resolve first unit
|
|
1677
|
+
|
|
1678
|
+
await drainMicrotasks(100);
|
|
1679
|
+
mock.timers.tick(30_000);
|
|
1680
|
+
await waitForMicrotasks(() => pi.calls.length === 2, "retry dispatch");
|
|
1681
|
+
resolveAgentEnd(makeEvent()); // resolve retry unit
|
|
1682
|
+
|
|
1683
|
+
await loopPromise;
|
|
1684
|
+
|
|
1685
|
+
// Verify deriveState was called twice (two iterations)
|
|
1686
|
+
const deriveCount = deps.callLog.filter((c) => c === "deriveState").length;
|
|
1687
|
+
assert.ok(
|
|
1688
|
+
deriveCount >= 2,
|
|
1689
|
+
`deriveState should be called at least 2 times (got ${deriveCount})`,
|
|
1690
|
+
);
|
|
1691
|
+
|
|
1692
|
+
// Verify verification was called twice
|
|
1693
|
+
assert.equal(
|
|
1694
|
+
verifyCallCount,
|
|
1695
|
+
2,
|
|
1696
|
+
"verification should have been called twice (once retry, once pass)",
|
|
1697
|
+
);
|
|
1698
|
+
} finally {
|
|
1699
|
+
mock.timers.reset();
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
test("autoLoop pauses instead of redispatching identical verification failure context", async () => {
|
|
1704
|
+
_resetPendingResolve();
|
|
1705
|
+
mock.timers.enable({ apis: ["Date", "setTimeout"], now: 15_000 });
|
|
1706
|
+
|
|
1707
|
+
try {
|
|
1708
|
+
const ctx = makeMockCtx();
|
|
1709
|
+
ctx.ui.setStatus = () => {};
|
|
1710
|
+
ctx.ui.notify = () => {};
|
|
1711
|
+
ctx.sessionManager = { getSessionFile: () => "/tmp/session.json" };
|
|
1712
|
+
const pi = makeMockPi();
|
|
1713
|
+
const s = makeLoopSession();
|
|
1714
|
+
let verifyCallCount = 0;
|
|
1715
|
+
let pauseCallCount = 0;
|
|
1716
|
+
|
|
1717
|
+
const deps = makeMockDeps({
|
|
1718
|
+
deriveState: async () =>
|
|
1719
|
+
({
|
|
1720
|
+
phase: "executing",
|
|
1721
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
1722
|
+
activeSlice: { id: "S01", title: "Slice 1" },
|
|
1723
|
+
activeTask: { id: "T01" },
|
|
1724
|
+
registry: [{ id: "M001", status: "active" }],
|
|
1725
|
+
blockers: [],
|
|
1726
|
+
}) as any,
|
|
1727
|
+
runPostUnitVerification: async () => {
|
|
1728
|
+
verifyCallCount++;
|
|
1729
|
+
deps.callLog.push("runPostUnitVerification");
|
|
1423
1730
|
s.pendingVerificationRetry = {
|
|
1424
1731
|
unitId: "M001/S01/T01",
|
|
1425
1732
|
failureContext: "test failed: expected X got Y",
|
|
1426
|
-
attempt:
|
|
1733
|
+
attempt: verifyCallCount,
|
|
1427
1734
|
};
|
|
1735
|
+
return "retry" as const;
|
|
1428
1736
|
},
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
const deps = makeMockDeps({
|
|
1435
|
-
deriveState: async () => {
|
|
1436
|
-
deriveCallCount++;
|
|
1437
|
-
deps.callLog.push("deriveState");
|
|
1438
|
-
return {
|
|
1439
|
-
phase: "executing",
|
|
1440
|
-
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
1441
|
-
activeSlice: { id: "S01", title: "Slice 1" },
|
|
1442
|
-
activeTask: { id: "T01" },
|
|
1443
|
-
registry: [{ id: "M001", status: "active" }],
|
|
1444
|
-
blockers: [],
|
|
1445
|
-
} as any;
|
|
1446
|
-
},
|
|
1447
|
-
runPostUnitVerification: async () => {
|
|
1448
|
-
const action = verificationActions[verifyCallCount] ?? { response: "continue" as const };
|
|
1449
|
-
verifyCallCount++;
|
|
1450
|
-
deps.callLog.push("runPostUnitVerification");
|
|
1451
|
-
action.sideEffect?.();
|
|
1452
|
-
return action.response;
|
|
1453
|
-
},
|
|
1454
|
-
postUnitPostVerification: async () => {
|
|
1455
|
-
deps.callLog.push("postUnitPostVerification");
|
|
1456
|
-
// After the retry cycle completes, deactivate
|
|
1457
|
-
s.active = false;
|
|
1458
|
-
return "continue" as const;
|
|
1459
|
-
},
|
|
1460
|
-
});
|
|
1461
|
-
|
|
1462
|
-
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
1737
|
+
pauseAuto: async () => {
|
|
1738
|
+
pauseCallCount++;
|
|
1739
|
+
s.active = false;
|
|
1740
|
+
},
|
|
1741
|
+
});
|
|
1463
1742
|
|
|
1464
|
-
|
|
1465
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
1466
|
-
resolveAgentEnd(makeEvent()); // resolve first unit
|
|
1743
|
+
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
1467
1744
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1745
|
+
await waitForMicrotasks(() => pi.calls.length === 1, "first dispatch");
|
|
1746
|
+
resolveAgentEnd(makeEvent());
|
|
1747
|
+
await drainMicrotasks(100);
|
|
1748
|
+
mock.timers.tick(30_000);
|
|
1471
1749
|
|
|
1472
|
-
|
|
1750
|
+
await waitForMicrotasks(() => pi.calls.length === 2, "retry dispatch");
|
|
1751
|
+
resolveAgentEnd(makeEvent());
|
|
1473
1752
|
|
|
1474
|
-
|
|
1475
|
-
const deriveCount = deps.callLog.filter((c) => c === "deriveState").length;
|
|
1476
|
-
assert.ok(
|
|
1477
|
-
deriveCount >= 2,
|
|
1478
|
-
`deriveState should be called at least 2 times (got ${deriveCount})`,
|
|
1479
|
-
);
|
|
1753
|
+
await loopPromise;
|
|
1480
1754
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1755
|
+
assert.equal(verifyCallCount, 2);
|
|
1756
|
+
assert.equal(pi.calls.length, 2, "duplicate failure should not be redispatched a third time");
|
|
1757
|
+
assert.equal(pauseCallCount, 1, "duplicate failure should pause auto-mode");
|
|
1758
|
+
} finally {
|
|
1759
|
+
mock.timers.reset();
|
|
1760
|
+
}
|
|
1487
1761
|
});
|
|
1488
1762
|
|
|
1489
1763
|
test("autoLoop handles dispatch stop action", async (t) => {
|
|
@@ -1870,76 +2144,84 @@ test("stuck detection: window resets recovery when deriveState returns a differe
|
|
|
1870
2144
|
);
|
|
1871
2145
|
});
|
|
1872
2146
|
|
|
1873
|
-
test("stuck detection:
|
|
2147
|
+
test("stuck detection: verification retries remain visible to the sliding window", async () => {
|
|
1874
2148
|
_resetPendingResolve();
|
|
2149
|
+
mock.timers.enable({ apis: ["Date", "setTimeout"], now: 20_000 });
|
|
1875
2150
|
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2151
|
+
try {
|
|
2152
|
+
const ctx = makeMockCtx();
|
|
2153
|
+
ctx.ui.setStatus = () => {};
|
|
2154
|
+
ctx.ui.notify = () => {};
|
|
2155
|
+
const pi = makeMockPi();
|
|
2156
|
+
const s = makeLoopSession();
|
|
1881
2157
|
|
|
1882
|
-
|
|
1883
|
-
|
|
2158
|
+
let verifyCallCount = 0;
|
|
2159
|
+
let stopReason = "";
|
|
1884
2160
|
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
() =>
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
2161
|
+
// Pre-queued responses: 3 retries then a continue (exit). Failure
|
|
2162
|
+
// contexts differ so this test exercises stuck-window behavior without
|
|
2163
|
+
// tripping duplicate-failure suppression.
|
|
2164
|
+
const verifyActions: Array<() => "retry" | "continue"> = [
|
|
2165
|
+
() => { s.pendingVerificationRetry = { unitId: "M001/S01/T01", failureContext: "test failed: 1", attempt: 1 }; return "retry"; },
|
|
2166
|
+
() => { s.pendingVerificationRetry = { unitId: "M001/S01/T01", failureContext: "test failed: 2", attempt: 2 }; return "retry"; },
|
|
2167
|
+
() => { s.pendingVerificationRetry = { unitId: "M001/S01/T01", failureContext: "test failed: 3", attempt: 3 }; return "retry"; },
|
|
2168
|
+
() => { s.active = false; return "continue"; },
|
|
2169
|
+
];
|
|
1892
2170
|
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2171
|
+
const deps = makeMockDeps({
|
|
2172
|
+
deriveState: async () =>
|
|
2173
|
+
({
|
|
2174
|
+
phase: "executing",
|
|
2175
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
2176
|
+
activeSlice: { id: "S01", title: "Slice 1" },
|
|
2177
|
+
activeTask: { id: "T01" },
|
|
2178
|
+
registry: [{ id: "M001", status: "active" }],
|
|
2179
|
+
blockers: [],
|
|
2180
|
+
}) as any,
|
|
2181
|
+
resolveDispatch: async () => ({
|
|
2182
|
+
action: "dispatch" as const,
|
|
2183
|
+
unitType: "execute-task",
|
|
2184
|
+
unitId: "M001/S01/T01",
|
|
2185
|
+
prompt: "do the thing",
|
|
2186
|
+
}),
|
|
2187
|
+
runPostUnitVerification: async () => {
|
|
2188
|
+
const action = verifyActions[verifyCallCount] ?? (() => { s.active = false; return "continue" as const; });
|
|
2189
|
+
verifyCallCount++;
|
|
2190
|
+
deps.callLog.push("runPostUnitVerification");
|
|
2191
|
+
return action();
|
|
2192
|
+
},
|
|
2193
|
+
stopAuto: async (_ctx?: any, _pi?: any, reason?: string) => {
|
|
2194
|
+
deps.callLog.push("stopAuto");
|
|
2195
|
+
stopReason = reason ?? "";
|
|
2196
|
+
s.active = false;
|
|
2197
|
+
},
|
|
2198
|
+
});
|
|
1921
2199
|
|
|
1922
|
-
|
|
2200
|
+
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
1923
2201
|
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2202
|
+
// Resolve agent_end for 3 attempts. The 4th iteration should stop before
|
|
2203
|
+
// dispatch because retry dispatches stay visible to stuck detection.
|
|
2204
|
+
for (let i = 1; i <= 3; i++) {
|
|
2205
|
+
await waitForMicrotasks(() => pi.calls.length === i, `dispatch ${i}`);
|
|
2206
|
+
resolveAgentEnd(makeEvent());
|
|
2207
|
+
await drainMicrotasks(100);
|
|
2208
|
+
mock.timers.tick(30_000);
|
|
2209
|
+
}
|
|
1929
2210
|
|
|
1930
|
-
|
|
2211
|
+
await loopPromise;
|
|
1931
2212
|
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2213
|
+
assert.ok(
|
|
2214
|
+
stopReason.includes("Stuck"),
|
|
2215
|
+
`stuck detection should fire during repeated verification retries, got: ${stopReason}`,
|
|
2216
|
+
);
|
|
2217
|
+
assert.equal(
|
|
2218
|
+
verifyCallCount,
|
|
2219
|
+
3,
|
|
2220
|
+
"verification should stop before a 4th repeated retry dispatch",
|
|
2221
|
+
);
|
|
2222
|
+
} finally {
|
|
2223
|
+
mock.timers.reset();
|
|
2224
|
+
}
|
|
1943
2225
|
});
|
|
1944
2226
|
|
|
1945
2227
|
// ── detectStuck unit tests ────────────────────────────────────────────────────
|
|
@@ -2021,7 +2303,8 @@ test("detectStuck: truncates long error strings", () => {
|
|
|
2021
2303
|
{ key: "A", error: longError },
|
|
2022
2304
|
]);
|
|
2023
2305
|
assert.ok(result?.stuck);
|
|
2024
|
-
assert.ok(result!.reason.
|
|
2306
|
+
assert.ok(result!.reason.includes(longError.slice(0, 200)), "reason should include the truncated error prefix");
|
|
2307
|
+
assert.equal(result!.reason.includes(longError), false, "reason should not include the full long error");
|
|
2025
2308
|
});
|
|
2026
2309
|
|
|
2027
2310
|
// NOTE: the "stuck-detected" / "stuck-counter-reset" debug-log grep was
|
|
@@ -2265,6 +2548,108 @@ test("resolveAgentEndCancelled with errorContext passes it through to resolved p
|
|
|
2265
2548
|
assert.equal(resolved.errorContext!.isTransient, true);
|
|
2266
2549
|
});
|
|
2267
2550
|
|
|
2551
|
+
test("runUnitPhase pauses ghost completions before closeout and finalize side effects", async (t) => {
|
|
2552
|
+
_resetPendingResolve();
|
|
2553
|
+
|
|
2554
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-ghost-completion-"));
|
|
2555
|
+
t.after(() => {
|
|
2556
|
+
_resetPendingResolve();
|
|
2557
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
2558
|
+
});
|
|
2559
|
+
|
|
2560
|
+
let closeoutCalls = 0;
|
|
2561
|
+
let preVerificationCalls = 0;
|
|
2562
|
+
let postVerificationCalls = 0;
|
|
2563
|
+
const journalEvents: any[] = [];
|
|
2564
|
+
const deps = makeMockDeps({
|
|
2565
|
+
closeoutUnit: async () => {
|
|
2566
|
+
closeoutCalls++;
|
|
2567
|
+
},
|
|
2568
|
+
postUnitPreVerification: async () => {
|
|
2569
|
+
preVerificationCalls++;
|
|
2570
|
+
return "continue";
|
|
2571
|
+
},
|
|
2572
|
+
postUnitPostVerification: async () => {
|
|
2573
|
+
postVerificationCalls++;
|
|
2574
|
+
return "continue";
|
|
2575
|
+
},
|
|
2576
|
+
emitJournalEvent: (event: any) => {
|
|
2577
|
+
journalEvents.push(event);
|
|
2578
|
+
},
|
|
2579
|
+
});
|
|
2580
|
+
const ctx = {
|
|
2581
|
+
...makeMockCtx(),
|
|
2582
|
+
ui: {
|
|
2583
|
+
notify: () => {},
|
|
2584
|
+
setStatus: () => {},
|
|
2585
|
+
setWorkingMessage: () => {},
|
|
2586
|
+
},
|
|
2587
|
+
sessionManager: {
|
|
2588
|
+
getEntries: () => [],
|
|
2589
|
+
},
|
|
2590
|
+
modelRegistry: {
|
|
2591
|
+
getProviderAuthMode: () => undefined,
|
|
2592
|
+
isProviderRequestReady: () => true,
|
|
2593
|
+
},
|
|
2594
|
+
} as any;
|
|
2595
|
+
const pi = {
|
|
2596
|
+
...makeMockPi(),
|
|
2597
|
+
sendMessage: () => {
|
|
2598
|
+
queueMicrotask(() => resolveAgentEnd({ messages: [] }));
|
|
2599
|
+
},
|
|
2600
|
+
} as any;
|
|
2601
|
+
const s = makeLoopSession({
|
|
2602
|
+
basePath,
|
|
2603
|
+
canonicalProjectRoot: basePath,
|
|
2604
|
+
originalBasePath: basePath,
|
|
2605
|
+
});
|
|
2606
|
+
let seq = 0;
|
|
2607
|
+
|
|
2608
|
+
const result = await runUnitPhase(
|
|
2609
|
+
{ ctx, pi, s, deps, prefs: undefined, iteration: 1, flowId: "flow-ghost", nextSeq: () => ++seq },
|
|
2610
|
+
{
|
|
2611
|
+
unitType: "execute-task",
|
|
2612
|
+
unitId: "M001/S01/T01",
|
|
2613
|
+
prompt: "do work",
|
|
2614
|
+
finalPrompt: "do work",
|
|
2615
|
+
pauseAfterUatDispatch: false,
|
|
2616
|
+
state: {
|
|
2617
|
+
phase: "executing",
|
|
2618
|
+
activeMilestone: { id: "M001", title: "Milestone" },
|
|
2619
|
+
activeSlice: { id: "S01", title: "Slice" },
|
|
2620
|
+
activeTask: { id: "T01", title: "Task" },
|
|
2621
|
+
registry: [{ id: "M001", title: "Milestone", status: "active" }],
|
|
2622
|
+
recentDecisions: [],
|
|
2623
|
+
blockers: [],
|
|
2624
|
+
nextAction: "",
|
|
2625
|
+
progress: { milestones: { done: 0, total: 1 } },
|
|
2626
|
+
requirements: { active: 0, validated: 0, deferred: 0, outOfScope: 0, blocked: 0, total: 0 },
|
|
2627
|
+
} as any,
|
|
2628
|
+
mid: "M001",
|
|
2629
|
+
midTitle: "Milestone",
|
|
2630
|
+
isRetry: false,
|
|
2631
|
+
previousTier: undefined,
|
|
2632
|
+
},
|
|
2633
|
+
{ recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 },
|
|
2634
|
+
);
|
|
2635
|
+
|
|
2636
|
+
assert.equal(result.action, "break");
|
|
2637
|
+
assert.equal((result as any).reason, "ghost-completion");
|
|
2638
|
+
assert.equal(deps.callLog.includes("pauseAuto"), true);
|
|
2639
|
+
assert.equal(closeoutCalls, 0);
|
|
2640
|
+
assert.equal(preVerificationCalls, 0);
|
|
2641
|
+
assert.equal(postVerificationCalls, 0);
|
|
2642
|
+
assert.equal(s.currentUnit, null);
|
|
2643
|
+
assert.ok(
|
|
2644
|
+
journalEvents.some((event) =>
|
|
2645
|
+
event.eventType === "unit-end" &&
|
|
2646
|
+
event.data?.status === "cancelled" &&
|
|
2647
|
+
event.data?.errorContext?.message.includes("stale ghost completion")
|
|
2648
|
+
),
|
|
2649
|
+
"ghost completion should emit a cancelled unit-end",
|
|
2650
|
+
);
|
|
2651
|
+
});
|
|
2652
|
+
|
|
2268
2653
|
test("resolveAgentEndCancelled without args produces no errorContext field", async () => {
|
|
2269
2654
|
_resetPendingResolve();
|
|
2270
2655
|
|
|
@@ -2316,60 +2701,75 @@ test("session-switch abort grace window is short-lived and resettable", () => {
|
|
|
2316
2701
|
|
|
2317
2702
|
test("autoLoop re-iterates when postUnitPreVerification returns retry (#1571)", async () => {
|
|
2318
2703
|
_resetPendingResolve();
|
|
2704
|
+
mock.timers.enable({ apis: ["Date", "setTimeout"], now: 30_000 });
|
|
2319
2705
|
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2706
|
+
try {
|
|
2707
|
+
const ctx = makeMockCtx();
|
|
2708
|
+
ctx.ui.setStatus = () => {};
|
|
2709
|
+
const pi = makeMockPi();
|
|
2710
|
+
const s = makeLoopSession();
|
|
2324
2711
|
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2712
|
+
let preVerifyCallCount = 0;
|
|
2713
|
+
// Pre-queued responses: first call returns "retry", second returns "continue"
|
|
2714
|
+
const preVerifyResponses = ["retry", "continue"] as const;
|
|
2328
2715
|
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2716
|
+
const deps = makeMockDeps({
|
|
2717
|
+
deriveState: async () => {
|
|
2718
|
+
deps.callLog.push("deriveState");
|
|
2719
|
+
return {
|
|
2720
|
+
phase: "executing",
|
|
2721
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
2722
|
+
activeSlice: { id: "S01", title: "Slice 1" },
|
|
2723
|
+
activeTask: { id: "T01" },
|
|
2724
|
+
registry: [{ id: "M001", status: "active" }],
|
|
2725
|
+
blockers: [],
|
|
2726
|
+
} as any;
|
|
2727
|
+
},
|
|
2728
|
+
postUnitPreVerification: async () => {
|
|
2729
|
+
deps.callLog.push("postUnitPreVerification");
|
|
2730
|
+
const response = preVerifyResponses[preVerifyCallCount++] ?? "continue";
|
|
2731
|
+
if (response === "retry") {
|
|
2732
|
+
s.pendingVerificationRetry = {
|
|
2733
|
+
unitId: "M001/S01/T01",
|
|
2734
|
+
failureContext: "missing artifact",
|
|
2735
|
+
attempt: 1,
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
return response;
|
|
2739
|
+
},
|
|
2740
|
+
postUnitPostVerification: async () => {
|
|
2741
|
+
deps.callLog.push("postUnitPostVerification");
|
|
2742
|
+
s.active = false;
|
|
2743
|
+
return "continue" as const;
|
|
2744
|
+
},
|
|
2745
|
+
});
|
|
2351
2746
|
|
|
2352
|
-
|
|
2747
|
+
const loopPromise = autoLoop(ctx, pi, s, deps);
|
|
2353
2748
|
|
|
2354
|
-
|
|
2355
|
-
|
|
2749
|
+
await waitForMicrotasks(() => pi.calls.length === 1, "first dispatch");
|
|
2750
|
+
resolveAgentEnd(makeEvent());
|
|
2356
2751
|
|
|
2357
|
-
|
|
2358
|
-
|
|
2752
|
+
await drainMicrotasks(100);
|
|
2753
|
+
mock.timers.tick(30_000);
|
|
2754
|
+
await waitForMicrotasks(() => pi.calls.length === 2, "retry dispatch");
|
|
2755
|
+
resolveAgentEnd(makeEvent());
|
|
2359
2756
|
|
|
2360
|
-
|
|
2757
|
+
await loopPromise;
|
|
2361
2758
|
|
|
2362
|
-
|
|
2759
|
+
assert.equal(preVerifyCallCount, 2, "preVerification should be called twice");
|
|
2363
2760
|
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2761
|
+
const postVerifyCalls = deps.callLog.filter(
|
|
2762
|
+
(c: string) => c === "runPostUnitVerification",
|
|
2763
|
+
);
|
|
2764
|
+
const postPostVerifyCalls = deps.callLog.filter(
|
|
2765
|
+
(c: string) => c === "postUnitPostVerification",
|
|
2766
|
+
);
|
|
2370
2767
|
|
|
2371
|
-
|
|
2372
|
-
|
|
2768
|
+
assert.equal(postVerifyCalls.length, 1, "runPostUnitVerification should only be called once");
|
|
2769
|
+
assert.equal(postPostVerifyCalls.length, 1, "postUnitPostVerification should only be called once");
|
|
2770
|
+
} finally {
|
|
2771
|
+
mock.timers.reset();
|
|
2772
|
+
}
|
|
2373
2773
|
});
|
|
2374
2774
|
|
|
2375
2775
|
// ─── stopAuto unitPromise leak regression (#1799) ────────────────────────────
|
|
@@ -2807,6 +3207,77 @@ test("dispatch Worktree Safety wins before stuck detection for execute-task with
|
|
|
2807
3207
|
);
|
|
2808
3208
|
});
|
|
2809
3209
|
|
|
3210
|
+
test("runDispatch runs stuck detection while artifact verification retry is pending (#5719)", async (t) => {
|
|
3211
|
+
_resetPendingResolve();
|
|
3212
|
+
|
|
3213
|
+
const ctx = makeMockCtx();
|
|
3214
|
+
const pi = makeMockPi();
|
|
3215
|
+
const notifications: string[] = [];
|
|
3216
|
+
ctx.ui.notify = (msg: string) => { notifications.push(msg); };
|
|
3217
|
+
|
|
3218
|
+
const basePath = mkdtempSync(join(tmpdir(), "gsd-5719-retry-stuck-"));
|
|
3219
|
+
t.after(() => rmSync(basePath, { recursive: true, force: true }));
|
|
3220
|
+
|
|
3221
|
+
const s = makeLoopSession({
|
|
3222
|
+
basePath,
|
|
3223
|
+
pendingVerificationRetry: {
|
|
3224
|
+
unitId: "M001/S01/T01",
|
|
3225
|
+
failureContext: "ENOENT: no such file or directory, access '/tmp/missing-plan.md'",
|
|
3226
|
+
attempt: 1,
|
|
3227
|
+
},
|
|
3228
|
+
});
|
|
3229
|
+
const deps = makeMockDeps();
|
|
3230
|
+
const loopState = {
|
|
3231
|
+
recentUnits: [
|
|
3232
|
+
{
|
|
3233
|
+
key: "execute-task/M001/S01/T01",
|
|
3234
|
+
error: "ENOENT: no such file or directory, access '/tmp/missing-plan.md'",
|
|
3235
|
+
},
|
|
3236
|
+
{ key: "plan-slice/M001/S02", error: "other failure" },
|
|
3237
|
+
{
|
|
3238
|
+
key: "complete-slice/M001/S01",
|
|
3239
|
+
error: "ENOENT: no such file or directory, access '/tmp/missing-plan.md'",
|
|
3240
|
+
},
|
|
3241
|
+
],
|
|
3242
|
+
stuckRecoveryAttempts: 0,
|
|
3243
|
+
consecutiveFinalizeTimeouts: 0,
|
|
3244
|
+
};
|
|
3245
|
+
|
|
3246
|
+
const result = await runDispatch(
|
|
3247
|
+
{
|
|
3248
|
+
ctx,
|
|
3249
|
+
pi,
|
|
3250
|
+
s,
|
|
3251
|
+
deps,
|
|
3252
|
+
prefs: undefined,
|
|
3253
|
+
iteration: 1,
|
|
3254
|
+
flowId: "test-flow",
|
|
3255
|
+
nextSeq: () => 1,
|
|
3256
|
+
},
|
|
3257
|
+
{
|
|
3258
|
+
state: {
|
|
3259
|
+
phase: "executing",
|
|
3260
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
3261
|
+
activeSlice: { id: "S01", title: "Slice 1" },
|
|
3262
|
+
activeTask: { id: "T01" },
|
|
3263
|
+
registry: [{ id: "M001", status: "active" }],
|
|
3264
|
+
blockers: [],
|
|
3265
|
+
} as any,
|
|
3266
|
+
mid: "M001",
|
|
3267
|
+
midTitle: "Test",
|
|
3268
|
+
},
|
|
3269
|
+
loopState,
|
|
3270
|
+
);
|
|
3271
|
+
|
|
3272
|
+
assert.equal(result.action, "next", "level-1 stuck recovery should still allow the recovery dispatch");
|
|
3273
|
+
assert.equal(loopState.stuckRecoveryAttempts, 1, "stuck recovery should record the first recovery attempt");
|
|
3274
|
+
assert.ok(deps.callLog.includes("invalidateAllCaches"), "stuck recovery should invalidate caches");
|
|
3275
|
+
assert.ok(
|
|
3276
|
+
notifications.some((n) => n.includes("Missing file referenced twice")),
|
|
3277
|
+
"notification should surface the repeated ENOENT stuck reason",
|
|
3278
|
+
);
|
|
3279
|
+
});
|
|
3280
|
+
|
|
2810
3281
|
test("dispatch Worktree Safety stops unknown unit types with missing Tool Contract", async (t) => {
|
|
2811
3282
|
_resetPendingResolve();
|
|
2812
3283
|
|
|
@@ -3272,6 +3743,13 @@ test("autoLoop classifies ModelPolicyDispatchBlockedError as blocked, not a retr
|
|
|
3272
3743
|
);
|
|
3273
3744
|
assert.ok(unitEnd, "should emit unit-end with status=blocked");
|
|
3274
3745
|
assert.equal(unitEnd!.data.reason, "model-policy-dispatch-blocked");
|
|
3746
|
+
const unitEndIndex = journalEvents.findIndex(
|
|
3747
|
+
e => e.eventType === "unit-end" && e.data?.status === "blocked",
|
|
3748
|
+
);
|
|
3749
|
+
const iterationEndIndex = journalEvents.findIndex(
|
|
3750
|
+
e => e.eventType === "iteration-end" && e.data?.status === "blocked",
|
|
3751
|
+
);
|
|
3752
|
+
assert.ok(iterationEndIndex > unitEndIndex, "blocked policy iterations must close after unit-end");
|
|
3275
3753
|
|
|
3276
3754
|
// Loop must pause for manual attention, NOT retry until 3-strike hard stop.
|
|
3277
3755
|
assert.equal(pauseAutoCalls, 1, "should pause once on policy block");
|