@opengsd/gsd-pi 1.1.1-dev.b2556262 → 1.2.0-dev.4813ead6
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-web-branch.d.ts +2 -0
- package/dist/cli-web-branch.js +9 -2
- package/dist/help-text.js +5 -0
- package/dist/project-sessions.js +4 -2
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/ask-user-questions.js +78 -23
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +101 -237
- package/dist/resources/extensions/claude-code-cli/turn-assembler.js +224 -0
- package/dist/resources/extensions/github-sync/templates.js +3 -3
- package/dist/resources/extensions/gsd/artifact-projection.js +14 -0
- package/dist/resources/extensions/gsd/auto/contracts.js +8 -1
- package/dist/resources/extensions/gsd/auto/loop.js +74 -56
- package/dist/resources/extensions/gsd/auto/orchestrator.js +763 -63
- package/dist/resources/extensions/gsd/auto/phases.js +28 -3
- package/dist/resources/extensions/gsd/auto/run-unit.js +2 -1
- package/dist/resources/extensions/gsd/auto/session.js +3 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +16 -4
- package/dist/resources/extensions/gsd/auto-dispatch.js +6 -5
- package/dist/resources/extensions/gsd/auto-model-selection.js +8 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +4 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +191 -9
- package/dist/resources/extensions/gsd/auto-recovery.js +48 -49
- package/dist/resources/extensions/gsd/auto-runtime-state.js +17 -0
- package/dist/resources/extensions/gsd/auto-start.js +12 -23
- package/dist/resources/extensions/gsd/auto-timers.js +16 -2
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +37 -0
- package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +33 -29
- package/dist/resources/extensions/gsd/auto-verification.js +7 -7
- package/dist/resources/extensions/gsd/auto-worktree.js +45 -36
- package/dist/resources/extensions/gsd/auto.js +73 -471
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +28 -37
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +11 -37
- package/dist/resources/extensions/gsd/bootstrap/query-tools.js +2 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +103 -138
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +63 -4
- package/dist/resources/extensions/gsd/closeout-consistency-gate.js +21 -4
- package/dist/resources/extensions/gsd/codebase-generator.js +8 -4
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +3 -0
- package/dist/resources/extensions/gsd/commands-handlers.js +20 -0
- package/dist/resources/extensions/gsd/commands-inspect.js +4 -8
- package/dist/resources/extensions/gsd/commands-maintenance.js +61 -41
- package/dist/resources/extensions/gsd/commands-ship.js +2 -2
- package/dist/resources/extensions/gsd/commands-verdict.js +12 -2
- package/dist/resources/extensions/gsd/db-workspace.js +103 -0
- package/dist/resources/extensions/gsd/debug-logger.js +10 -0
- package/dist/resources/extensions/gsd/delegation-policy.js +2 -10
- package/dist/resources/extensions/gsd/discussion-handoff.js +218 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +9 -0
- package/dist/resources/extensions/gsd/doctor-proactive.js +7 -2
- package/dist/resources/extensions/gsd/doctor.js +16 -9
- package/dist/resources/extensions/gsd/error-classifier.js +1 -1
- package/dist/resources/extensions/gsd/git-conflict-state.js +16 -1
- package/dist/resources/extensions/gsd/gsd-db.js +12 -0
- package/dist/resources/extensions/gsd/guided-flow.js +36 -470
- package/dist/resources/extensions/gsd/guided-unit-completion.js +225 -0
- package/dist/resources/extensions/gsd/markdown-renderer.js +33 -33
- package/dist/resources/extensions/gsd/mcp-filter.js +8 -1
- package/dist/resources/extensions/gsd/mcp-tool-name.js +26 -0
- package/dist/resources/extensions/gsd/md-importer.js +4 -3
- package/dist/resources/extensions/gsd/migrate/safety.js +2 -2
- package/dist/resources/extensions/gsd/migration-auto-check.js +3 -2
- package/dist/resources/extensions/gsd/milestone-closeout-proof.js +72 -0
- package/dist/resources/extensions/gsd/milestone-closeout.js +12 -4
- package/dist/resources/extensions/gsd/milestone-merge-transaction.js +10 -0
- package/dist/resources/extensions/gsd/milestone-planning-persistence.js +156 -0
- package/dist/resources/extensions/gsd/milestone-readiness.js +77 -0
- package/dist/resources/extensions/gsd/milestone-settlement.js +50 -0
- package/dist/resources/extensions/gsd/milestone-validation-evidence.js +73 -0
- package/dist/resources/extensions/gsd/milestone-validation-verdict.js +57 -0
- package/dist/resources/extensions/gsd/native-git-bridge.js +45 -0
- package/dist/resources/extensions/gsd/parallel-eligibility.js +3 -6
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +3 -2
- package/dist/resources/extensions/gsd/preferences-diagnostics.js +67 -0
- package/dist/resources/extensions/gsd/preferences.js +147 -29
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -0
- package/dist/resources/extensions/gsd/prompts/discuss.md +6 -7
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +5 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +6 -6
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +1 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +5 -6
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -0
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +2 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +5 -3
- package/dist/resources/extensions/gsd/provider-payload-policy.js +83 -0
- package/dist/resources/extensions/gsd/pull-request-process.js +13 -0
- package/dist/resources/extensions/gsd/quality-gate-closure.js +109 -0
- package/dist/resources/extensions/gsd/question-transport.js +86 -0
- package/dist/resources/extensions/gsd/roadmap-slices.js +8 -2
- package/dist/resources/extensions/gsd/schemas/parsers.js +6 -1
- package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +3 -2
- package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +21 -1
- package/dist/resources/extensions/gsd/state.js +13 -5
- package/dist/resources/extensions/gsd/templates/plan.md +7 -0
- package/dist/resources/extensions/gsd/templates/project.md +1 -0
- package/dist/resources/extensions/gsd/templates/roadmap.md +1 -1
- package/dist/resources/extensions/gsd/templates/uat.md +5 -1
- package/dist/resources/extensions/gsd/tool-contract.js +52 -8
- package/dist/resources/extensions/gsd/tool-presentation-plan.js +15 -34
- package/dist/resources/extensions/gsd/tool-surface-snapshot.js +17 -0
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +15 -143
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +39 -0
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +15 -78
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +169 -20
- package/dist/resources/extensions/gsd/uat-policy.js +16 -10
- package/dist/resources/extensions/gsd/uat-run.js +9 -14
- package/dist/resources/extensions/gsd/unit-context-composer.js +40 -20
- package/dist/resources/extensions/gsd/unit-runtime.js +3 -2
- package/dist/resources/extensions/gsd/unit-tool-contracts.js +2 -1
- package/dist/resources/extensions/gsd/user-input-boundary.js +65 -4
- package/dist/resources/extensions/gsd/validation-block-guard.js +2 -0
- package/dist/resources/extensions/gsd/web-app-uat.js +80 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +15 -102
- package/dist/resources/extensions/gsd/workflow-reconcile.js +4 -3
- package/dist/resources/extensions/gsd/workflow-tool-surface.js +46 -0
- package/dist/resources/extensions/gsd/workspace-git-guard.js +2 -0
- package/dist/resources/extensions/gsd/worktree-state-projection.js +33 -4
- package/dist/resources/extensions/gsd/worktree-telemetry.js +12 -0
- package/dist/resources/extensions/shared/interview-ui.js +2 -2
- package/dist/resources/shared/claude-runtime-floor.js +182 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -0
- package/dist/update-cmd.js +20 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +8 -8
- 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/api/boot/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +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 +5 -5
- package/dist/web/standalone/.next/server/chunks/5047.js +2 -0
- package/dist/web/standalone/.next/server/chunks/5124.js +1 -0
- package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-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/web/standalone/.next/static/chunks/2659.b7b129ee6a769448.js +1 -0
- package/dist/web/standalone/.next/static/chunks/2772.bfa657f49f955239.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{3616.4113d484a994e411.js → 3616.3c60753b8ffcbd2e.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/4283.e4873b058df143a1.js +2 -0
- package/dist/web/standalone/.next/static/chunks/5826.a46ecdd1cfe8dabc.js +1 -0
- package/dist/web/standalone/.next/static/chunks/796.cf859a427a2cb2ac.js +10 -0
- package/dist/web/standalone/.next/static/chunks/8785.2e5a118797fb2dd2.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-dda80a1ef5587410.js → webpack-fbea77b5f9953368.js} +1 -1
- package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/dist/web/standalone/node_modules/postcss/lib/container.js +26 -18
- package/dist/web/standalone/node_modules/postcss/lib/css-syntax-error.js +47 -14
- package/dist/web/standalone/node_modules/postcss/lib/declaration.js +4 -4
- package/dist/web/standalone/node_modules/postcss/lib/fromJSON.js +3 -3
- package/dist/web/standalone/node_modules/postcss/lib/input.js +54 -29
- package/dist/web/standalone/node_modules/postcss/lib/lazy-result.js +47 -37
- package/dist/web/standalone/node_modules/postcss/lib/map-generator.js +26 -9
- package/dist/web/standalone/node_modules/postcss/lib/no-work-result.js +57 -55
- package/dist/web/standalone/node_modules/postcss/lib/node.js +99 -31
- package/dist/web/standalone/node_modules/postcss/lib/parse.js +1 -1
- package/dist/web/standalone/node_modules/postcss/lib/parser.js +10 -9
- package/dist/web/standalone/node_modules/postcss/lib/postcss.js +12 -12
- package/dist/web/standalone/node_modules/postcss/lib/previous-map.js +30 -11
- package/dist/web/standalone/node_modules/postcss/lib/processor.js +7 -7
- package/dist/web/standalone/node_modules/postcss/lib/result.js +5 -5
- package/dist/web/standalone/node_modules/postcss/lib/rule.js +6 -6
- package/dist/web/standalone/node_modules/postcss/lib/stringifier.js +69 -28
- package/dist/web/standalone/node_modules/postcss/lib/tokenize.js +6 -2
- package/dist/web/standalone/node_modules/postcss/package.json +48 -48
- package/dist/web-mode.d.ts +2 -0
- package/dist/web-mode.js +20 -8
- package/package.json +17 -11
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/dist/session/agent-session-extensions.d.ts +2 -0
- package/packages/gsd-agent-core/dist/session/agent-session-extensions.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/session/agent-session-extensions.js +14 -0
- package/packages/gsd-agent-core/dist/session/agent-session-extensions.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +106 -40
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-widgets.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-widgets.js +6 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-widgets.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/server.d.ts +10 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +8 -0
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +41 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +2 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/image-models.generated.d.ts +30 -0
- package/packages/pi-ai/dist/image-models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/image-models.generated.js +30 -0
- package/packages/pi-ai/dist/image-models.generated.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +8 -127
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +47 -166
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +11 -3
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/dist/components/input.js +1 -1
- package/packages/pi-tui/dist/components/input.js.map +1 -1
- package/packages/pi-tui/dist/keys.d.ts.map +1 -1
- package/packages/pi-tui/dist/keys.js +39 -30
- package/packages/pi-tui/dist/keys.js.map +1 -1
- package/packages/pi-tui/dist/stdin-buffer.d.ts.map +1 -1
- package/packages/pi-tui/dist/stdin-buffer.js +22 -0
- package/packages/pi-tui/dist/stdin-buffer.js.map +1 -1
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/scripts/install/deps.js +10 -0
- package/scripts/link-workspace-packages.cjs +7 -40
- package/src/resources/extensions/ask-user-questions.ts +87 -24
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +126 -289
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +242 -2
- package/src/resources/extensions/claude-code-cli/turn-assembler.ts +287 -0
- package/src/resources/extensions/github-sync/templates.ts +3 -3
- package/src/resources/extensions/github-sync/tests/templates.test.ts +2 -2
- package/src/resources/extensions/gsd/artifact-projection.ts +31 -0
- package/src/resources/extensions/gsd/auto/contracts.ts +40 -121
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -0
- package/src/resources/extensions/gsd/auto/loop.ts +83 -61
- package/src/resources/extensions/gsd/auto/orchestrator.ts +913 -64
- package/src/resources/extensions/gsd/auto/phases.ts +35 -3
- package/src/resources/extensions/gsd/auto/run-unit.ts +2 -1
- package/src/resources/extensions/gsd/auto/session.ts +4 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +18 -4
- package/src/resources/extensions/gsd/auto-dispatch.ts +20 -7
- package/src/resources/extensions/gsd/auto-model-selection.ts +8 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +4 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +220 -9
- package/src/resources/extensions/gsd/auto-recovery.ts +50 -50
- package/src/resources/extensions/gsd/auto-runtime-state.ts +30 -0
- package/src/resources/extensions/gsd/auto-start.ts +17 -20
- package/src/resources/extensions/gsd/auto-timers.ts +16 -2
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +40 -0
- package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +42 -30
- package/src/resources/extensions/gsd/auto-verification.ts +7 -8
- package/src/resources/extensions/gsd/auto-worktree.ts +57 -42
- package/src/resources/extensions/gsd/auto.ts +96 -508
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +29 -37
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +10 -37
- package/src/resources/extensions/gsd/bootstrap/query-tools.ts +2 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +120 -151
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +107 -3
- package/src/resources/extensions/gsd/closeout-consistency-gate.ts +27 -5
- package/src/resources/extensions/gsd/codebase-generator.ts +9 -5
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +3 -0
- package/src/resources/extensions/gsd/commands-handlers.ts +18 -0
- package/src/resources/extensions/gsd/commands-inspect.ts +7 -8
- package/src/resources/extensions/gsd/commands-maintenance.ts +74 -40
- package/src/resources/extensions/gsd/commands-ship.ts +2 -2
- package/src/resources/extensions/gsd/commands-verdict.ts +19 -2
- package/src/resources/extensions/gsd/db-workspace.ts +170 -0
- package/src/resources/extensions/gsd/debug-logger.ts +11 -0
- package/src/resources/extensions/gsd/delegation-policy.ts +3 -11
- package/src/resources/extensions/gsd/discussion-handoff.ts +276 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +9 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +8 -2
- package/src/resources/extensions/gsd/doctor.ts +15 -5
- package/src/resources/extensions/gsd/error-classifier.ts +1 -1
- package/src/resources/extensions/gsd/git-conflict-state.ts +17 -1
- package/src/resources/extensions/gsd/gsd-db.ts +12 -0
- package/src/resources/extensions/gsd/guided-flow.ts +49 -560
- package/src/resources/extensions/gsd/guided-unit-completion.ts +275 -0
- package/src/resources/extensions/gsd/markdown-renderer.ts +40 -20
- package/src/resources/extensions/gsd/mcp-filter.ts +9 -1
- package/src/resources/extensions/gsd/mcp-tool-name.ts +35 -0
- package/src/resources/extensions/gsd/md-importer.ts +3 -3
- package/src/resources/extensions/gsd/migrate/safety.ts +2 -2
- package/src/resources/extensions/gsd/migration-auto-check.ts +2 -2
- package/src/resources/extensions/gsd/milestone-closeout-proof.ts +131 -0
- package/src/resources/extensions/gsd/milestone-closeout.ts +12 -4
- package/src/resources/extensions/gsd/milestone-merge-transaction.ts +47 -0
- package/src/resources/extensions/gsd/milestone-planning-persistence.ts +224 -0
- package/src/resources/extensions/gsd/milestone-readiness.ts +125 -0
- package/src/resources/extensions/gsd/milestone-settlement.ts +81 -0
- package/src/resources/extensions/gsd/milestone-validation-evidence.ts +95 -0
- package/src/resources/extensions/gsd/milestone-validation-verdict.ts +80 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +48 -0
- package/src/resources/extensions/gsd/parallel-eligibility.ts +4 -5
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +6 -2
- package/src/resources/extensions/gsd/preferences-diagnostics.ts +98 -0
- package/src/resources/extensions/gsd/preferences-types.ts +16 -0
- package/src/resources/extensions/gsd/preferences.ts +173 -28
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +6 -7
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +5 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +6 -6
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +1 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +5 -6
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/research-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -3
- package/src/resources/extensions/gsd/provider-payload-policy.ts +140 -0
- package/src/resources/extensions/gsd/pull-request-process.ts +41 -0
- package/src/resources/extensions/gsd/quality-gate-closure.ts +140 -0
- package/src/resources/extensions/gsd/question-transport.ts +138 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +8 -2
- package/src/resources/extensions/gsd/schemas/parsers.ts +6 -1
- package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +6 -2
- package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +31 -10
- package/src/resources/extensions/gsd/state.ts +15 -5
- package/src/resources/extensions/gsd/templates/plan.md +7 -0
- package/src/resources/extensions/gsd/templates/project.md +1 -0
- package/src/resources/extensions/gsd/templates/roadmap.md +1 -1
- package/src/resources/extensions/gsd/templates/uat.md +5 -1
- package/src/resources/extensions/gsd/tests/artifact-db-drift-memo.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/ask-user-questions-render.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +29 -1
- package/src/resources/extensions/gsd/tests/auto-dispatch-baseline-harness.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +321 -5
- package/src/resources/extensions/gsd/tests/auto-milestone-target.test.ts +23 -0
- package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +709 -845
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +38 -10
- package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/canonical-milestone-root.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/commands-dispatcher-workspace-git.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +38 -1
- package/src/resources/extensions/gsd/tests/debug-logger.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +34 -3
- package/src/resources/extensions/gsd/tests/dispatch-run-uat-browser-tools.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +64 -1
- package/src/resources/extensions/gsd/tests/execute-task-rendering.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/fixtures/pr-body/swarm-lane-no-blockers.md +1 -5
- package/src/resources/extensions/gsd/tests/fixtures/pr-body/swarm-lane-with-blockers.md +1 -5
- package/src/resources/extensions/gsd/tests/gate-state-canonicalization.test.ts +48 -1
- package/src/resources/extensions/gsd/tests/integration/merge-strategy-regular.test.ts +157 -0
- package/src/resources/extensions/gsd/tests/markdown-renderer-parse-cache.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/mcp-tool-name.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/milestone-closeout-proof.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/milestone-readiness.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/milestone-validation-evidence.test.ts +41 -0
- package/src/resources/extensions/gsd/tests/milestone-validation-verdict.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/native-merge-regular.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/orchestrator-legacy-parity.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/parse-project-milestone-bridge.test.ts +77 -0
- package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +6 -2
- package/src/resources/extensions/gsd/tests/planning-crossval.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/preferences-diagnostics.test.ts +67 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +183 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +75 -2
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/provider-payload-policy.test.ts +165 -0
- package/src/resources/extensions/gsd/tests/pull-request-process.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +94 -0
- package/src/resources/extensions/gsd/tests/research-milestone-composer.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +25 -1
- package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +80 -0
- package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +101 -1
- package/src/resources/extensions/gsd/tests/stale-queued-milestone.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +21 -6
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/tool-availability-audit.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +35 -42
- package/src/resources/extensions/gsd/tests/uat-policy.test.ts +23 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/user-input-boundary.test.ts +147 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/web-app-uat.test.ts +150 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +126 -9
- package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/worktree-projection-writers.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/worktree-telemetry.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -3
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +79 -0
- package/src/resources/extensions/gsd/tool-contract.ts +86 -8
- package/src/resources/extensions/gsd/tool-presentation-plan.ts +16 -33
- package/src/resources/extensions/gsd/tool-surface-snapshot.ts +47 -0
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +19 -160
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +43 -0
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +25 -84
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +183 -21
- package/src/resources/extensions/gsd/uat-policy.ts +19 -10
- package/src/resources/extensions/gsd/uat-run.ts +10 -14
- package/src/resources/extensions/gsd/unit-context-composer.ts +85 -20
- package/src/resources/extensions/gsd/unit-runtime.ts +3 -2
- package/src/resources/extensions/gsd/unit-tool-contracts.ts +2 -1
- package/src/resources/extensions/gsd/user-input-boundary.ts +55 -5
- package/src/resources/extensions/gsd/validation-block-guard.ts +2 -0
- package/src/resources/extensions/gsd/web-app-uat.ts +101 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +22 -110
- package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -3
- package/src/resources/extensions/gsd/workflow-tool-surface.ts +73 -0
- package/src/resources/extensions/gsd/workspace-git-guard.ts +1 -0
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +7 -16
- package/src/resources/extensions/gsd/worktree-state-projection.ts +55 -7
- package/src/resources/extensions/gsd/worktree-telemetry.ts +16 -0
- package/src/resources/extensions/shared/interview-ui.ts +15 -2
- package/src/resources/shared/claude-runtime-floor.ts +248 -0
- package/dist/web/standalone/.next/server/chunks/678.js +0 -2
- package/dist/web/standalone/.next/static/chunks/2659.feb6499ca863ebfc.js +0 -1
- package/dist/web/standalone/.next/static/chunks/2772.151789db0edea835.js +0 -1
- package/dist/web/standalone/.next/static/chunks/4283.10a065467b5340d8.js +0 -2
- package/dist/web/standalone/.next/static/chunks/5826.960dc4634cc9b0d3.js +0 -1
- package/dist/web/standalone/.next/static/chunks/796.46f811c0fac23aab.js +0 -10
- package/dist/web/standalone/.next/static/chunks/8785.d32f7a61f55c1600.js +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.d.ts +0 -21
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.d.ts.map +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.js +0 -213
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.js.map +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.d.ts +0 -28
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.d.ts.map +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.js +0 -249
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.js.map +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.d.ts +0 -19
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.d.ts.map +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.js +0 -797
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.js.map +0 -1
- package/scripts/ensure-workspace-builds.cjs +0 -129
- /package/dist/web/standalone/.next/static/{tJOKQbQRO-9MiFDO8DIDS → tkLHUSzPA2kMmWz4DmGwI}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{tJOKQbQRO-9MiFDO8DIDS → tkLHUSzPA2kMmWz4DmGwI}/_ssgManifest.js +0 -0
|
@@ -1,26 +1,203 @@
|
|
|
1
1
|
// Project/App: gsd-pi
|
|
2
2
|
// File Purpose: Auto Orchestration module contract and ADR-015 invariant sequence tests.
|
|
3
|
+
//
|
|
4
|
+
// Phase 2 of #442 collapsed the nine adapter seams into AutoOrchestrator. These
|
|
5
|
+
// tests therefore drive the REAL collapsed orchestrator against real temp
|
|
6
|
+
// SQLite + git fixtures (fixture builder modelled on
|
|
7
|
+
// state-reconciliation-drift.test.ts) and inject dispatch decisions through the
|
|
8
|
+
// real unified rule registry (setRegistry) rather than mock adapters. Decision
|
|
9
|
+
// logic is asserted on observable advance() outcomes and journal events instead
|
|
10
|
+
// of an internal calls[] array. Dispatch-decision parity (formerly the
|
|
11
|
+
// createWiredDispatchAdapter tests) is asserted against the exported pure
|
|
12
|
+
// decideOrchestratorDispatch helper.
|
|
3
13
|
|
|
4
14
|
import test from "node:test";
|
|
5
15
|
import assert from "node:assert/strict";
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
6
17
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
7
18
|
import { tmpdir } from "node:os";
|
|
8
19
|
import { join } from "node:path";
|
|
9
20
|
|
|
10
|
-
import {
|
|
11
|
-
|
|
21
|
+
import {
|
|
22
|
+
createAutoOrchestrator,
|
|
23
|
+
decideOrchestratorDispatch,
|
|
24
|
+
resolveLiveOrchestratorBasePath,
|
|
25
|
+
STUCK_WINDOW_SIZE,
|
|
26
|
+
} from "../auto/orchestrator.js";
|
|
27
|
+
import type { OrchestratorContext } from "../auto/orchestrator.js";
|
|
28
|
+
import type { AutoOrchestrationModule, AutoSessionContext } from "../auto/contracts.js";
|
|
12
29
|
import type { GSDState } from "../types.js";
|
|
13
|
-
import { createWiredDispatchAdapter, resolveLiveOrchestratorBasePath } from "../auto.js";
|
|
14
30
|
import { resolveDispatch, type DispatchContext } from "../auto-dispatch.js";
|
|
15
31
|
import { RuleRegistry, setRegistry, resetRegistry } from "../rule-registry.js";
|
|
16
32
|
import type { UnifiedRule } from "../rule-types.js";
|
|
17
33
|
import { supportsStructuredQuestions } from "../workflow-mcp.js";
|
|
18
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
closeDatabase,
|
|
36
|
+
insertAssessment,
|
|
37
|
+
insertGateRow,
|
|
38
|
+
insertMilestone,
|
|
39
|
+
insertSlice,
|
|
40
|
+
insertTask,
|
|
41
|
+
openDatabase,
|
|
42
|
+
} from "../gsd-db.js";
|
|
43
|
+
import { AutoSession } from "../auto/session.js";
|
|
44
|
+
import { acquireSessionLock, releaseSessionLock } from "../session-lock.js";
|
|
45
|
+
import { queryJournal } from "../journal.js";
|
|
46
|
+
import { invalidateAllCaches } from "../cache.js";
|
|
47
|
+
import { invalidateStateCache } from "../state.js";
|
|
48
|
+
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
// Fixture builder
|
|
51
|
+
//
|
|
52
|
+
// Builds a real, isolated project: a git repo (so the pre-dispatch health gate
|
|
53
|
+
// and merge-state reconciliation have something real to probe), a SQLite DB
|
|
54
|
+
// seeded with one active milestone/slice/task, and the matching ROADMAP/PLAN
|
|
55
|
+
// markdown projection. A real session lock is acquired so the orchestrator's
|
|
56
|
+
// ensureLockOwnership passes. A fresh AutoSession is wired to the base path. A
|
|
57
|
+
// dispatch rule is installed in the real unified registry so resolveDispatch
|
|
58
|
+
// yields a deterministic decision — this is the only "injection", and it is the
|
|
59
|
+
// same public seam (setRegistry) the dispatch engine already exposes.
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
type DispatchRuleResult =
|
|
63
|
+
| { action: "dispatch"; unitType: string; unitId: string; prompt: string; pauseAfterDispatch?: boolean }
|
|
64
|
+
| { action: "stop"; reason: string; level: "info" | "warning" | "error" }
|
|
65
|
+
| { action: "skip"; matchedRule?: string };
|
|
66
|
+
|
|
67
|
+
interface FixtureOptions {
|
|
68
|
+
/** When provided, the rule returns this result. Defaults to dispatching M001/S01/T01. */
|
|
69
|
+
dispatch?: () => DispatchRuleResult | Promise<DispatchRuleResult>;
|
|
70
|
+
/** Rule name (becomes the dispatch `reason`/`matchedRule`). */
|
|
71
|
+
ruleName?: string;
|
|
72
|
+
/** Skip seeding a ready task (used for the "no remaining units" / complete scenarios). */
|
|
73
|
+
noTask?: boolean;
|
|
74
|
+
/** Mark the seeded milestone complete (drives the completion → stopped path). */
|
|
75
|
+
complete?: boolean;
|
|
76
|
+
}
|
|
19
77
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
78
|
+
interface Fixture {
|
|
79
|
+
base: string;
|
|
80
|
+
session: AutoSession;
|
|
81
|
+
ctx: OrchestratorContext;
|
|
82
|
+
orchestrator: AutoOrchestrationModule;
|
|
83
|
+
/** Names emitted to the journal by the orchestrator (data.name), in order. */
|
|
84
|
+
journalNames(): string[];
|
|
85
|
+
cleanup(): void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const DEFAULT_DISPATCH: DispatchRuleResult = {
|
|
89
|
+
action: "dispatch",
|
|
90
|
+
unitType: "execute-task",
|
|
91
|
+
unitId: "M001/S01/T01",
|
|
92
|
+
prompt: "fixture-prompt",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function gitInit(base: string): void {
|
|
96
|
+
execFileSync("git", ["init", "--initial-branch=main"], { cwd: base, stdio: "ignore" });
|
|
97
|
+
execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: base, stdio: "ignore" });
|
|
98
|
+
execFileSync("git", ["config", "user.name", "Test"], { cwd: base, stdio: "ignore" });
|
|
99
|
+
writeFileSync(join(base, ".gitkeep"), "");
|
|
100
|
+
execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
|
|
101
|
+
execFileSync("git", ["commit", "-m", "initial"], { cwd: base, stdio: "ignore" });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function makeFixture(opts: FixtureOptions = {}): Fixture {
|
|
105
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-orchestrator-"));
|
|
106
|
+
gitInit(base);
|
|
107
|
+
|
|
108
|
+
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
109
|
+
const sliceDir = join(milestoneDir, "slices", "S01");
|
|
110
|
+
mkdirSync(join(sliceDir, "tasks"), { recursive: true });
|
|
111
|
+
|
|
112
|
+
invalidateAllCaches();
|
|
113
|
+
invalidateStateCache();
|
|
114
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
115
|
+
insertMilestone({ id: "M001", title: "Milestone", status: opts.complete ? "complete" : "active" });
|
|
116
|
+
if (!opts.noTask && !opts.complete) {
|
|
117
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "active", risk: "low", depends: [], demo: "", sequence: 1 });
|
|
118
|
+
insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "Task", status: "active" });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
writeFileSync(
|
|
122
|
+
join(milestoneDir, "M001-ROADMAP.md"),
|
|
123
|
+
[
|
|
124
|
+
"# M001: Milestone",
|
|
125
|
+
"",
|
|
126
|
+
"**Vision:** Fixture milestone",
|
|
127
|
+
"",
|
|
128
|
+
"## Slices",
|
|
129
|
+
"",
|
|
130
|
+
"- [ ] **S01: Slice** `risk:low` `depends:[]`",
|
|
131
|
+
"",
|
|
132
|
+
].join("\n"),
|
|
133
|
+
);
|
|
134
|
+
if (!opts.noTask && !opts.complete) {
|
|
135
|
+
writeFileSync(
|
|
136
|
+
join(sliceDir, "S01-PLAN.md"),
|
|
137
|
+
[
|
|
138
|
+
"# S01: Slice",
|
|
139
|
+
"",
|
|
140
|
+
"**Goal:** Fixture goal",
|
|
141
|
+
"**Demo:** Fixture demo",
|
|
142
|
+
"",
|
|
143
|
+
"## Must-Haves",
|
|
144
|
+
"",
|
|
145
|
+
"- Everything works",
|
|
146
|
+
"",
|
|
147
|
+
"## Tasks",
|
|
148
|
+
"",
|
|
149
|
+
"- [ ] **T01: Task** `est:1h`",
|
|
150
|
+
"",
|
|
151
|
+
].join("\n"),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
acquireSessionLock(base);
|
|
156
|
+
|
|
157
|
+
const session = new AutoSession();
|
|
158
|
+
session.basePath = base;
|
|
159
|
+
session.originalBasePath = base;
|
|
160
|
+
session.currentMilestoneId = "M001";
|
|
161
|
+
session.resourceVersionOnStart = null;
|
|
162
|
+
|
|
163
|
+
const ctx: OrchestratorContext = {
|
|
164
|
+
ctx: { model: {}, modelRegistry: { getAll: () => [] }, ui: { notify() {} } } as never,
|
|
165
|
+
pi: { getActiveTools: () => [] } as never,
|
|
166
|
+
dispatchBasePath: base,
|
|
167
|
+
runtimeBasePath: base,
|
|
168
|
+
session,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const ruleName = opts.ruleName ?? "fixture-dispatch";
|
|
172
|
+
const decide = opts.dispatch ?? (() => DEFAULT_DISPATCH);
|
|
173
|
+
const rule: UnifiedRule = {
|
|
174
|
+
name: ruleName,
|
|
175
|
+
when: "dispatch",
|
|
176
|
+
evaluation: "first-match",
|
|
177
|
+
where: async () => decide(),
|
|
178
|
+
then: (r: unknown) => r,
|
|
179
|
+
};
|
|
180
|
+
setRegistry(new RuleRegistry([rule]));
|
|
181
|
+
|
|
182
|
+
const orchestrator = createAutoOrchestrator(ctx);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
base,
|
|
186
|
+
session,
|
|
187
|
+
ctx,
|
|
188
|
+
orchestrator,
|
|
189
|
+
journalNames() {
|
|
190
|
+
return queryJournal(base)
|
|
191
|
+
.map((e) => (e.data as Record<string, unknown> | undefined)?.name)
|
|
192
|
+
.filter((n): n is string => typeof n === "string");
|
|
193
|
+
},
|
|
194
|
+
cleanup() {
|
|
195
|
+
resetRegistry();
|
|
196
|
+
try { releaseSessionLock(base); } catch { /* */ }
|
|
197
|
+
try { closeDatabase(); } catch { /* */ }
|
|
198
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
|
|
199
|
+
},
|
|
200
|
+
};
|
|
24
201
|
}
|
|
25
202
|
|
|
26
203
|
function makeState(): GSDState {
|
|
@@ -38,637 +215,465 @@ function makeState(): GSDState {
|
|
|
38
215
|
};
|
|
39
216
|
}
|
|
40
217
|
|
|
41
|
-
|
|
42
|
-
const calls: string[] = [];
|
|
43
|
-
const stateSnapshot = makeState();
|
|
44
|
-
|
|
45
|
-
const deps: AutoOrchestratorDeps = {
|
|
46
|
-
stateReconciliation: {
|
|
47
|
-
async reconcileBeforeDispatch() {
|
|
48
|
-
calls.push("state.reconcile");
|
|
49
|
-
return { ok: true, stateSnapshot };
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
dispatch: {
|
|
53
|
-
async decideNextUnit(input) {
|
|
54
|
-
calls.push("dispatch.decide");
|
|
55
|
-
assert.equal(input.stateSnapshot, stateSnapshot);
|
|
56
|
-
return { unitType: "execute-task", unitId: "T01", reason: "ready", preconditions: [] };
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
toolContract: {
|
|
60
|
-
async compileUnitToolContract() {
|
|
61
|
-
calls.push("tool.compile");
|
|
62
|
-
return { ok: true };
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
recovery: {
|
|
66
|
-
async classifyAndRecover() {
|
|
67
|
-
calls.push("recovery.classify");
|
|
68
|
-
return { action: "stop", reason: "fatal" };
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
worktree: {
|
|
72
|
-
async prepareForUnit() {
|
|
73
|
-
calls.push("worktree.prepare");
|
|
74
|
-
return { ok: true };
|
|
75
|
-
},
|
|
76
|
-
async syncAfterUnit() { calls.push("worktree.sync"); },
|
|
77
|
-
async cleanupOnStop() { calls.push("worktree.cleanup"); },
|
|
78
|
-
},
|
|
79
|
-
health: {
|
|
80
|
-
checkResourcesStale() {
|
|
81
|
-
calls.push("health.stale");
|
|
82
|
-
return null;
|
|
83
|
-
},
|
|
84
|
-
async preAdvanceGate() {
|
|
85
|
-
calls.push("health.pre");
|
|
86
|
-
return { kind: "pass" };
|
|
87
|
-
},
|
|
88
|
-
async postAdvanceRecord() { calls.push("health.post"); },
|
|
89
|
-
},
|
|
90
|
-
runtime: {
|
|
91
|
-
async ensureLockOwnership() { calls.push("runtime.lock"); },
|
|
92
|
-
async journalTransition(event) { calls.push(`journal:${event.name}`); },
|
|
93
|
-
},
|
|
94
|
-
notifications: {
|
|
95
|
-
async notifyLifecycle(event) { calls.push(`notify:${event.name}`); },
|
|
96
|
-
},
|
|
97
|
-
uokGate: {
|
|
98
|
-
async emit(input) { calls.push(`gate:${input.gateId}:${input.outcome}`); },
|
|
99
|
-
},
|
|
100
|
-
};
|
|
218
|
+
const SESSION_CONTEXT: AutoSessionContext = { basePath: "/tmp/project", trigger: "manual" };
|
|
101
219
|
|
|
102
|
-
|
|
103
|
-
|
|
220
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
221
|
+
// Lifecycle: start / resume / stop
|
|
222
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
104
223
|
|
|
105
|
-
test("start() enters running phase without dispatching", async () => {
|
|
106
|
-
const
|
|
107
|
-
|
|
224
|
+
test("start() enters running phase without dispatching", async (t) => {
|
|
225
|
+
const f = makeFixture();
|
|
226
|
+
t.after(() => f.cleanup());
|
|
108
227
|
|
|
109
|
-
const result = await orchestrator.start(
|
|
228
|
+
const result = await f.orchestrator.start(SESSION_CONTEXT);
|
|
110
229
|
|
|
111
230
|
assert.equal(result.kind, "started");
|
|
112
|
-
const status = orchestrator.getStatus();
|
|
231
|
+
const status = f.orchestrator.getStatus();
|
|
113
232
|
assert.equal(status.phase, "running");
|
|
114
233
|
assert.equal(status.activeUnit, undefined);
|
|
115
|
-
assert.ok(
|
|
116
|
-
assert.ok(!
|
|
234
|
+
assert.ok(f.journalNames().includes("start"));
|
|
235
|
+
assert.ok(!f.journalNames().includes("advance"));
|
|
117
236
|
});
|
|
118
237
|
|
|
119
|
-
test("
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
checkResourcesStale: () => null,
|
|
123
|
-
async preAdvanceGate() { return { kind: "fail", reason: "doctor-block" }; },
|
|
124
|
-
async postAdvanceRecord() {},
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
238
|
+
test("resume() enters running phase without dispatching", async (t) => {
|
|
239
|
+
const f = makeFixture();
|
|
240
|
+
t.after(() => f.cleanup());
|
|
128
241
|
|
|
129
|
-
const result = await orchestrator.
|
|
242
|
+
const result = await f.orchestrator.resume();
|
|
130
243
|
|
|
131
|
-
|
|
132
|
-
assert.equal(
|
|
133
|
-
assert.
|
|
134
|
-
assert.ok(calls.includes("gate:pre-dispatch-health-gate:manual-attention"));
|
|
244
|
+
assert.equal(result.kind, "resumed");
|
|
245
|
+
assert.equal(f.orchestrator.getStatus().phase, "running");
|
|
246
|
+
assert.ok(!f.journalNames().includes("advance"));
|
|
135
247
|
});
|
|
136
248
|
|
|
137
|
-
test("
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
checkResourcesStale: () => null,
|
|
141
|
-
async preAdvanceGate() {
|
|
142
|
-
return { kind: "fail", reason: "Could not verify git conflict state", action: "stop" };
|
|
143
|
-
},
|
|
144
|
-
async postAdvanceRecord() {},
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
249
|
+
test("transitionCount increases across lifecycle transitions", async (t) => {
|
|
250
|
+
const f = makeFixture();
|
|
251
|
+
t.after(() => f.cleanup());
|
|
148
252
|
|
|
149
|
-
const
|
|
253
|
+
const before = f.orchestrator.getStatus().transitionCount;
|
|
254
|
+
await f.orchestrator.start(SESSION_CONTEXT);
|
|
255
|
+
const afterStart = f.orchestrator.getStatus().transitionCount;
|
|
256
|
+
await f.orchestrator.stop("done");
|
|
257
|
+
const afterStop = f.orchestrator.getStatus().transitionCount;
|
|
150
258
|
|
|
151
|
-
|
|
152
|
-
assert.
|
|
259
|
+
assert.ok(afterStart > before);
|
|
260
|
+
assert.ok(afterStop > afterStart);
|
|
153
261
|
});
|
|
154
262
|
|
|
155
|
-
test("
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
checkResourcesStale: () => "resources changed since session start",
|
|
159
|
-
async preAdvanceGate() { return { kind: "pass" }; },
|
|
160
|
-
async postAdvanceRecord() {},
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
263
|
+
test("stop() transitions to stopped and journals stop", async (t) => {
|
|
264
|
+
const f = makeFixture();
|
|
265
|
+
t.after(() => f.cleanup());
|
|
164
266
|
|
|
165
|
-
const result = await orchestrator.
|
|
267
|
+
const result = await f.orchestrator.stop("user-request");
|
|
166
268
|
|
|
167
|
-
|
|
168
|
-
assert.equal(
|
|
169
|
-
assert.
|
|
170
|
-
assert.ok(calls.includes("gate:resource-version-guard:fail"));
|
|
171
|
-
assert.ok(!calls.includes("health.pre"));
|
|
172
|
-
assert.ok(!calls.includes("state.reconcile"));
|
|
269
|
+
assert.equal(result.kind, "stopped");
|
|
270
|
+
assert.equal(f.orchestrator.getStatus().phase, "stopped");
|
|
271
|
+
assert.ok(f.journalNames().includes("stop"));
|
|
173
272
|
});
|
|
174
273
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
staleMsg: string | null;
|
|
179
|
-
gateResult: Awaited<ReturnType<AutoOrchestratorDeps["health"]["preAdvanceGate"]>>;
|
|
180
|
-
expectedKind: "advanced" | "blocked";
|
|
181
|
-
expectedAction?: "pause" | "stop";
|
|
182
|
-
expectedReason?: string;
|
|
183
|
-
expectedGates: string[];
|
|
184
|
-
};
|
|
185
|
-
const scenarios: Scenario[] = [
|
|
186
|
-
{
|
|
187
|
-
name: "pass",
|
|
188
|
-
staleMsg: null,
|
|
189
|
-
gateResult: { kind: "pass" },
|
|
190
|
-
expectedKind: "advanced",
|
|
191
|
-
expectedGates: [
|
|
192
|
-
"resource-version-guard:policy:pass:none:resource version guard passed:",
|
|
193
|
-
"pre-dispatch-health-gate:execution:pass:none:pre-dispatch health gate passed:",
|
|
194
|
-
],
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
name: "resource-stale",
|
|
198
|
-
staleMsg: "resources changed since session start",
|
|
199
|
-
gateResult: { kind: "pass" },
|
|
200
|
-
expectedKind: "blocked",
|
|
201
|
-
expectedAction: "pause",
|
|
202
|
-
expectedReason: "resources changed since session start",
|
|
203
|
-
expectedGates: [
|
|
204
|
-
"resource-version-guard:policy:fail:policy:resource version guard blocked dispatch:resources changed since session start",
|
|
205
|
-
],
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
name: "health-gate-fail",
|
|
209
|
-
staleMsg: null,
|
|
210
|
-
gateResult: { kind: "fail", reason: "doctor-block" },
|
|
211
|
-
expectedKind: "blocked",
|
|
212
|
-
expectedAction: "pause",
|
|
213
|
-
expectedReason: "doctor-block",
|
|
214
|
-
expectedGates: [
|
|
215
|
-
"resource-version-guard:policy:pass:none:resource version guard passed:",
|
|
216
|
-
"pre-dispatch-health-gate:execution:manual-attention:manual-attention:pre-dispatch health gate blocked dispatch:doctor-block",
|
|
217
|
-
],
|
|
218
|
-
},
|
|
219
|
-
];
|
|
220
|
-
|
|
221
|
-
for (const scenario of scenarios) {
|
|
222
|
-
const gateEvents: string[] = [];
|
|
223
|
-
const { deps } = makeDeps({
|
|
224
|
-
health: {
|
|
225
|
-
checkResourcesStale: () => scenario.staleMsg,
|
|
226
|
-
async preAdvanceGate() { return scenario.gateResult; },
|
|
227
|
-
async postAdvanceRecord() {},
|
|
228
|
-
},
|
|
229
|
-
uokGate: {
|
|
230
|
-
async emit(input) {
|
|
231
|
-
gateEvents.push(
|
|
232
|
-
`${input.gateId}:${input.gateType}:${input.outcome}:${input.failureClass}:${input.rationale}:${input.findings ?? ""}`,
|
|
233
|
-
);
|
|
234
|
-
},
|
|
235
|
-
},
|
|
236
|
-
});
|
|
237
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
238
|
-
const result = await orchestrator.advance();
|
|
239
|
-
|
|
240
|
-
assert.equal(result.kind, scenario.expectedKind, `${scenario.name} result kind`);
|
|
241
|
-
if (scenario.expectedKind === "blocked") {
|
|
242
|
-
assertBlockedResult(result);
|
|
243
|
-
assert.equal(result.action, scenario.expectedAction, `${scenario.name} blocked action`);
|
|
244
|
-
assert.equal(result.reason, scenario.expectedReason, `${scenario.name} blocked reason`);
|
|
245
|
-
}
|
|
246
|
-
assert.deepEqual(gateEvents, scenario.expectedGates, `${scenario.name} gate parity`);
|
|
247
|
-
}
|
|
248
|
-
});
|
|
274
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
275
|
+
// advance(): happy path + ADR-015 invariant sequence
|
|
276
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
249
277
|
|
|
250
|
-
test("advance()
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
checkResourcesStale: () => null,
|
|
254
|
-
async preAdvanceGate() { return { kind: "threw", error: new Error("boom") }; },
|
|
255
|
-
async postAdvanceRecord() {},
|
|
256
|
-
},
|
|
257
|
-
});
|
|
258
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
278
|
+
test("advance() dispatches the resolved unit and journals advance", async (t) => {
|
|
279
|
+
const f = makeFixture();
|
|
280
|
+
t.after(() => f.cleanup());
|
|
259
281
|
|
|
260
|
-
const result = await orchestrator.advance();
|
|
282
|
+
const result = await f.orchestrator.advance();
|
|
261
283
|
|
|
262
284
|
assert.equal(result.kind, "advanced");
|
|
263
|
-
|
|
264
|
-
assert.
|
|
265
|
-
assert.
|
|
285
|
+
if (result.kind !== "advanced") return;
|
|
286
|
+
assert.deepEqual(result.unit, { unitType: "execute-task", unitId: "M001/S01/T01" });
|
|
287
|
+
assert.equal(f.orchestrator.getStatus().phase, "running");
|
|
288
|
+
// Journal records the advance AFTER the invariant gates (lock, health,
|
|
289
|
+
// reconcile, dispatch, tool-contract, worktree) — i.e. no advance-blocked.
|
|
290
|
+
const names = f.journalNames();
|
|
291
|
+
assert.ok(names.includes("advance"));
|
|
292
|
+
assert.ok(!names.includes("advance-blocked"));
|
|
266
293
|
});
|
|
267
294
|
|
|
268
|
-
test("advance()
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
health: {
|
|
272
|
-
checkResourcesStale: () => null,
|
|
273
|
-
async preAdvanceGate() { return { kind: "pass", fixesApplied: ["fix-a", "fix-b"] }; },
|
|
274
|
-
async postAdvanceRecord() {},
|
|
275
|
-
},
|
|
276
|
-
uokGate: {
|
|
277
|
-
async emit(input) {
|
|
278
|
-
if (input.gateId === "pre-dispatch-health-gate" && input.outcome === "pass") {
|
|
279
|
-
observed = input.findings ?? "";
|
|
280
|
-
}
|
|
281
|
-
},
|
|
282
|
-
},
|
|
283
|
-
});
|
|
284
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
295
|
+
test("advance() sets active unit and is reflected in status", async (t) => {
|
|
296
|
+
const f = makeFixture();
|
|
297
|
+
t.after(() => f.cleanup());
|
|
285
298
|
|
|
286
|
-
await orchestrator.advance();
|
|
299
|
+
await f.orchestrator.advance();
|
|
287
300
|
|
|
288
|
-
assert.
|
|
301
|
+
assert.deepEqual(f.orchestrator.getStatus().activeUnit, {
|
|
302
|
+
unitType: "execute-task",
|
|
303
|
+
unitId: "M001/S01/T01",
|
|
304
|
+
});
|
|
289
305
|
});
|
|
290
306
|
|
|
291
|
-
test("advance()
|
|
292
|
-
const
|
|
293
|
-
|
|
307
|
+
test("advance() blocks source dispatch when an earlier slice is incomplete", async (t) => {
|
|
308
|
+
const f = makeFixture({
|
|
309
|
+
dispatch: () => ({
|
|
310
|
+
action: "dispatch",
|
|
311
|
+
unitType: "execute-task",
|
|
312
|
+
unitId: "M001/S02/T01",
|
|
313
|
+
prompt: "fixture-prompt",
|
|
314
|
+
}),
|
|
315
|
+
});
|
|
316
|
+
t.after(() => f.cleanup());
|
|
317
|
+
|
|
318
|
+
insertSlice({
|
|
319
|
+
id: "S02",
|
|
320
|
+
milestoneId: "M001",
|
|
321
|
+
title: "Second slice",
|
|
322
|
+
status: "active",
|
|
323
|
+
risk: "low",
|
|
324
|
+
depends: [],
|
|
325
|
+
demo: "",
|
|
326
|
+
sequence: 2,
|
|
327
|
+
});
|
|
328
|
+
insertTask({
|
|
329
|
+
id: "T01",
|
|
330
|
+
sliceId: "S02",
|
|
331
|
+
milestoneId: "M001",
|
|
332
|
+
title: "Second task",
|
|
333
|
+
status: "active",
|
|
334
|
+
});
|
|
294
335
|
|
|
295
|
-
const result = await orchestrator.advance();
|
|
336
|
+
const result = await f.orchestrator.advance();
|
|
296
337
|
|
|
297
|
-
assert.equal(result.kind, "
|
|
298
|
-
|
|
299
|
-
assert.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
"gate:pre-dispatch-health-gate:pass",
|
|
305
|
-
"state.reconcile",
|
|
306
|
-
"dispatch.decide",
|
|
307
|
-
"tool.compile",
|
|
308
|
-
"worktree.prepare",
|
|
309
|
-
"journal:advance",
|
|
310
|
-
"worktree.sync",
|
|
311
|
-
"health.post",
|
|
312
|
-
]);
|
|
338
|
+
assert.equal(result.kind, "blocked");
|
|
339
|
+
if (result.kind !== "blocked") return;
|
|
340
|
+
assert.equal(result.action, "stop");
|
|
341
|
+
assert.match(result.reason, /earlier slice M001\/S01 is not complete/);
|
|
342
|
+
assert.equal(f.session.pendingOrchestrationDispatch, null);
|
|
343
|
+
assert.deepEqual(f.orchestrator.getStatus().activeUnit, undefined);
|
|
344
|
+
assert.ok(f.journalNames().includes("advance-blocked"));
|
|
313
345
|
});
|
|
314
346
|
|
|
315
|
-
test("
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
async reconcileBeforeDispatch() {
|
|
319
|
-
calls.push("state.reconcile");
|
|
320
|
-
return { ok: false, reason: "state drift blocked", stateSnapshot: makeState() };
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
347
|
+
test("getStatus() returns defensive copy of activeUnit", async (t) => {
|
|
348
|
+
const f = makeFixture();
|
|
349
|
+
t.after(() => f.cleanup());
|
|
325
350
|
|
|
326
|
-
|
|
351
|
+
await f.orchestrator.advance();
|
|
352
|
+
const snap1 = f.orchestrator.getStatus();
|
|
353
|
+
if (snap1.activeUnit) snap1.activeUnit.unitId = "MUTATED";
|
|
354
|
+
const snap2 = f.orchestrator.getStatus();
|
|
327
355
|
|
|
328
|
-
|
|
329
|
-
assert.equal(result.reason, "state drift blocked");
|
|
330
|
-
assert.equal(result.action, "pause");
|
|
331
|
-
assert.ok(!calls.includes("dispatch.decide"));
|
|
332
|
-
assert.ok(calls.includes("journal:advance-blocked"));
|
|
356
|
+
assert.equal(snap2.activeUnit?.unitId, "M001/S01/T01");
|
|
333
357
|
});
|
|
334
358
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
},
|
|
359
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
360
|
+
// Dispatch passthrough decisions (skip / blocked / no-remaining-units)
|
|
361
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
test("advance() keeps running when dispatch intentionally skips a phase", async (t) => {
|
|
364
|
+
const f = makeFixture({
|
|
365
|
+
dispatch: () => ({ action: "skip", matchedRule: "evaluating-gates skipped after marking gates omitted" }),
|
|
343
366
|
});
|
|
344
|
-
|
|
367
|
+
t.after(() => f.cleanup());
|
|
345
368
|
|
|
346
|
-
const result = await orchestrator.advance();
|
|
369
|
+
const result = await f.orchestrator.advance();
|
|
347
370
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
assert.equal(result.
|
|
351
|
-
assert.
|
|
352
|
-
|
|
353
|
-
assert.ok(
|
|
371
|
+
assert.equal(result.kind, "skipped");
|
|
372
|
+
if (result.kind !== "skipped") return;
|
|
373
|
+
assert.equal(result.reason, "evaluating-gates skipped after marking gates omitted");
|
|
374
|
+
assert.equal(f.orchestrator.getStatus().phase, "running");
|
|
375
|
+
const names = f.journalNames();
|
|
376
|
+
assert.ok(names.includes("advance-skipped"));
|
|
377
|
+
assert.ok(!names.includes("advance-stopped"));
|
|
354
378
|
});
|
|
355
379
|
|
|
356
|
-
test("advance()
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
calls.push("worktree.prepare");
|
|
361
|
-
return { ok: false, reason: "worktree invalid" };
|
|
362
|
-
},
|
|
363
|
-
async syncAfterUnit() { calls.push("worktree.sync"); },
|
|
364
|
-
async cleanupOnStop() { calls.push("worktree.cleanup"); },
|
|
365
|
-
},
|
|
380
|
+
test("advance() surfaces dispatch blocker reason instead of generic no remaining units", async (t) => {
|
|
381
|
+
const reason = "Milestone M001 validation verdict is needs-remediation but all slices are complete.";
|
|
382
|
+
const f = makeFixture({
|
|
383
|
+
dispatch: () => ({ action: "stop", reason, level: "warning" }),
|
|
366
384
|
});
|
|
367
|
-
|
|
385
|
+
t.after(() => f.cleanup());
|
|
368
386
|
|
|
369
|
-
const result = await orchestrator.advance();
|
|
387
|
+
const result = await f.orchestrator.advance();
|
|
370
388
|
|
|
371
|
-
|
|
372
|
-
|
|
389
|
+
assert.equal(result.kind, "blocked");
|
|
390
|
+
if (result.kind !== "blocked") return;
|
|
391
|
+
assert.equal(result.reason, reason);
|
|
373
392
|
assert.equal(result.action, "pause");
|
|
374
|
-
|
|
375
|
-
assert.ok(
|
|
376
|
-
assert.ok(
|
|
393
|
+
const names = f.journalNames();
|
|
394
|
+
assert.ok(names.includes("advance-blocked"));
|
|
395
|
+
assert.ok(!names.includes("advance-stopped"));
|
|
377
396
|
});
|
|
378
397
|
|
|
379
|
-
test("advance()
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
async prepareForUnit() {
|
|
383
|
-
calls.push("worktree.prepare");
|
|
384
|
-
return { ok: true, reason: "isolation-not-worktree" };
|
|
385
|
-
},
|
|
386
|
-
async syncAfterUnit() { calls.push("worktree.sync"); },
|
|
387
|
-
async cleanupOnStop() { calls.push("worktree.cleanup"); },
|
|
388
|
-
},
|
|
398
|
+
test("advance() stop level=error blocks with action stop", async (t) => {
|
|
399
|
+
const f = makeFixture({
|
|
400
|
+
dispatch: () => ({ action: "stop", reason: "hard blocker", level: "error" }),
|
|
389
401
|
});
|
|
390
|
-
|
|
402
|
+
t.after(() => f.cleanup());
|
|
391
403
|
|
|
392
|
-
const result = await orchestrator.advance();
|
|
404
|
+
const result = await f.orchestrator.advance();
|
|
393
405
|
|
|
394
|
-
assert.equal(result.kind, "
|
|
395
|
-
|
|
396
|
-
assert.
|
|
406
|
+
assert.equal(result.kind, "blocked");
|
|
407
|
+
if (result.kind !== "blocked") return;
|
|
408
|
+
assert.equal(result.action, "stop");
|
|
397
409
|
});
|
|
398
410
|
|
|
399
|
-
test("advance()
|
|
400
|
-
const {
|
|
401
|
-
|
|
402
|
-
async decideNextUnit() { return null; },
|
|
403
|
-
},
|
|
404
|
-
});
|
|
405
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
411
|
+
test("advance() reports completion when complete state has no next unit", async (t) => {
|
|
412
|
+
const f = makeFixture({ complete: true, noTask: true });
|
|
413
|
+
t.after(() => f.cleanup());
|
|
406
414
|
|
|
407
|
-
const result = await orchestrator.advance();
|
|
415
|
+
const result = await f.orchestrator.advance();
|
|
408
416
|
|
|
409
417
|
assert.equal(result.kind, "stopped");
|
|
410
|
-
|
|
418
|
+
if (result.kind !== "stopped") return;
|
|
419
|
+
assert.equal(result.reason, "All milestones complete");
|
|
420
|
+
assert.equal(result.terminalOutcome?.code, "all-complete");
|
|
421
|
+
assert.equal(f.orchestrator.getStatus().phase, "stopped");
|
|
411
422
|
});
|
|
412
423
|
|
|
413
|
-
test("advance()
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
},
|
|
427
|
-
dispatch: {
|
|
428
|
-
async decideNextUnit() { return null; },
|
|
429
|
-
},
|
|
424
|
+
test("advance() blocks all-complete stop when completed milestone is still unmerged in a worktree", async (t) => {
|
|
425
|
+
const f = makeFixture({ complete: true, noTask: true });
|
|
426
|
+
t.after(() => f.cleanup());
|
|
427
|
+
|
|
428
|
+
insertSlice({
|
|
429
|
+
id: "S01",
|
|
430
|
+
milestoneId: "M001",
|
|
431
|
+
title: "Slice",
|
|
432
|
+
status: "complete",
|
|
433
|
+
risk: "low",
|
|
434
|
+
depends: [],
|
|
435
|
+
demo: "",
|
|
436
|
+
sequence: 1,
|
|
430
437
|
});
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
test("advance() keeps running when dispatch intentionally skips a phase", async () => {
|
|
440
|
-
const { deps, calls } = makeDeps({
|
|
441
|
-
dispatch: {
|
|
442
|
-
async decideNextUnit() {
|
|
443
|
-
return { kind: "skipped", reason: "evaluating-gates skipped after marking gates omitted" };
|
|
444
|
-
},
|
|
445
|
-
},
|
|
438
|
+
insertAssessment({
|
|
439
|
+
path: "milestones/M001/M001-VALIDATION.md",
|
|
440
|
+
milestoneId: "M001",
|
|
441
|
+
status: "pass",
|
|
442
|
+
scope: "milestone-validation",
|
|
443
|
+
fullContent: "verdict: pass",
|
|
446
444
|
});
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
assert.equal(result.reason, "evaluating-gates skipped after marking gates omitted");
|
|
454
|
-
assert.equal(orchestrator.getStatus().phase, "running");
|
|
455
|
-
assert.ok(calls.includes("journal:advance-skipped"));
|
|
456
|
-
assert.ok(!calls.includes("journal:advance-stopped"));
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
test("advance() surfaces dispatch blocker reason instead of generic no remaining units", async () => {
|
|
460
|
-
const { deps, calls } = makeDeps({
|
|
461
|
-
dispatch: {
|
|
462
|
-
async decideNextUnit() {
|
|
463
|
-
return {
|
|
464
|
-
kind: "blocked",
|
|
465
|
-
reason: "Milestone M001 validation verdict is needs-remediation but all slices are complete.",
|
|
466
|
-
action: "pause",
|
|
467
|
-
};
|
|
468
|
-
},
|
|
469
|
-
},
|
|
445
|
+
insertGateRow({
|
|
446
|
+
milestoneId: "M001",
|
|
447
|
+
sliceId: "S01",
|
|
448
|
+
gateId: "Q3",
|
|
449
|
+
scope: "slice",
|
|
450
|
+
status: "pending",
|
|
470
451
|
});
|
|
471
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
472
452
|
|
|
473
|
-
const
|
|
453
|
+
const worktreePath = join(f.base, ".gsd", "worktrees", "M001");
|
|
454
|
+
mkdirSync(join(f.base, ".gsd", "worktrees"), { recursive: true });
|
|
455
|
+
execFileSync("git", ["worktree", "add", "-b", "milestone/M001", worktreePath], { cwd: f.base, stdio: "ignore" });
|
|
456
|
+
mkdirSync(join(worktreePath, ".gsd", "milestones", "M001"), { recursive: true });
|
|
457
|
+
writeFileSync(join(worktreePath, ".gsd", "milestones", "M001", "M001-SUMMARY.md"), "# Milestone Summary\n");
|
|
458
|
+
f.session.basePath = worktreePath;
|
|
459
|
+
f.session.originalBasePath = f.base;
|
|
460
|
+
f.session.currentMilestoneId = "M001";
|
|
461
|
+
f.session.milestoneMergedInPhases = false;
|
|
462
|
+
|
|
463
|
+
const result = await f.orchestrator.advance();
|
|
474
464
|
|
|
475
465
|
assert.equal(result.kind, "blocked");
|
|
476
466
|
if (result.kind !== "blocked") return;
|
|
477
|
-
assert.equal(result.reason, "Milestone M001 validation verdict is needs-remediation but all slices are complete.");
|
|
478
467
|
assert.equal(result.action, "pause");
|
|
479
|
-
assert.
|
|
480
|
-
assert.
|
|
468
|
+
assert.equal(result.terminalOutcome?.code, "settlement-blocked");
|
|
469
|
+
assert.match(result.reason, /worktree branch has not been merged to main/);
|
|
470
|
+
assert.doesNotMatch(result.reason, /quality gate Q3 is still pending/);
|
|
471
|
+
assert.equal(f.orchestrator.getStatus().phase, "paused");
|
|
472
|
+
assert.equal(f.session.milestoneSettlement?.ok, false);
|
|
473
|
+
const names = f.journalNames();
|
|
474
|
+
assert.ok(names.includes("advance-blocked"));
|
|
475
|
+
assert.ok(!names.includes("advance-stopped"));
|
|
481
476
|
});
|
|
482
477
|
|
|
483
|
-
test("
|
|
484
|
-
|
|
485
|
-
|
|
478
|
+
test("advance() stopped clears previous activeUnit and resets idempotent lock", async (t) => {
|
|
479
|
+
// First advance dispatches; then we make the milestone resolve to no unit by
|
|
480
|
+
// closing it on disk + DB and re-deriving. Simpler: drive a fixture that
|
|
481
|
+
// dispatches once, finalize externally, then the next decision is complete.
|
|
482
|
+
let dispatchOnce = true;
|
|
483
|
+
const f = makeFixture({
|
|
484
|
+
dispatch: () => {
|
|
485
|
+
if (dispatchOnce) {
|
|
486
|
+
dispatchOnce = false;
|
|
487
|
+
return DEFAULT_DISPATCH;
|
|
488
|
+
}
|
|
489
|
+
// After the first advance, signal completion via a benign skip → still
|
|
490
|
+
// exercises the running/active-unit transition. For the stopped path we
|
|
491
|
+
// rely on the complete-state test above.
|
|
492
|
+
return { action: "skip", matchedRule: "done" };
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
t.after(() => f.cleanup());
|
|
486
496
|
|
|
487
|
-
const
|
|
497
|
+
const first = await f.orchestrator.advance();
|
|
498
|
+
assert.equal(first.kind, "advanced");
|
|
488
499
|
|
|
489
|
-
|
|
490
|
-
assert.equal(
|
|
491
|
-
|
|
492
|
-
assert.
|
|
500
|
+
const second = await f.orchestrator.advance();
|
|
501
|
+
assert.equal(second.kind, "skipped");
|
|
502
|
+
// skip clears activeUnit
|
|
503
|
+
assert.equal(f.orchestrator.getStatus().activeUnit, undefined);
|
|
493
504
|
});
|
|
494
505
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
async classifyAndRecover() { return { action: "escalate", reason: "needs manual" }; },
|
|
503
|
-
},
|
|
504
|
-
});
|
|
505
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
506
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
507
|
+
// Idempotency + finalized guard + stuck-loop ring (issues #5786 / #5787 / #415)
|
|
508
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
509
|
+
|
|
510
|
+
test("advance() is idempotent for the same active unit", async (t) => {
|
|
511
|
+
const f = makeFixture();
|
|
512
|
+
t.after(() => f.cleanup());
|
|
506
513
|
|
|
507
|
-
const
|
|
514
|
+
const first = await f.orchestrator.advance();
|
|
515
|
+
const second = await f.orchestrator.advance();
|
|
508
516
|
|
|
509
|
-
assert.equal(
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
517
|
+
assert.equal(first.kind, "advanced");
|
|
518
|
+
if (first.kind === "advanced") {
|
|
519
|
+
assert.deepEqual(first.unit, { unitType: "execute-task", unitId: "M001/S01/T01" });
|
|
520
|
+
}
|
|
521
|
+
assert.equal(second.kind, "blocked");
|
|
522
|
+
if (second.kind !== "blocked") return;
|
|
523
|
+
assert.equal(second.reason, "idempotent advance: unit already active");
|
|
524
|
+
assert.equal(second.action, "pause");
|
|
513
525
|
});
|
|
514
526
|
|
|
515
|
-
test("
|
|
516
|
-
const
|
|
517
|
-
|
|
527
|
+
test("idempotency block fires with its own reason before saturation", async (t) => {
|
|
528
|
+
const f = makeFixture();
|
|
529
|
+
t.after(() => f.cleanup());
|
|
518
530
|
|
|
519
|
-
const first = await orchestrator.advance();
|
|
520
|
-
const second = await orchestrator.advance();
|
|
531
|
+
const first = await f.orchestrator.advance();
|
|
532
|
+
const second = await f.orchestrator.advance();
|
|
521
533
|
|
|
522
534
|
assert.equal(first.kind, "advanced");
|
|
523
|
-
assert.deepEqual(first.unit, { unitType: "execute-task", unitId: "T01" });
|
|
524
535
|
assert.equal(second.kind, "blocked");
|
|
536
|
+
if (second.kind !== "blocked") return;
|
|
525
537
|
assert.equal(second.reason, "idempotent advance: unit already active");
|
|
526
538
|
assert.equal(second.action, "pause");
|
|
527
|
-
|
|
528
|
-
const prepareCalls = calls.filter((c) => c === "worktree.prepare").length;
|
|
529
|
-
assert.equal(prepareCalls, 1);
|
|
530
539
|
});
|
|
531
540
|
|
|
532
|
-
test("completeActiveUnit clears in-flight idempotency and stops stale same-unit advance", async () => {
|
|
533
|
-
const
|
|
534
|
-
|
|
541
|
+
test("completeActiveUnit clears in-flight idempotency and stops stale same-unit advance", async (t) => {
|
|
542
|
+
const f = makeFixture();
|
|
543
|
+
t.after(() => f.cleanup());
|
|
535
544
|
|
|
536
|
-
const first = await orchestrator.advance();
|
|
545
|
+
const first = await f.orchestrator.advance();
|
|
537
546
|
assert.equal(first.kind, "advanced");
|
|
538
547
|
if (first.kind !== "advanced") throw new Error("expected first advance");
|
|
539
548
|
|
|
540
|
-
await orchestrator.completeActiveUnit(first.unit);
|
|
541
|
-
const second = await orchestrator.advance();
|
|
549
|
+
await f.orchestrator.completeActiveUnit(first.unit);
|
|
550
|
+
const second = await f.orchestrator.advance();
|
|
542
551
|
|
|
543
|
-
assert.equal(orchestrator.getStatus().activeUnit, undefined);
|
|
552
|
+
assert.equal(f.orchestrator.getStatus().activeUnit, undefined);
|
|
544
553
|
assert.equal(second.kind, "blocked");
|
|
545
554
|
if (second.kind !== "blocked") throw new Error("expected stale same-unit block");
|
|
546
555
|
assert.equal(second.action, "stop");
|
|
547
|
-
assert.equal(second.reason, "state did not advance after finalized execute-task T01");
|
|
548
|
-
assert.ok(
|
|
549
|
-
const prepareCalls = calls.filter((c) => c === "worktree.prepare").length;
|
|
550
|
-
assert.equal(prepareCalls, 1, "stale same-unit advance must not prepare or redispatch");
|
|
556
|
+
assert.equal(second.reason, "state did not advance after finalized execute-task M001/S01/T01");
|
|
557
|
+
assert.ok(f.journalNames().includes("unit-finalized"));
|
|
551
558
|
});
|
|
552
559
|
|
|
553
|
-
test("
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
560
|
+
test("#442: finalized-repeat recovers (skipped) when the unit's artifact already exists on disk", async (t) => {
|
|
561
|
+
// plan-milestone's expected artifact is the ROADMAP, which the fixture
|
|
562
|
+
// already writes — so verifyExpectedArtifact returns true. This is the legacy
|
|
563
|
+
// stuck-recovery scenario (unit completed on disk, DB row stale): instead of
|
|
564
|
+
// the finalized-repeat HARD-STOP, #442 verify-and-recover should refresh +
|
|
565
|
+
// skip so the loop can progress. plan-milestone is deliberately NOT one of
|
|
566
|
+
// the DB-refreshing unit types, so the recovery stays side-effect-light.
|
|
567
|
+
const f = makeFixture({
|
|
568
|
+
dispatch: () => ({ action: "dispatch", unitType: "plan-milestone", unitId: "M001", prompt: "p" }),
|
|
569
|
+
});
|
|
570
|
+
t.after(() => f.cleanup());
|
|
571
|
+
|
|
572
|
+
const first = await f.orchestrator.advance();
|
|
573
|
+
if (first.kind !== "advanced") {
|
|
574
|
+
throw new Error(`expected advanced, got ${first.kind}: ${(first as { reason?: string }).reason ?? ""}`);
|
|
575
|
+
}
|
|
576
|
+
await f.orchestrator.completeActiveUnit(first.unit);
|
|
577
|
+
|
|
578
|
+
const second = await f.orchestrator.advance();
|
|
579
|
+
assert.equal(second.kind, "skipped", "should recover via artifact verification, not hard-stop");
|
|
580
|
+
if (second.kind !== "skipped") throw new Error("expected skipped recovery");
|
|
581
|
+
assert.match(second.reason, /stuck-recovery/);
|
|
582
|
+
assert.ok(f.journalNames().includes("advance-skipped"));
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test("completeActiveUnit allows a different next unit to advance", async (t) => {
|
|
586
|
+
let nextTaskId = "M001/S01/T01";
|
|
587
|
+
const f = makeFixture({
|
|
588
|
+
dispatch: () => ({ action: "dispatch", unitType: "execute-task", unitId: nextTaskId, prompt: "p" }),
|
|
561
589
|
});
|
|
562
|
-
|
|
590
|
+
t.after(() => f.cleanup());
|
|
563
591
|
|
|
564
|
-
const first = await orchestrator.advance();
|
|
592
|
+
const first = await f.orchestrator.advance();
|
|
565
593
|
assert.equal(first.kind, "advanced");
|
|
566
594
|
if (first.kind !== "advanced") throw new Error("expected first advance");
|
|
567
595
|
|
|
568
|
-
await orchestrator.completeActiveUnit(first.unit);
|
|
569
|
-
nextTaskId = "T02";
|
|
570
|
-
const second = await orchestrator.advance();
|
|
596
|
+
await f.orchestrator.completeActiveUnit(first.unit);
|
|
597
|
+
nextTaskId = "M001/S01/T02";
|
|
598
|
+
const second = await f.orchestrator.advance();
|
|
571
599
|
|
|
572
600
|
assert.equal(second.kind, "advanced");
|
|
573
601
|
if (second.kind !== "advanced") throw new Error("expected second advance");
|
|
574
|
-
assert.deepEqual(second.unit, { unitType: "execute-task", unitId: "T02" });
|
|
602
|
+
assert.deepEqual(second.unit, { unitType: "execute-task", unitId: "M001/S01/T02" });
|
|
575
603
|
});
|
|
576
604
|
|
|
577
|
-
test("completeActiveUnit guard survives an intervening advance and blocks X→Y→X re-dispatch", async () => {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const { deps } = makeDeps({
|
|
582
|
-
dispatch: {
|
|
583
|
-
async decideNextUnit() {
|
|
584
|
-
return { unitType: "execute-task", unitId: nextTaskId, reason: "ready", preconditions: [] };
|
|
585
|
-
},
|
|
586
|
-
},
|
|
605
|
+
test("completeActiveUnit guard survives an intervening advance and blocks X→Y→X re-dispatch (#415)", async (t) => {
|
|
606
|
+
let nextTaskId = "M001/S01/T01";
|
|
607
|
+
const f = makeFixture({
|
|
608
|
+
dispatch: () => ({ action: "dispatch", unitType: "execute-task", unitId: nextTaskId, prompt: "p" }),
|
|
587
609
|
});
|
|
588
|
-
|
|
610
|
+
t.after(() => f.cleanup());
|
|
589
611
|
|
|
590
|
-
|
|
591
|
-
const first = await orchestrator.advance();
|
|
612
|
+
const first = await f.orchestrator.advance();
|
|
592
613
|
assert.equal(first.kind, "advanced");
|
|
593
614
|
if (first.kind !== "advanced") throw new Error("expected first advance");
|
|
594
615
|
|
|
595
|
-
|
|
596
|
-
await orchestrator.completeActiveUnit(first.unit);
|
|
616
|
+
await f.orchestrator.completeActiveUnit(first.unit);
|
|
597
617
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const second = await orchestrator.advance();
|
|
618
|
+
nextTaskId = "M001/S01/T02";
|
|
619
|
+
const second = await f.orchestrator.advance();
|
|
601
620
|
assert.equal(second.kind, "advanced");
|
|
602
621
|
if (second.kind !== "advanced") throw new Error("expected second advance (T02)");
|
|
603
|
-
assert.deepEqual(second.unit, { unitType: "execute-task", unitId: "T02" });
|
|
622
|
+
assert.deepEqual(second.unit, { unitType: "execute-task", unitId: "M001/S01/T02" });
|
|
604
623
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const third = await orchestrator.advance();
|
|
624
|
+
nextTaskId = "M001/S01/T01";
|
|
625
|
+
const third = await f.orchestrator.advance();
|
|
608
626
|
assert.equal(third.kind, "blocked");
|
|
609
627
|
if (third.kind !== "blocked") throw new Error("expected X→Y→X re-dispatch to be blocked");
|
|
610
628
|
assert.equal(third.action, "stop");
|
|
611
|
-
assert.equal(third.reason, "state did not advance after finalized execute-task T01");
|
|
629
|
+
assert.equal(third.reason, "state did not advance after finalized execute-task M001/S01/T01");
|
|
612
630
|
});
|
|
613
631
|
|
|
614
|
-
test("retryActiveUnit clears in-flight idempotency without marking the unit finalized", async () => {
|
|
615
|
-
const
|
|
616
|
-
|
|
632
|
+
test("retryActiveUnit clears in-flight idempotency without marking the unit finalized", async (t) => {
|
|
633
|
+
const f = makeFixture();
|
|
634
|
+
t.after(() => f.cleanup());
|
|
617
635
|
|
|
618
|
-
const first = await orchestrator.advance();
|
|
636
|
+
const first = await f.orchestrator.advance();
|
|
619
637
|
assert.equal(first.kind, "advanced");
|
|
620
638
|
if (first.kind !== "advanced") throw new Error("expected first advance");
|
|
621
639
|
|
|
622
|
-
await orchestrator.retryActiveUnit(first.unit);
|
|
623
|
-
const second = await orchestrator.advance();
|
|
640
|
+
await f.orchestrator.retryActiveUnit(first.unit);
|
|
641
|
+
const second = await f.orchestrator.advance();
|
|
624
642
|
|
|
625
643
|
assert.equal(second.kind, "advanced");
|
|
626
644
|
if (second.kind !== "advanced") throw new Error("expected retry advance");
|
|
627
645
|
assert.deepEqual(second.unit, first.unit);
|
|
628
|
-
assert.ok(
|
|
629
|
-
const prepareCalls = calls.filter((c) => c === "worktree.prepare").length;
|
|
630
|
-
assert.equal(prepareCalls, 2, "retry should intentionally redispatch the same unit");
|
|
646
|
+
assert.ok(f.journalNames().includes("unit-retry"));
|
|
631
647
|
});
|
|
632
648
|
|
|
633
|
-
test("retryActiveUnit clears finalized same-unit guard for post-hook retries", async () => {
|
|
634
|
-
const
|
|
635
|
-
|
|
649
|
+
test("retryActiveUnit clears finalized same-unit guard for post-hook retries", async (t) => {
|
|
650
|
+
const f = makeFixture();
|
|
651
|
+
t.after(() => f.cleanup());
|
|
636
652
|
|
|
637
|
-
const first = await orchestrator.advance();
|
|
653
|
+
const first = await f.orchestrator.advance();
|
|
638
654
|
assert.equal(first.kind, "advanced");
|
|
639
655
|
if (first.kind !== "advanced") throw new Error("expected first advance");
|
|
640
656
|
|
|
641
|
-
await orchestrator.completeActiveUnit(first.unit);
|
|
642
|
-
await orchestrator.retryActiveUnit(first.unit);
|
|
643
|
-
const second = await orchestrator.advance();
|
|
657
|
+
await f.orchestrator.completeActiveUnit(first.unit);
|
|
658
|
+
await f.orchestrator.retryActiveUnit(first.unit);
|
|
659
|
+
const second = await f.orchestrator.advance();
|
|
644
660
|
|
|
645
661
|
assert.equal(second.kind, "advanced");
|
|
646
662
|
if (second.kind !== "advanced") throw new Error("expected retry advance");
|
|
647
663
|
assert.deepEqual(second.unit, first.unit);
|
|
648
|
-
|
|
649
|
-
assert.ok(
|
|
650
|
-
|
|
651
|
-
assert.equal(prepareCalls, 2, "post-hook retry should redispatch the finalized unit");
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
test("resume() re-enters running phase", async () => {
|
|
655
|
-
const { deps } = makeDeps();
|
|
656
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
657
|
-
|
|
658
|
-
const result = await orchestrator.resume();
|
|
659
|
-
|
|
660
|
-
assert.equal(result.kind, "resumed");
|
|
661
|
-
assert.equal(orchestrator.getStatus().phase, "running");
|
|
664
|
+
const names = f.journalNames();
|
|
665
|
+
assert.ok(names.includes("unit-finalized"));
|
|
666
|
+
assert.ok(names.includes("unit-retry"));
|
|
662
667
|
});
|
|
663
668
|
|
|
664
|
-
test("resume() clears idempotent lock and allows re-advance", async () => {
|
|
665
|
-
const
|
|
666
|
-
|
|
669
|
+
test("resume() clears idempotent lock and allows re-advance", async (t) => {
|
|
670
|
+
const f = makeFixture();
|
|
671
|
+
t.after(() => f.cleanup());
|
|
667
672
|
|
|
668
|
-
const first = await orchestrator.advance();
|
|
669
|
-
const blocked = await orchestrator.advance();
|
|
670
|
-
const resumed = await orchestrator.resume();
|
|
671
|
-
const next = await orchestrator.advance();
|
|
673
|
+
const first = await f.orchestrator.advance();
|
|
674
|
+
const blocked = await f.orchestrator.advance();
|
|
675
|
+
const resumed = await f.orchestrator.resume();
|
|
676
|
+
const next = await f.orchestrator.advance();
|
|
672
677
|
|
|
673
678
|
assert.equal(first.kind, "advanced");
|
|
674
679
|
assert.equal(blocked.kind, "blocked");
|
|
@@ -676,263 +681,81 @@ test("resume() clears idempotent lock and allows re-advance", async () => {
|
|
|
676
681
|
assert.equal(next.kind, "advanced");
|
|
677
682
|
});
|
|
678
683
|
|
|
679
|
-
test("
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const before = orchestrator.getStatus().transitionCount;
|
|
684
|
-
await orchestrator.start({ basePath: "/tmp/project", trigger: "manual" });
|
|
685
|
-
const afterStart = orchestrator.getStatus().transitionCount;
|
|
686
|
-
await orchestrator.stop("done");
|
|
687
|
-
const afterStop = orchestrator.getStatus().transitionCount;
|
|
688
|
-
|
|
689
|
-
assert.ok(afterStart > before);
|
|
690
|
-
assert.ok(afterStop > afterStart);
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
test("stop() clears idempotent unit lock so advance can run again", async () => {
|
|
694
|
-
const { deps } = makeDeps();
|
|
695
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
684
|
+
test("start() clears prior idempotent lock", async (t) => {
|
|
685
|
+
const f = makeFixture();
|
|
686
|
+
t.after(() => f.cleanup());
|
|
696
687
|
|
|
697
|
-
|
|
698
|
-
const blocked = await orchestrator.advance();
|
|
699
|
-
const
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
assert.equal(first.kind, "advanced");
|
|
703
|
-
assert.equal(blocked.kind, "blocked");
|
|
704
|
-
assert.equal(stopped.kind, "stopped");
|
|
705
|
-
assert.equal(second.kind, "advanced");
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
test("advance() stopped clears previous activeUnit", async () => {
|
|
709
|
-
let first = true;
|
|
710
|
-
const { deps } = makeDeps({
|
|
711
|
-
dispatch: {
|
|
712
|
-
async decideNextUnit() {
|
|
713
|
-
if (first) {
|
|
714
|
-
first = false;
|
|
715
|
-
return { unitType: "execute-task", unitId: "T01", reason: "ready", preconditions: [] };
|
|
716
|
-
}
|
|
717
|
-
return null;
|
|
718
|
-
},
|
|
719
|
-
},
|
|
720
|
-
});
|
|
721
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
722
|
-
|
|
723
|
-
await orchestrator.advance();
|
|
724
|
-
const stopped = await orchestrator.advance();
|
|
725
|
-
|
|
726
|
-
assert.equal(stopped.kind, "stopped");
|
|
727
|
-
assert.equal(orchestrator.getStatus().activeUnit, undefined);
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
test("recovery stop clears activeUnit", async () => {
|
|
731
|
-
const { deps, calls } = makeDeps({
|
|
732
|
-
runtime: {
|
|
733
|
-
async ensureLockOwnership() { throw new Error("boom"); },
|
|
734
|
-
async journalTransition(event) { calls.push(`journal:${event.name}`); },
|
|
735
|
-
},
|
|
736
|
-
recovery: {
|
|
737
|
-
async classifyAndRecover() { return { action: "stop", reason: "fatal" }; },
|
|
738
|
-
},
|
|
739
|
-
});
|
|
740
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
741
|
-
|
|
742
|
-
const result = await orchestrator.advance();
|
|
743
|
-
|
|
744
|
-
assert.equal(result.kind, "stopped");
|
|
745
|
-
assert.equal(orchestrator.getStatus().activeUnit, undefined);
|
|
746
|
-
assert.ok(calls.includes("journal:advance-stopped"));
|
|
747
|
-
assert.ok(calls.includes("notify:stopped"));
|
|
748
|
-
assert.ok(!calls.includes("notify:error"));
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
test("recovery retry maps to paused result", async () => {
|
|
752
|
-
const { deps, calls } = makeDeps({
|
|
753
|
-
runtime: {
|
|
754
|
-
async ensureLockOwnership() { throw new Error("boom"); },
|
|
755
|
-
async journalTransition(event) { calls.push(`journal:${event.name}`); },
|
|
756
|
-
},
|
|
757
|
-
recovery: {
|
|
758
|
-
async classifyAndRecover() { return { action: "retry", reason: "transient" }; },
|
|
759
|
-
},
|
|
760
|
-
});
|
|
761
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
762
|
-
|
|
763
|
-
const result = await orchestrator.advance();
|
|
764
|
-
|
|
765
|
-
assert.equal(result.kind, "paused");
|
|
766
|
-
assert.equal(result.reason, "transient");
|
|
767
|
-
assert.equal(orchestrator.getStatus().phase, "paused");
|
|
768
|
-
assert.ok(calls.includes("journal:advance-paused"));
|
|
769
|
-
assert.ok(calls.includes("notify:pause"));
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
test("getStatus() returns defensive copy of activeUnit", async () => {
|
|
773
|
-
const { deps } = makeDeps();
|
|
774
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
775
|
-
|
|
776
|
-
await orchestrator.advance();
|
|
777
|
-
const snap1 = orchestrator.getStatus();
|
|
778
|
-
if (snap1.activeUnit) snap1.activeUnit.unitId = "MUTATED";
|
|
779
|
-
const snap2 = orchestrator.getStatus();
|
|
780
|
-
|
|
781
|
-
assert.equal(snap2.activeUnit?.unitId, "T01");
|
|
782
|
-
});
|
|
783
|
-
|
|
784
|
-
test("start() clears prior idempotent lock", async () => {
|
|
785
|
-
const { deps } = makeDeps();
|
|
786
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
787
|
-
|
|
788
|
-
await orchestrator.advance();
|
|
789
|
-
const blocked = await orchestrator.advance();
|
|
790
|
-
const restarted = await orchestrator.start({ basePath: "/tmp/project", trigger: "manual" });
|
|
791
|
-
const next = await orchestrator.advance();
|
|
688
|
+
await f.orchestrator.advance();
|
|
689
|
+
const blocked = await f.orchestrator.advance();
|
|
690
|
+
const restarted = await f.orchestrator.start(SESSION_CONTEXT);
|
|
691
|
+
const next = await f.orchestrator.advance();
|
|
792
692
|
|
|
793
693
|
assert.equal(blocked.kind, "blocked");
|
|
794
694
|
assert.equal(restarted.kind, "started");
|
|
795
695
|
assert.equal(next.kind, "advanced");
|
|
796
696
|
});
|
|
797
697
|
|
|
798
|
-
test("
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
async ensureLockOwnership() { throw new Error("boom"); },
|
|
802
|
-
async journalTransition(event) { calls.push(`journal:${event.name}`); },
|
|
803
|
-
},
|
|
804
|
-
recovery: {
|
|
805
|
-
async classifyAndRecover() { return { action: "escalate", reason: "needs manual" }; },
|
|
806
|
-
},
|
|
807
|
-
});
|
|
808
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
809
|
-
|
|
810
|
-
await orchestrator.advance();
|
|
811
|
-
|
|
812
|
-
assert.ok(calls.includes("notify:error"));
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
test("blocked path journals advance-blocked", async () => {
|
|
816
|
-
const { deps, calls } = makeDeps();
|
|
817
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
698
|
+
test("stop() clears idempotent unit lock so advance can run again", async (t) => {
|
|
699
|
+
const f = makeFixture();
|
|
700
|
+
t.after(() => f.cleanup());
|
|
818
701
|
|
|
819
|
-
await orchestrator.advance();
|
|
820
|
-
await orchestrator.advance();
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
test("health post hook runs on blocked result", async () => {
|
|
826
|
-
const { deps, calls } = makeDeps();
|
|
827
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
828
|
-
|
|
829
|
-
await orchestrator.advance();
|
|
830
|
-
await orchestrator.advance();
|
|
831
|
-
|
|
832
|
-
assert.ok(calls.includes("health.post"));
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
test("start() emits start notification", async () => {
|
|
836
|
-
const { deps, calls } = makeDeps();
|
|
837
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
838
|
-
|
|
839
|
-
await orchestrator.start({ basePath: "/tmp/project", trigger: "manual" });
|
|
840
|
-
|
|
841
|
-
assert.ok(calls.includes("notify:start"));
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
test("resume() emits resume notification", async () => {
|
|
845
|
-
const { deps, calls } = makeDeps();
|
|
846
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
847
|
-
|
|
848
|
-
await orchestrator.resume();
|
|
849
|
-
|
|
850
|
-
assert.ok(calls.includes("notify:resume"));
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
test("stopped with no remaining units clears idempotent lock for next advance", async () => {
|
|
854
|
-
let callCount = 0;
|
|
855
|
-
const { deps } = makeDeps({
|
|
856
|
-
dispatch: {
|
|
857
|
-
async decideNextUnit() {
|
|
858
|
-
callCount += 1;
|
|
859
|
-
if (callCount === 2) return null;
|
|
860
|
-
return { unitType: "execute-task", unitId: "T01", reason: "ready", preconditions: [] };
|
|
861
|
-
},
|
|
862
|
-
},
|
|
863
|
-
});
|
|
864
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
865
|
-
|
|
866
|
-
const first = await orchestrator.advance();
|
|
867
|
-
const stopped = await orchestrator.advance();
|
|
868
|
-
const after = await orchestrator.advance();
|
|
702
|
+
const first = await f.orchestrator.advance();
|
|
703
|
+
const blocked = await f.orchestrator.advance();
|
|
704
|
+
const stopped = await f.orchestrator.stop("reset");
|
|
705
|
+
const second = await f.orchestrator.advance();
|
|
869
706
|
|
|
870
707
|
assert.equal(first.kind, "advanced");
|
|
708
|
+
assert.equal(blocked.kind, "blocked");
|
|
871
709
|
assert.equal(stopped.kind, "stopped");
|
|
872
|
-
assert.equal(
|
|
710
|
+
assert.equal(second.kind, "advanced");
|
|
873
711
|
});
|
|
874
712
|
|
|
875
|
-
test("
|
|
876
|
-
const
|
|
877
|
-
|
|
713
|
+
test("blocked path journals advance-blocked and records a health snapshot", async (t) => {
|
|
714
|
+
const f = makeFixture();
|
|
715
|
+
t.after(() => f.cleanup());
|
|
878
716
|
|
|
879
|
-
|
|
717
|
+
await f.orchestrator.advance();
|
|
718
|
+
await f.orchestrator.advance();
|
|
880
719
|
|
|
881
|
-
assert.
|
|
882
|
-
assert.equal(orchestrator.getStatus().phase, "stopped");
|
|
883
|
-
assert.ok(calls.includes("worktree.cleanup"));
|
|
884
|
-
assert.ok(calls.includes("journal:stop"));
|
|
885
|
-
assert.ok(calls.includes("notify:stop"));
|
|
720
|
+
assert.ok(f.journalNames().includes("advance-blocked"));
|
|
886
721
|
});
|
|
887
722
|
|
|
888
|
-
//
|
|
889
|
-
// Stuck-loop ring buffer (issue #5787)
|
|
890
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
723
|
+
// ─── Stuck-loop ring buffer (issue #5787) ──────────────────────────────────
|
|
891
724
|
|
|
892
725
|
test("STUCK_WINDOW_SIZE matches the legacy auto/phases.ts constant", () => {
|
|
893
726
|
assert.equal(STUCK_WINDOW_SIZE, 6);
|
|
894
727
|
});
|
|
895
728
|
|
|
896
|
-
test("stuck-loop: empty ring on a freshly constructed orchestrator advances normally", async () => {
|
|
897
|
-
const
|
|
898
|
-
|
|
729
|
+
test("stuck-loop: empty ring on a freshly constructed orchestrator advances normally", async (t) => {
|
|
730
|
+
const f = makeFixture();
|
|
731
|
+
t.after(() => f.cleanup());
|
|
899
732
|
|
|
900
|
-
const result = await orchestrator.advance();
|
|
733
|
+
const result = await f.orchestrator.advance();
|
|
901
734
|
|
|
902
735
|
assert.equal(result.kind, "advanced");
|
|
903
736
|
});
|
|
904
737
|
|
|
905
|
-
test("stuck-loop: partial fill of mixed units does not block", async () => {
|
|
906
|
-
// Alternate A/B for STUCK_WINDOW_SIZE rounds. No single key saturates the
|
|
907
|
-
// window, so neither idempotency nor stuck-loop should fire.
|
|
738
|
+
test("stuck-loop: partial fill of mixed units does not block", async (t) => {
|
|
908
739
|
let i = 0;
|
|
909
|
-
const sequence = ["A", "B", "A", "B", "A", "B"];
|
|
910
|
-
const
|
|
911
|
-
dispatch: {
|
|
912
|
-
async decideNextUnit() {
|
|
913
|
-
const id = sequence[i++ % sequence.length];
|
|
914
|
-
return { unitType: "execute-task", unitId: id, reason: "ready", preconditions: [] };
|
|
915
|
-
},
|
|
916
|
-
},
|
|
740
|
+
const sequence = ["M001/S01/A", "M001/S01/B", "M001/S01/A", "M001/S01/B", "M001/S01/A", "M001/S01/B"];
|
|
741
|
+
const f = makeFixture({
|
|
742
|
+
dispatch: () => ({ action: "dispatch", unitType: "execute-task", unitId: sequence[i++ % sequence.length], prompt: "p" }),
|
|
917
743
|
});
|
|
918
|
-
|
|
744
|
+
t.after(() => f.cleanup());
|
|
919
745
|
|
|
920
746
|
for (let round = 0; round < STUCK_WINDOW_SIZE; round++) {
|
|
921
|
-
const result = await orchestrator.advance();
|
|
747
|
+
const result = await f.orchestrator.advance();
|
|
922
748
|
assert.equal(result.kind, "advanced", `round ${round} should advance, got ${result.kind}`);
|
|
923
749
|
}
|
|
924
750
|
});
|
|
925
751
|
|
|
926
|
-
test("stuck-loop: ring saturated with same unit blocks with action 'stop' and stuck-loop reason", async () => {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
// The STUCK_WINDOW_SIZE'th call sees a saturated ring and returns stuck-loop.
|
|
930
|
-
const { deps } = makeDeps();
|
|
931
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
752
|
+
test("stuck-loop: ring saturated with same unit blocks with action 'stop' and stuck-loop reason", async (t) => {
|
|
753
|
+
const f = makeFixture();
|
|
754
|
+
t.after(() => f.cleanup());
|
|
932
755
|
|
|
933
|
-
const results: Awaited<ReturnType<typeof orchestrator.advance>>[] = [];
|
|
756
|
+
const results: Awaited<ReturnType<typeof f.orchestrator.advance>>[] = [];
|
|
934
757
|
for (let i = 0; i < STUCK_WINDOW_SIZE; i++) {
|
|
935
|
-
results.push(await orchestrator.advance());
|
|
758
|
+
results.push(await f.orchestrator.advance());
|
|
936
759
|
}
|
|
937
760
|
|
|
938
761
|
// First call advances.
|
|
@@ -952,88 +775,140 @@ test("stuck-loop: ring saturated with same unit blocks with action 'stop' and st
|
|
|
952
775
|
assert.equal(last.kind, "blocked");
|
|
953
776
|
if (last.kind !== "blocked") return;
|
|
954
777
|
assert.equal(last.action, "stop");
|
|
955
|
-
assert.equal(last.reason, `stuck-loop: execute-task:T01 picked ${STUCK_WINDOW_SIZE} times`);
|
|
778
|
+
assert.equal(last.reason, `stuck-loop: execute-task:M001/S01/T01 picked ${STUCK_WINDOW_SIZE} times`);
|
|
956
779
|
});
|
|
957
780
|
|
|
958
|
-
test("stuck-loop:
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
const { deps } = makeDeps();
|
|
962
|
-
const orchestrator = createAutoOrchestrator(deps);
|
|
781
|
+
test("stuck-loop: start() resets the ring so a fresh saturation cycle is required", async (t) => {
|
|
782
|
+
const f = makeFixture();
|
|
783
|
+
t.after(() => f.cleanup());
|
|
963
784
|
|
|
964
|
-
|
|
965
|
-
|
|
785
|
+
for (let i = 0; i < STUCK_WINDOW_SIZE - 1; i++) {
|
|
786
|
+
await f.orchestrator.advance();
|
|
787
|
+
}
|
|
966
788
|
|
|
967
|
-
|
|
968
|
-
assert.equal(
|
|
969
|
-
|
|
970
|
-
|
|
789
|
+
const restarted = await f.orchestrator.start(SESSION_CONTEXT);
|
|
790
|
+
assert.equal(restarted.kind, "started");
|
|
791
|
+
|
|
792
|
+
const next = await f.orchestrator.advance();
|
|
793
|
+
assert.equal(next.kind, "advanced");
|
|
971
794
|
});
|
|
972
795
|
|
|
973
|
-
test("stuck-loop:
|
|
974
|
-
//
|
|
975
|
-
//
|
|
976
|
-
|
|
977
|
-
const
|
|
796
|
+
test("stuck-loop: resume() preserves ring so detection accumulates across pause/resume", async (t) => {
|
|
797
|
+
// Regression for #572: resume() must NOT reset dispatchKeyWindow. Before the
|
|
798
|
+
// fix, a pause/resume cycle cleared the window, letting a stuck loop silently
|
|
799
|
+
// re-accumulate STUCK_WINDOW_SIZE dispatches before being detected again.
|
|
800
|
+
const f = makeFixture();
|
|
801
|
+
t.after(() => f.cleanup());
|
|
978
802
|
|
|
979
803
|
for (let i = 0; i < STUCK_WINDOW_SIZE - 1; i++) {
|
|
980
|
-
await orchestrator.advance();
|
|
804
|
+
await f.orchestrator.advance();
|
|
981
805
|
}
|
|
982
806
|
|
|
983
|
-
const
|
|
984
|
-
assert.equal(
|
|
807
|
+
const resumed = await f.orchestrator.resume();
|
|
808
|
+
assert.equal(resumed.kind, "resumed");
|
|
985
809
|
|
|
986
|
-
//
|
|
987
|
-
//
|
|
988
|
-
const next = await orchestrator.advance();
|
|
989
|
-
assert.equal(next.kind, "
|
|
810
|
+
// The ring is preserved, so the next advance pushes it to STUCK_WINDOW_SIZE
|
|
811
|
+
// and triggers stuck-loop detection — not a fresh dispatch.
|
|
812
|
+
const next = await f.orchestrator.advance();
|
|
813
|
+
assert.equal(next.kind, "blocked");
|
|
814
|
+
if (next.kind !== "blocked") return;
|
|
815
|
+
assert.equal(next.action, "stop");
|
|
816
|
+
assert.ok(next.reason.startsWith("stuck-loop:"), `expected stuck-loop reason, got: ${next.reason}`);
|
|
990
817
|
});
|
|
991
818
|
|
|
992
|
-
test("stuck-loop:
|
|
993
|
-
|
|
994
|
-
|
|
819
|
+
test("stuck-loop: stop('pause') preserves ring across the stop/resume cycle", async (t) => {
|
|
820
|
+
// Regression for #572: stop("pause") must behave the same as resume() —
|
|
821
|
+
// the window must survive so detection accumulates across pause/resume pairs.
|
|
822
|
+
const f = makeFixture();
|
|
823
|
+
t.after(() => f.cleanup());
|
|
995
824
|
|
|
996
825
|
for (let i = 0; i < STUCK_WINDOW_SIZE - 1; i++) {
|
|
997
|
-
await orchestrator.advance();
|
|
826
|
+
await f.orchestrator.advance();
|
|
998
827
|
}
|
|
999
828
|
|
|
1000
|
-
const
|
|
829
|
+
const stopped = await f.orchestrator.stop("pause");
|
|
830
|
+
assert.equal(stopped.kind, "stopped");
|
|
831
|
+
|
|
832
|
+
const resumed = await f.orchestrator.resume();
|
|
1001
833
|
assert.equal(resumed.kind, "resumed");
|
|
1002
834
|
|
|
1003
|
-
const next = await orchestrator.advance();
|
|
1004
|
-
assert.equal(next.kind, "
|
|
835
|
+
const next = await f.orchestrator.advance();
|
|
836
|
+
assert.equal(next.kind, "blocked");
|
|
837
|
+
if (next.kind !== "blocked") return;
|
|
838
|
+
assert.equal(next.action, "stop");
|
|
839
|
+
assert.ok(next.reason.startsWith("stuck-loop:"), `expected stuck-loop reason, got: ${next.reason}`);
|
|
1005
840
|
});
|
|
1006
841
|
|
|
1007
|
-
test("stuck-loop: stop() resets the ring", async () => {
|
|
1008
|
-
const
|
|
1009
|
-
|
|
842
|
+
test("stuck-loop: stop('user-request') resets the ring (hard stop)", async (t) => {
|
|
843
|
+
const f = makeFixture();
|
|
844
|
+
t.after(() => f.cleanup());
|
|
1010
845
|
|
|
1011
846
|
for (let i = 0; i < STUCK_WINDOW_SIZE - 1; i++) {
|
|
1012
|
-
await orchestrator.advance();
|
|
847
|
+
await f.orchestrator.advance();
|
|
1013
848
|
}
|
|
1014
849
|
|
|
1015
|
-
const stopped = await orchestrator.stop("user-request");
|
|
850
|
+
const stopped = await f.orchestrator.stop("user-request");
|
|
1016
851
|
assert.equal(stopped.kind, "stopped");
|
|
1017
852
|
|
|
1018
|
-
//
|
|
1019
|
-
const next = await orchestrator.advance();
|
|
853
|
+
// Hard stop clears the ring, so the next advance dispatches fresh.
|
|
854
|
+
const next = await f.orchestrator.advance();
|
|
1020
855
|
assert.equal(next.kind, "advanced");
|
|
1021
856
|
});
|
|
1022
857
|
|
|
1023
|
-
test("stuck-loop: journal records the stuck-loop reason on advance-blocked", async () => {
|
|
1024
|
-
const
|
|
1025
|
-
|
|
858
|
+
test("stuck-loop: journal records the stuck-loop reason on advance-blocked", async (t) => {
|
|
859
|
+
const f = makeFixture();
|
|
860
|
+
t.after(() => f.cleanup());
|
|
1026
861
|
|
|
1027
862
|
for (let i = 0; i < STUCK_WINDOW_SIZE; i++) {
|
|
1028
|
-
await orchestrator.advance();
|
|
863
|
+
await f.orchestrator.advance();
|
|
1029
864
|
}
|
|
1030
865
|
|
|
1031
|
-
|
|
866
|
+
const stuckEntry = queryJournal(f.base).find(
|
|
867
|
+
(e) => {
|
|
868
|
+
const reason = (e.data as Record<string, unknown> | undefined)?.reason;
|
|
869
|
+
return typeof reason === "string" && reason.startsWith("stuck-loop:");
|
|
870
|
+
},
|
|
871
|
+
);
|
|
872
|
+
assert.ok(stuckEntry, "journal must record an advance-blocked entry with the stuck-loop reason");
|
|
873
|
+
assert.ok(f.journalNames().includes("advance-blocked"));
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
877
|
+
// Recovery path: a lock held by another process throws inside advance() and is
|
|
878
|
+
// routed through the REAL classifyFailure → result mapping + notifications.
|
|
879
|
+
// We force the throw by acquiring the lock under a different PID (writing a
|
|
880
|
+
// foreign-PID lockfile is not portable, so we drive the deterministic-stop
|
|
881
|
+
// classification via a fixture whose runtimeBasePath has no valid lock).
|
|
882
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
883
|
+
|
|
884
|
+
test("advance() routes a lost-lock error through recovery and journals an outcome", async (t) => {
|
|
885
|
+
const f = makeFixture();
|
|
886
|
+
t.after(() => f.cleanup());
|
|
887
|
+
|
|
888
|
+
// Release the lock so ensureLockOwnership() sees missing-metadata and throws,
|
|
889
|
+
// exercising the catch → classifyAndRecover → result-mapping branch.
|
|
890
|
+
releaseSessionLock(f.base);
|
|
891
|
+
// Remove the lockfile artifact so getSessionLockStatus returns !valid.
|
|
892
|
+
try { rmSync(join(f.base, ".gsd", "auto.lock"), { force: true }); } catch { /* */ }
|
|
893
|
+
try { rmSync(join(f.base, ".gsd.lock"), { recursive: true, force: true }); } catch { /* */ }
|
|
894
|
+
|
|
895
|
+
const result = await f.orchestrator.advance();
|
|
896
|
+
|
|
897
|
+
// classifyFailure maps a generic Error to a recovery action; the orchestrator
|
|
898
|
+
// surfaces it as paused/stopped/error and journals the corresponding event.
|
|
899
|
+
assert.ok(["paused", "stopped", "error"].includes(result.kind), `unexpected kind ${result.kind}`);
|
|
900
|
+
const names = f.journalNames();
|
|
901
|
+
assert.ok(
|
|
902
|
+
names.includes("advance-paused") || names.includes("advance-stopped") || names.includes("advance-error"),
|
|
903
|
+
"recovery must journal an advance-paused/stopped/error event",
|
|
904
|
+
);
|
|
1032
905
|
});
|
|
1033
906
|
|
|
1034
|
-
//
|
|
907
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
908
|
+
// closeout regression: live-base resolver after worktree cleanup
|
|
909
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1035
910
|
|
|
1036
|
-
test("
|
|
911
|
+
test("live orchestrator base resolver prefers live project root after worktree cleanup", (t) => {
|
|
1037
912
|
const projectRoot = mkdtempSync(join(tmpdir(), "gsd-orch-root-"));
|
|
1038
913
|
const staleWorktreeRoot = join(projectRoot, ".gsd", "worktrees", "M002");
|
|
1039
914
|
mkdirSync(join(staleWorktreeRoot, ".bg-shell"), { recursive: true });
|
|
@@ -1050,7 +925,7 @@ test("wired orchestrator base resolver prefers live project root after worktree
|
|
|
1050
925
|
);
|
|
1051
926
|
});
|
|
1052
927
|
|
|
1053
|
-
test("
|
|
928
|
+
test("live orchestrator base resolver keeps a captured active git worktree", (t) => {
|
|
1054
929
|
const projectRoot = mkdtempSync(join(tmpdir(), "gsd-orch-worktree-"));
|
|
1055
930
|
const worktreeRoot = join(projectRoot, ".gsd", "worktrees", "M003");
|
|
1056
931
|
mkdirSync(worktreeRoot, { recursive: true });
|
|
@@ -1066,14 +941,14 @@ test("wired orchestrator base resolver keeps a captured active git worktree", (t
|
|
|
1066
941
|
);
|
|
1067
942
|
});
|
|
1068
943
|
|
|
1069
|
-
//
|
|
944
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
945
|
+
// Dispatch-decision parity (#5789) — formerly the createWiredDispatchAdapter
|
|
946
|
+
// tests. These exercise the exported pure decideOrchestratorDispatch helper.
|
|
947
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1070
948
|
|
|
1071
|
-
test("
|
|
949
|
+
test("decideOrchestratorDispatch forwards session-derived dispatch inputs identically to runDispatch", async () => {
|
|
1072
950
|
const stateSnapshot = makeState();
|
|
1073
951
|
|
|
1074
|
-
// Install a capturing registry so we observe the DispatchContext both code paths
|
|
1075
|
-
// build, and force a deterministic dispatch action so the parity assertion is
|
|
1076
|
-
// about *inputs*, not rule evaluation.
|
|
1077
952
|
const captured: DispatchContext[] = [];
|
|
1078
953
|
const captureRule: UnifiedRule = {
|
|
1079
954
|
name: "test-capture",
|
|
@@ -1093,7 +968,6 @@ test("wired DispatchAdapter forwards session-derived dispatch inputs identically
|
|
|
1093
968
|
setRegistry(new RuleRegistry([captureRule]));
|
|
1094
969
|
|
|
1095
970
|
try {
|
|
1096
|
-
// Mock ExtensionContext + ExtensionAPI with the surface the wired adapter touches.
|
|
1097
971
|
const fakeModelRegistry = {
|
|
1098
972
|
getAll: () => [],
|
|
1099
973
|
getProviderAuthMode: (_provider: string) => "apiKey" as const,
|
|
@@ -1105,30 +979,28 @@ test("wired DispatchAdapter forwards session-derived dispatch inputs identically
|
|
|
1105
979
|
contextWindow: 200_000,
|
|
1106
980
|
},
|
|
1107
981
|
modelRegistry: fakeModelRegistry,
|
|
1108
|
-
} as
|
|
982
|
+
} as never;
|
|
1109
983
|
const pi = {
|
|
1110
984
|
getActiveTools: () => ["read_file", "write_file"],
|
|
1111
|
-
} as
|
|
985
|
+
} as never;
|
|
1112
986
|
const basePath = "/tmp/parity-fixture";
|
|
1113
987
|
|
|
1114
|
-
// Path A —
|
|
1115
|
-
const
|
|
1116
|
-
const adapterResult = await adapter.decideNextUnit({ stateSnapshot });
|
|
988
|
+
// Path A — the orchestrator's pure dispatch decision.
|
|
989
|
+
const adapterResult = await decideOrchestratorDispatch(ctx, pi, basePath, undefined, { stateSnapshot });
|
|
1117
990
|
|
|
1118
991
|
// Path B — direct resolveDispatch call mirroring phases.ts:runDispatch.
|
|
1119
|
-
|
|
1120
|
-
const
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
? ctx.modelRegistry.getProviderAuthMode(provider)
|
|
992
|
+
const prefs = undefined;
|
|
993
|
+
const provider = (ctx as { model?: { provider?: string } }).model?.provider;
|
|
994
|
+
const authMode = provider && typeof fakeModelRegistry.getProviderAuthMode === "function"
|
|
995
|
+
? fakeModelRegistry.getProviderAuthMode(provider)
|
|
1124
996
|
: undefined;
|
|
1125
|
-
const activeTools =
|
|
997
|
+
const activeTools = ["read_file", "write_file"];
|
|
1126
998
|
const structuredQuestionsAvailable: "true" | "false" =
|
|
1127
999
|
prefs !== undefined && (prefs as { planning_depth?: string }).planning_depth === "deep"
|
|
1128
1000
|
? "false"
|
|
1129
1001
|
: supportsStructuredQuestions(activeTools, {
|
|
1130
1002
|
authMode,
|
|
1131
|
-
baseUrl: ctx.model?.baseUrl,
|
|
1003
|
+
baseUrl: (ctx as { model?: { baseUrl?: string } }).model?.baseUrl,
|
|
1132
1004
|
})
|
|
1133
1005
|
? "true"
|
|
1134
1006
|
: "false";
|
|
@@ -1140,17 +1012,15 @@ test("wired DispatchAdapter forwards session-derived dispatch inputs identically
|
|
|
1140
1012
|
state: stateSnapshot,
|
|
1141
1013
|
prefs,
|
|
1142
1014
|
structuredQuestionsAvailable,
|
|
1143
|
-
sessionContextWindow:
|
|
1144
|
-
sessionProvider:
|
|
1145
|
-
modelRegistry:
|
|
1015
|
+
sessionContextWindow: 200_000,
|
|
1016
|
+
sessionProvider: "anthropic",
|
|
1017
|
+
modelRegistry: fakeModelRegistry,
|
|
1146
1018
|
};
|
|
1147
1019
|
const directAction = await resolveDispatch(builtDirectCtx);
|
|
1148
1020
|
|
|
1149
|
-
// Two contexts captured: one per resolveDispatch call.
|
|
1150
1021
|
assert.equal(captured.length, 2, "expected two captured dispatch contexts");
|
|
1151
1022
|
const [adapterCtx, directCtx] = captured;
|
|
1152
1023
|
|
|
1153
|
-
// Parity assertion: session-derived fields are identical.
|
|
1154
1024
|
assert.equal(adapterCtx.structuredQuestionsAvailable, directCtx.structuredQuestionsAvailable);
|
|
1155
1025
|
assert.equal(adapterCtx.sessionContextWindow, directCtx.sessionContextWindow);
|
|
1156
1026
|
assert.equal(adapterCtx.sessionProvider, directCtx.sessionProvider);
|
|
@@ -1159,7 +1029,6 @@ test("wired DispatchAdapter forwards session-derived dispatch inputs identically
|
|
|
1159
1029
|
assert.equal(adapterCtx.mid, directCtx.mid);
|
|
1160
1030
|
assert.equal(adapterCtx.midTitle, directCtx.midTitle);
|
|
1161
1031
|
|
|
1162
|
-
// Dispatch action equality: both flows reach the same dispatch decision.
|
|
1163
1032
|
if (!adapterResult || !("unitType" in adapterResult)) {
|
|
1164
1033
|
assert.fail("expected adapter result to be a dispatch decision");
|
|
1165
1034
|
}
|
|
@@ -1177,7 +1046,7 @@ test("wired DispatchAdapter forwards session-derived dispatch inputs identically
|
|
|
1177
1046
|
}
|
|
1178
1047
|
});
|
|
1179
1048
|
|
|
1180
|
-
test("
|
|
1049
|
+
test("decideOrchestratorDispatch prefers caller-supplied dispatch inputs over ctx-derived values", async () => {
|
|
1181
1050
|
const stateSnapshot = makeState();
|
|
1182
1051
|
const captured: DispatchContext[] = [];
|
|
1183
1052
|
const captureRule: UnifiedRule = {
|
|
@@ -1213,14 +1082,11 @@ test("wired DispatchAdapter prefers caller-supplied dispatch inputs over ctx-der
|
|
|
1213
1082
|
contextWindow: 200_000,
|
|
1214
1083
|
},
|
|
1215
1084
|
modelRegistry: ctxModelRegistry,
|
|
1216
|
-
} as
|
|
1217
|
-
const pi = {
|
|
1218
|
-
|
|
1219
|
-
} as any;
|
|
1220
|
-
const adapter = createWiredDispatchAdapter(ctx, pi, "/tmp/parity-fixture");
|
|
1221
|
-
const session = { basePath: "/tmp/session-fixture" } as any;
|
|
1085
|
+
} as never;
|
|
1086
|
+
const pi = { getActiveTools: () => [] } as never;
|
|
1087
|
+
const session = { basePath: "/tmp/session-fixture" } as never;
|
|
1222
1088
|
|
|
1223
|
-
const result = await
|
|
1089
|
+
const result = await decideOrchestratorDispatch(ctx, pi, "/tmp/parity-fixture", undefined, {
|
|
1224
1090
|
stateSnapshot,
|
|
1225
1091
|
session,
|
|
1226
1092
|
structuredQuestionsAvailable: "true",
|
|
@@ -1242,7 +1108,7 @@ test("wired DispatchAdapter prefers caller-supplied dispatch inputs over ctx-der
|
|
|
1242
1108
|
}
|
|
1243
1109
|
});
|
|
1244
1110
|
|
|
1245
|
-
test("
|
|
1111
|
+
test("decideOrchestratorDispatch forwards constructor session when advance input omits session", async () => {
|
|
1246
1112
|
const stateSnapshot = makeState();
|
|
1247
1113
|
const captured: DispatchContext[] = [];
|
|
1248
1114
|
const captureRule: UnifiedRule = {
|
|
@@ -1263,16 +1129,15 @@ test("wired DispatchAdapter forwards constructor session when advance input omit
|
|
|
1263
1129
|
setRegistry(new RuleRegistry([captureRule]));
|
|
1264
1130
|
|
|
1265
1131
|
try {
|
|
1266
|
-
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as
|
|
1267
|
-
const pi = { getActiveTools: () => [] } as
|
|
1132
|
+
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
|
|
1133
|
+
const pi = { getActiveTools: () => [] } as never;
|
|
1268
1134
|
const session = {
|
|
1269
1135
|
basePath: "/tmp/worktree-fixture",
|
|
1270
1136
|
originalBasePath: "/tmp/project-fixture",
|
|
1271
1137
|
currentMilestoneId: "M001",
|
|
1272
|
-
} as
|
|
1273
|
-
const adapter = createWiredDispatchAdapter(ctx, pi, "/tmp/project-fixture", session);
|
|
1138
|
+
} as never;
|
|
1274
1139
|
|
|
1275
|
-
const result = await
|
|
1140
|
+
const result = await decideOrchestratorDispatch(ctx, pi, "/tmp/project-fixture", session, { stateSnapshot });
|
|
1276
1141
|
|
|
1277
1142
|
assert.ok(result);
|
|
1278
1143
|
assert.equal(captured.length, 1, "expected one captured dispatch context");
|
|
@@ -1283,7 +1148,7 @@ test("wired DispatchAdapter forwards constructor session when advance input omit
|
|
|
1283
1148
|
}
|
|
1284
1149
|
});
|
|
1285
1150
|
|
|
1286
|
-
test("
|
|
1151
|
+
test("decideOrchestratorDispatch adopts next active milestone after the session milestone is closed", async (t) => {
|
|
1287
1152
|
const base = mkdtempSync(join(tmpdir(), "gsd-orchestrator-milestone-adopt-"));
|
|
1288
1153
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
1289
1154
|
|
|
@@ -1314,28 +1179,27 @@ test("wired DispatchAdapter adopts next active milestone after the session miles
|
|
|
1314
1179
|
setRegistry(new RuleRegistry([captureRule]));
|
|
1315
1180
|
|
|
1316
1181
|
try {
|
|
1317
|
-
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as
|
|
1318
|
-
const pi = { getActiveTools: () => [] } as
|
|
1182
|
+
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
|
|
1183
|
+
const pi = { getActiveTools: () => [] } as never;
|
|
1319
1184
|
const session = {
|
|
1320
1185
|
basePath: base,
|
|
1321
1186
|
originalBasePath: base,
|
|
1322
1187
|
currentMilestoneId: "M001",
|
|
1323
|
-
} as
|
|
1324
|
-
const adapter = createWiredDispatchAdapter(ctx, pi, base, session);
|
|
1188
|
+
} as never;
|
|
1325
1189
|
|
|
1326
|
-
const result = await
|
|
1190
|
+
const result = await decideOrchestratorDispatch(ctx, pi, base, session, { stateSnapshot });
|
|
1327
1191
|
|
|
1328
1192
|
assert.ok(result);
|
|
1329
|
-
if (!("unitType" in result)) assert.fail(`expected dispatch decision, got ${JSON.stringify(result)}`);
|
|
1193
|
+
if (!result || !("unitType" in result)) assert.fail(`expected dispatch decision, got ${JSON.stringify(result)}`);
|
|
1330
1194
|
assert.equal(result.unitId, "M002/S01/T01");
|
|
1331
|
-
assert.equal(session.currentMilestoneId, "M002");
|
|
1195
|
+
assert.equal((session as { currentMilestoneId: string }).currentMilestoneId, "M002");
|
|
1332
1196
|
assert.equal(captured[0]?.session?.currentMilestoneId, "M002");
|
|
1333
1197
|
} finally {
|
|
1334
1198
|
resetRegistry();
|
|
1335
1199
|
}
|
|
1336
1200
|
});
|
|
1337
1201
|
|
|
1338
|
-
test("
|
|
1202
|
+
test("decideOrchestratorDispatch keeps blocking stale milestone worktree scope", async (t) => {
|
|
1339
1203
|
const base = mkdtempSync(join(tmpdir(), "gsd-orchestrator-worktree-block-"));
|
|
1340
1204
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
1341
1205
|
|
|
@@ -1349,16 +1213,15 @@ test("wired DispatchAdapter keeps blocking stale milestone worktree scope", asyn
|
|
|
1349
1213
|
};
|
|
1350
1214
|
const worktreePath = join(base, ".gsd", "worktrees", "M001");
|
|
1351
1215
|
mkdirSync(worktreePath, { recursive: true });
|
|
1352
|
-
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as
|
|
1353
|
-
const pi = { getActiveTools: () => [] } as
|
|
1216
|
+
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
|
|
1217
|
+
const pi = { getActiveTools: () => [] } as never;
|
|
1354
1218
|
const session = {
|
|
1355
1219
|
basePath: worktreePath,
|
|
1356
1220
|
originalBasePath: base,
|
|
1357
1221
|
currentMilestoneId: "M001",
|
|
1358
|
-
} as
|
|
1359
|
-
const adapter = createWiredDispatchAdapter(ctx, pi, base, session);
|
|
1222
|
+
} as never;
|
|
1360
1223
|
|
|
1361
|
-
const result = await
|
|
1224
|
+
const result = await decideOrchestratorDispatch(ctx, pi, base, session, { stateSnapshot });
|
|
1362
1225
|
|
|
1363
1226
|
assert.deepEqual(result, {
|
|
1364
1227
|
kind: "blocked",
|
|
@@ -1366,13 +1229,13 @@ test("wired DispatchAdapter keeps blocking stale milestone worktree scope", asyn
|
|
|
1366
1229
|
'Dispatch milestone mismatch: context mid "M002" does not match session.currentMilestoneId "M001". The active worktree/session and derived project state disagree; recover, park, or discard the stranded milestone before continuing.',
|
|
1367
1230
|
action: "pause",
|
|
1368
1231
|
});
|
|
1369
|
-
assert.equal(session.currentMilestoneId, "M001");
|
|
1232
|
+
assert.equal((session as { currentMilestoneId: string }).currentMilestoneId, "M001");
|
|
1370
1233
|
});
|
|
1371
1234
|
|
|
1372
|
-
test("
|
|
1235
|
+
test("decideOrchestratorDispatch replays pending verification retry dispatch", async () => {
|
|
1373
1236
|
const stateSnapshot = makeState();
|
|
1374
|
-
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as
|
|
1375
|
-
const pi = { getActiveTools: () => [] } as
|
|
1237
|
+
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
|
|
1238
|
+
const pi = { getActiveTools: () => [] } as never;
|
|
1376
1239
|
const session = {
|
|
1377
1240
|
basePath: "/tmp/worktree-fixture",
|
|
1378
1241
|
pendingOrchestrationDispatch: null,
|
|
@@ -1385,22 +1248,25 @@ test("wired DispatchAdapter replays pending verification retry dispatch", async
|
|
|
1385
1248
|
mid: "M004",
|
|
1386
1249
|
midTitle: "Milestone 4",
|
|
1387
1250
|
},
|
|
1388
|
-
} as
|
|
1389
|
-
const adapter = createWiredDispatchAdapter(ctx, pi, "/tmp/project-fixture", session);
|
|
1251
|
+
} as never;
|
|
1390
1252
|
|
|
1391
|
-
const result = await
|
|
1253
|
+
const result = await decideOrchestratorDispatch(ctx, pi, "/tmp/project-fixture", session, { stateSnapshot });
|
|
1392
1254
|
|
|
1393
1255
|
assert.ok(result);
|
|
1394
|
-
if (!("unitType" in result)) assert.fail("expected dispatch decision");
|
|
1256
|
+
if (!result || !("unitType" in result)) assert.fail("expected dispatch decision");
|
|
1395
1257
|
assert.equal(result.unitType, "complete-slice");
|
|
1396
1258
|
assert.equal(result.unitId, "M004/S01");
|
|
1397
1259
|
assert.equal(result.reason, "verification-retry");
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1260
|
+
const sess = session as {
|
|
1261
|
+
pendingVerificationRetryDispatch: unknown;
|
|
1262
|
+
pendingOrchestrationDispatch: { prompt?: string; state?: unknown } | null;
|
|
1263
|
+
};
|
|
1264
|
+
assert.equal(sess.pendingVerificationRetryDispatch, null);
|
|
1265
|
+
assert.equal(sess.pendingOrchestrationDispatch?.prompt, "repair slice closeout");
|
|
1266
|
+
assert.equal(sess.pendingOrchestrationDispatch?.state, stateSnapshot);
|
|
1401
1267
|
});
|
|
1402
1268
|
|
|
1403
|
-
test("
|
|
1269
|
+
test("decideOrchestratorDispatch clears verification retry state when skipping an already closed retry dispatch", async () => {
|
|
1404
1270
|
const stateSnapshot = makeState();
|
|
1405
1271
|
const base = mkdtempSync(join(tmpdir(), "gsd-orchestrator-closed-retry-"));
|
|
1406
1272
|
|
|
@@ -1425,8 +1291,8 @@ test("wired DispatchAdapter clears verification retry state when skipping an alr
|
|
|
1425
1291
|
};
|
|
1426
1292
|
setRegistry(new RuleRegistry([retryRule]));
|
|
1427
1293
|
|
|
1428
|
-
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as
|
|
1429
|
-
const pi = { getActiveTools: () => [] } as
|
|
1294
|
+
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
|
|
1295
|
+
const pi = { getActiveTools: () => [] } as never;
|
|
1430
1296
|
const session = {
|
|
1431
1297
|
basePath: base,
|
|
1432
1298
|
pendingOrchestrationDispatch: { stale: true },
|
|
@@ -1435,17 +1301,17 @@ test("wired DispatchAdapter clears verification retry state when skipping an alr
|
|
|
1435
1301
|
failureContext: "artifact missing",
|
|
1436
1302
|
attempt: 1,
|
|
1437
1303
|
},
|
|
1438
|
-
} as
|
|
1439
|
-
const adapter = createWiredDispatchAdapter(ctx, pi, base, session);
|
|
1304
|
+
} as never;
|
|
1440
1305
|
|
|
1441
|
-
const result = await
|
|
1306
|
+
const result = await decideOrchestratorDispatch(ctx, pi, base, session, { stateSnapshot });
|
|
1442
1307
|
|
|
1443
1308
|
assert.deepEqual(result, {
|
|
1444
1309
|
kind: "skipped",
|
|
1445
1310
|
reason: "execute-task M001/S01/T01 is already complete",
|
|
1446
1311
|
});
|
|
1447
|
-
|
|
1448
|
-
assert.equal(
|
|
1312
|
+
const sess = session as { pendingVerificationRetry: unknown; pendingOrchestrationDispatch: unknown };
|
|
1313
|
+
assert.equal(sess.pendingVerificationRetry, null);
|
|
1314
|
+
assert.equal(sess.pendingOrchestrationDispatch, null);
|
|
1449
1315
|
} finally {
|
|
1450
1316
|
resetRegistry();
|
|
1451
1317
|
closeDatabase();
|
|
@@ -1453,7 +1319,7 @@ test("wired DispatchAdapter clears verification retry state when skipping an alr
|
|
|
1453
1319
|
}
|
|
1454
1320
|
});
|
|
1455
1321
|
|
|
1456
|
-
test("
|
|
1322
|
+
test("decideOrchestratorDispatch preserves stop reason as a blocked decision", async () => {
|
|
1457
1323
|
const stateSnapshot = makeState();
|
|
1458
1324
|
const stopRule: UnifiedRule = {
|
|
1459
1325
|
name: "test-stop",
|
|
@@ -1469,11 +1335,10 @@ test("wired DispatchAdapter preserves stop reason as a blocked decision", async
|
|
|
1469
1335
|
setRegistry(new RuleRegistry([stopRule]));
|
|
1470
1336
|
|
|
1471
1337
|
try {
|
|
1472
|
-
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as
|
|
1473
|
-
const pi = { getActiveTools: () => [] } as
|
|
1474
|
-
const adapter = createWiredDispatchAdapter(ctx, pi, "/tmp/parity-fixture");
|
|
1338
|
+
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
|
|
1339
|
+
const pi = { getActiveTools: () => [] } as never;
|
|
1475
1340
|
|
|
1476
|
-
const result = await
|
|
1341
|
+
const result = await decideOrchestratorDispatch(ctx, pi, "/tmp/parity-fixture", undefined, { stateSnapshot });
|
|
1477
1342
|
|
|
1478
1343
|
assert.deepEqual(result, {
|
|
1479
1344
|
kind: "blocked",
|
|
@@ -1485,7 +1350,7 @@ test("wired DispatchAdapter preserves stop reason as a blocked decision", async
|
|
|
1485
1350
|
}
|
|
1486
1351
|
});
|
|
1487
1352
|
|
|
1488
|
-
test("
|
|
1353
|
+
test("decideOrchestratorDispatch preserves dispatch skip instead of collapsing it to no remaining units", async () => {
|
|
1489
1354
|
const stateSnapshot = makeState();
|
|
1490
1355
|
const skipRule: UnifiedRule = {
|
|
1491
1356
|
name: "test-skip-gate",
|
|
@@ -1500,11 +1365,10 @@ test("wired DispatchAdapter preserves dispatch skip instead of collapsing it to
|
|
|
1500
1365
|
setRegistry(new RuleRegistry([skipRule]));
|
|
1501
1366
|
|
|
1502
1367
|
try {
|
|
1503
|
-
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as
|
|
1504
|
-
const pi = { getActiveTools: () => [] } as
|
|
1505
|
-
const adapter = createWiredDispatchAdapter(ctx, pi, "/tmp/parity-fixture");
|
|
1368
|
+
const ctx = { model: {}, modelRegistry: { getAll: () => [] } } as never;
|
|
1369
|
+
const pi = { getActiveTools: () => [] } as never;
|
|
1506
1370
|
|
|
1507
|
-
const result = await
|
|
1371
|
+
const result = await decideOrchestratorDispatch(ctx, pi, "/tmp/parity-fixture", undefined, { stateSnapshot });
|
|
1508
1372
|
|
|
1509
1373
|
assert.deepEqual(result, {
|
|
1510
1374
|
kind: "skipped",
|