@selesai/code 0.9.10 → 0.9.12
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/CHANGELOG.md +271 -255
- package/dist/cli/args.js +13 -2
- package/dist/cli/args.test.js +8 -0
- package/dist/cli/credential-print.js +4 -4
- package/dist/cli.js +0 -0
- package/dist/config.d.ts +4 -6
- package/dist/config.js +10 -14
- package/dist/core/agent-session-auto-handoff.test.js +24 -1
- package/dist/core/agent-session.d.ts +10 -4
- package/dist/core/agent-session.js +61 -46
- package/dist/core/compaction/branch-summarization.js +30 -30
- package/dist/core/compaction/compaction.js +81 -81
- package/dist/core/compaction/utils.js +2 -2
- package/dist/core/defaults.d.ts +1 -0
- package/dist/core/defaults.js +9 -0
- package/dist/core/export-html/template.css +1066 -1066
- package/dist/core/export-html/template.html +55 -55
- package/dist/core/export-html/template.js +1864 -1864
- package/dist/core/export-html/vendor/highlight.min.js +1212 -1212
- package/dist/core/export-html/vendor/marked.min.js +78 -78
- package/dist/core/extensions/loader.js +108 -35
- package/dist/core/extensions/loader.test.js +21 -0
- package/dist/core/extensions/types.d.ts +12 -4
- package/dist/core/keybindings.d.ts +2 -2
- package/dist/core/messages.js +7 -7
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.js +10 -4
- package/dist/core/package-manager.js +4 -4
- package/dist/core/sdk.d.ts +5 -5
- package/dist/core/sdk.js +16 -4
- package/dist/core/settings-manager.d.ts +6 -2
- package/dist/core/settings-manager.js +22 -3
- package/dist/core/tools/bash.d.ts +18 -1
- package/dist/core/tools/bash.js +41 -23
- package/dist/core/tools/index.d.ts +4 -2
- package/dist/core/tools/index.js +9 -2
- package/dist/core/tools/powershell.d.ts +15 -0
- package/dist/core/tools/powershell.js +38 -0
- package/dist/core/tools/powershell.test.d.ts +1 -0
- package/dist/core/tools/powershell.test.js +20 -0
- package/dist/defaults/settings.json +0 -1
- package/dist/extensions/context-compaction-reminder.test.ts +82 -82
- package/dist/extensions/context-compaction-reminder.ts +28 -28
- package/dist/extensions/model-prompt-injector/index.test.ts +0 -10
- package/dist/extensions/model-prompt-injector/index.ts +0 -3
- package/dist/extensions/pi-intercom/CHANGELOG.md +53 -16
- package/dist/extensions/pi-intercom/LICENSE +21 -21
- package/dist/extensions/pi-intercom/README.md +41 -8
- package/dist/extensions/pi-intercom/banner.png +0 -0
- package/dist/extensions/pi-intercom/broker/broker.ts +498 -215
- package/dist/extensions/pi-intercom/broker/client.test.ts +83 -83
- package/dist/extensions/pi-intercom/broker/client.ts +39 -12
- package/dist/extensions/pi-intercom/broker/extension.test.ts +486 -387
- package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -114
- package/dist/extensions/pi-intercom/broker/paths.test.ts +153 -153
- package/dist/extensions/pi-intercom/broker/paths.ts +134 -134
- package/dist/extensions/pi-intercom/broker/protocol.ts +26 -0
- package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -34
- package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -21
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +5 -20
- package/dist/extensions/pi-intercom/config.test.ts +41 -0
- package/dist/extensions/pi-intercom/config.ts +21 -0
- package/dist/extensions/pi-intercom/cwd.test.ts +40 -40
- package/dist/extensions/pi-intercom/cwd.ts +31 -31
- package/dist/extensions/pi-intercom/extension-api.ts +84 -44
- package/dist/extensions/pi-intercom/format-context.test.ts +31 -31
- package/dist/extensions/pi-intercom/format-context.ts +32 -32
- package/dist/extensions/pi-intercom/index.ts +362 -26
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +819 -13
- package/dist/extensions/pi-intercom/package.json +1 -1
- package/dist/extensions/pi-intercom/project-agent.ts +2 -2
- package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +8 -110
- package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -66
- package/dist/extensions/pi-intercom/types.ts +32 -5
- package/dist/extensions/pi-intercom/ui/compose.ts +143 -143
- package/dist/extensions/pi-intercom/ui/inline-message.ts +6 -0
- package/dist/extensions/pi-intercom/ui/session-list.ts +166 -166
- package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +286 -286
- package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +108 -108
- package/dist/extensions/pi-powerline-footer/separators.ts +57 -57
- package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +47 -47
- package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +39 -39
- package/dist/extensions/pi-powerline-footer/theme.example.json +24 -24
- package/dist/extensions/pi-powerline-footer/theme.json +12 -12
- package/dist/extensions/pi-powerline-footer/tps.ts +345 -345
- package/dist/extensions/pi-rewind-hook/README.md +245 -245
- package/dist/extensions/pi-rewind-hook/index.ts +1445 -1445
- package/dist/extensions/pi-rewind-hook/package.json +29 -29
- package/dist/extensions/pi-subagents/.github/assets/subagent-fleet-inspector.png +0 -0
- package/dist/extensions/pi-subagents/.github/workflows/release.yml +28 -0
- package/dist/extensions/pi-subagents/.github/workflows/test.yml +51 -0
- package/dist/extensions/pi-subagents/.greptile/config.json +33 -0
- package/dist/extensions/pi-subagents/.greptile/files.json +8 -0
- package/dist/extensions/pi-subagents/.greptile/rules.md +16 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +309 -87
- package/dist/extensions/pi-subagents/README.md +15 -17
- package/dist/extensions/pi-subagents/agents/oracle.md +3 -1
- package/dist/extensions/pi-subagents/agents/researcher.md +32 -13
- package/dist/extensions/pi-subagents/agents/reviewer.md +13 -2
- package/dist/extensions/pi-subagents/agents/scout.md +2 -2
- package/dist/extensions/pi-subagents/agents/worker.md +2 -1
- package/dist/extensions/pi-subagents/async-retention-discovery-worker.mjs +180 -0
- package/dist/extensions/pi-subagents/docs/configuration.md +106 -27
- package/dist/extensions/pi-subagents/docs/extension-api.md +85 -21
- package/dist/extensions/pi-subagents/docs/missions.md +41 -4
- package/dist/extensions/pi-subagents/docs/models.md +52 -9
- package/dist/extensions/pi-subagents/docs/observability.md +103 -16
- package/dist/extensions/pi-subagents/docs/tool-reference.md +60 -45
- package/dist/extensions/pi-subagents/docs/watchdog.md +60 -3
- package/dist/extensions/pi-subagents/docs/workflows.md +181 -7
- package/dist/extensions/pi-subagents/index.ts +10 -1
- package/dist/extensions/pi-subagents/install.mjs +4 -4
- package/dist/extensions/pi-subagents/package-lock.json +1483 -1470
- package/dist/extensions/pi-subagents/package.json +6 -3
- package/dist/extensions/pi-subagents/prompts/council.md +60 -0
- package/dist/extensions/pi-subagents/prompts/parallel-review.md +5 -1
- package/dist/extensions/pi-subagents/prompts/review-loop.md +9 -5
- package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +254 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +18 -17
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +31 -9
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +107 -140
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +9 -12
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +39 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +44 -92
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +261 -368
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +6 -2
- package/dist/extensions/pi-subagents/src/agents/agents.ts +388 -85
- package/dist/extensions/pi-subagents/src/agents/builtin-names.ts +14 -0
- package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +418 -0
- package/dist/extensions/pi-subagents/src/agents/skills.ts +1 -1
- package/dist/extensions/pi-subagents/src/api/agents.ts +7 -0
- package/dist/extensions/pi-subagents/src/api/external-job-provider.ts +195 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +88 -27
- package/dist/extensions/pi-subagents/src/api/project-panes.ts +2 -0
- package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +20 -11
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +14 -6
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +5 -4
- package/dist/extensions/pi-subagents/src/extension/index.ts +270 -187
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +65 -28
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +120 -11
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +46 -64
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +51 -37
- package/dist/extensions/pi-subagents/src/extension/tool-result.ts +19 -0
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +2 -2
- package/dist/extensions/pi-subagents/src/inspectors/herdr/client.ts +3 -3
- package/dist/extensions/pi-subagents/src/inspectors/herdr/focus.ts +55 -0
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +230 -46
- package/dist/extensions/pi-subagents/src/integrations/herdr-status.ts +26 -4
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +32 -10
- package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +6 -1
- package/dist/extensions/pi-subagents/src/missions/store.ts +4 -9
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +2 -2
- package/dist/extensions/pi-subagents/src/policy/authority.ts +2 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +31 -8
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +210 -52
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +63 -33
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +71 -17
- package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +886 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +14 -5
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +49 -56
- package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +1 -0
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +3 -33
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +5 -0
- package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +5 -1
- package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +22 -12
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +105 -71
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +68 -19
- package/dist/extensions/pi-subagents/src/runs/background/index-segment.ts +59 -0
- package/dist/extensions/pi-subagents/src/runs/background/inspect-rpc.ts +443 -0
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +94 -10
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +158 -90
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +141 -53
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +8 -5
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +14 -4
- package/dist/extensions/pi-subagents/src/runs/background/run-id-query.ts +7 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +13 -14
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +46 -25
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +823 -0
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +9 -3
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +11 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +594 -216
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +17 -13
- package/dist/extensions/pi-subagents/src/runs/background/terminal-run-index.ts +138 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +23 -4
- package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +82 -4
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +5 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +21 -14
- package/dist/extensions/pi-subagents/src/runs/foreground/async-stop-action.ts +23 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +153 -58
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -9
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1283 -1657
- package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +274 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +59 -10
- package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/chain-outputs.ts +1 -3
- package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +36 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +52 -2
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/extension-bindings.ts +78 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-job-bridge.ts +460 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-job-runner.ts +389 -0
- package/dist/extensions/pi-subagents/src/runs/shared/fast-mode-extension.ts +10 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +243 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +201 -85
- package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +106 -39
- package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +145 -0
- package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +150 -27
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +46 -4
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +19 -15
- package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +5 -1
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +86 -2
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +9 -9
- package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +0 -6
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +18 -4
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +51 -7
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +62 -21
- package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +1 -3
- package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +1 -3
- package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +1 -23
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +14 -2
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +2 -2
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +22 -2
- package/dist/extensions/pi-subagents/src/shared/capacity-resilient-json.ts +102 -0
- package/dist/extensions/pi-subagents/src/shared/completion-owner.ts +14 -0
- package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +49 -1
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +59 -1
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +12 -5
- package/dist/extensions/pi-subagents/src/shared/model-info.ts +20 -0
- package/dist/extensions/pi-subagents/src/shared/prompt-resources.ts +0 -40
- package/dist/extensions/pi-subagents/src/shared/settings.ts +11 -30
- package/dist/extensions/pi-subagents/src/shared/thinking-ceiling.ts +52 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +228 -49
- package/dist/extensions/pi-subagents/src/shared/utils.ts +11 -3
- package/dist/extensions/pi-subagents/src/shared/watch-strategy.ts +10 -0
- package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +0 -162
- package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +2 -5
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +105 -13
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +6 -36
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +829 -0
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +1415 -0
- package/dist/extensions/pi-subagents/src/tui/render.ts +78 -7
- package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +1 -1
- package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +1 -0
- package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +59 -51
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +8 -2
- package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +2 -1
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +4 -4
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +471 -86
- package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +140 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/cli.d.ts +3 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/register-bedrock.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/restore-sandbox-env.d.ts +17 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/args.d.ts +57 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/config-selector.d.ts +16 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/file-processor.d.ts +15 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/initial-message.d.ts +18 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/list-models.d.ts +9 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/project-trust.d.ts +10 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/session-picker.d.ts +10 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/startup-ui.d.ts +20 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli.d.ts +3 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/config.d.ts +96 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session-runtime.d.ts +119 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session-services.d.ts +85 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session.d.ts +620 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/auth-guidance.d.ts +5 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/auth-storage.d.ts +56 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/bash-executor.d.ts +32 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/cache-stats.d.ts +49 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/branch-summarization.d.ts +93 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/compaction.d.ts +128 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/index.d.ts +7 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/utils.d.ts +38 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/defaults.d.ts +3 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/diagnostics.d.ts +15 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/event-bus.d.ts +9 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/exec.d.ts +29 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/experimental.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/ansi-to-html.d.ts +22 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/index.d.ts +37 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/tool-renderer.d.ts +34 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/index.d.ts +12 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/loader.d.ts +23 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/runner.d.ts +171 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +1261 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/wrapper.d.ts +20 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/footer-data-provider.d.ts +54 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/http-dispatcher.d.ts +22 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/index.d.ts +13 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/keybindings.d.ts +358 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/messages.d.ts +77 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-config.d.ts +512 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-registry.d.ts +43 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-resolver.d.ts +121 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-runtime.d.ts +82 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/models-store.d.ts +17 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/output-guard.d.ts +7 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/package-manager.d.ts +210 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/project-trust.d.ts +15 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/prompt-templates.d.ts +54 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/provider-attribution.d.ts +4 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/provider-composer.d.ts +55 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/radius.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/remote-catalog-provider.d.ts +5 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/resolve-config-value.d.ts +30 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/resource-loader.d.ts +206 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/runtime-credentials.d.ts +15 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/sdk.d.ts +106 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/session-cwd.d.ts +19 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/session-manager.d.ts +356 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/settings-manager.d.ts +297 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/skills.d.ts +60 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/slash-commands.d.ts +15 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/source-info.d.ts +18 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/system-prompt.d.ts +28 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/telemetry.d.ts +3 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/timings.d.ts +10 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/bash.d.ts +68 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/edit-diff.d.ts +106 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/edit.d.ts +51 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/file-mutation-queue.d.ts +6 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/find.d.ts +35 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/grep.d.ts +37 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/index.d.ts +40 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/ls.d.ts +37 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/output-accumulator.d.ts +52 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/path-utils.d.ts +10 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/read.d.ts +35 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/render-utils.d.ts +24 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/tool-definition-wrapper.d.ts +14 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/truncate.d.ts +70 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/write.d.ts +26 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/trust-manager.d.ts +36 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/usage-totals.d.ts +19 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/index.d.ts +3 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/client.d.ts +61 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/huggingface.d.ts +23 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/index.d.ts +3 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/provider.d.ts +10 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/ui.d.ts +42 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/index.d.ts +35 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/main.d.ts +12 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/migrations.d.ts +33 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/index.d.ts +9 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/armin.d.ts +34 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/assistant-message.d.ts +22 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/bash-execution.d.ts +34 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/bordered-loader.d.ts +16 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/branch-summary-message.d.ts +16 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/compaction-summary-message.d.ts +16 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/config-selector.d.ts +102 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/countdown-timer.d.ts +14 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-editor.d.ts +21 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-entry.d.ts +19 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-message.d.ts +20 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/daxnuts.d.ts +23 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/diff.d.ts +12 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/dynamic-border.d.ts +15 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/earendil-announcement.d.ts +5 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-editor.d.ts +22 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-input.d.ts +23 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-selector.d.ts +26 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/first-time-setup.d.ts +25 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/footer.d.ts +32 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/index.d.ts +34 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/keybinding-hints.d.ts +13 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/login-dialog.d.ts +52 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/model-selector.d.ts +54 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/oauth-selector.d.ts +33 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/scoped-models-selector.d.ts +42 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/session-selector-search.d.ts +23 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/session-selector.d.ts +95 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/settings-selector.d.ts +77 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/show-images-selector.d.ts +10 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/skill-invocation-message.d.ts +17 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/status-indicator.d.ts +28 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/theme-selector.d.ts +11 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/thinking-selector.d.ts +11 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/tool-execution.d.ts +63 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/tree-selector.d.ts +94 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/trust-selector.d.ts +23 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/user-message-selector.d.ts +30 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/user-message.d.ts +14 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/visual-truncate.d.ts +24 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/interactive-mode.d.ts +394 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/model-search.d.ts +12 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/theme/theme-controller.d.ts +29 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/theme/theme.d.ts +120 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/print-mode.d.ts +28 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/jsonl.d.ts +17 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-client.d.ts +246 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-mode.d.ts +20 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-types.d.ts +457 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/package-manager-cli.d.ts +8 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/rpc-entry.d.ts +3 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/ansi.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/changelog.d.ts +22 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/child-process.d.ts +18 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard-image.d.ts +11 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard-native.d.ts +11 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard.d.ts +4 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/deprecation.d.ts +4 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/exif-orientation.d.ts +5 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/frontmatter.d.ts +8 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/fs-watch.d.ts +5 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/git.d.ts +26 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/html.d.ts +7 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-convert.d.ts +10 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-process.d.ts +18 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize-core.d.ts +30 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize-worker.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize.d.ts +16 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/json.d.ts +3 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/mime.d.ts +3 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/open-browser.d.ts +9 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/paths.d.ts +31 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/photon.d.ts +21 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/pi-user-agent.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/shell.d.ts +31 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/sleep.d.ts +5 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/syntax-highlight.d.ts +12 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/tools-manager.d.ts +3 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/version-check.d.ts +15 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/windows-self-update.d.ts +3 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/index.mjs +1 -1
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +0 -54
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1053 -414
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +194 -4
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +59 -4
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +0 -77
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +107 -6
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +211 -897
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +265 -375
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +77 -7
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +10 -10
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +547 -30
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +2112 -310
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +274 -34
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +20 -3
- package/dist/extensions/pi-subagents/test/support/isolated-temp-root.mjs +9 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +30 -5
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +2 -0
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +4 -2
- package/dist/extensions/pi-subagents/test/support/register-loader.mjs +1 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +197 -0
- package/dist/extensions/pi-subagents/test/unit/agent-contract.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +204 -27
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +444 -145
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +256 -4
- package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +9 -7
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +152 -22
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +29 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +205 -23
- package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +579 -0
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +24 -0
- package/dist/extensions/pi-subagents/test/unit/authority-policy.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/capacity-resilient-json.test.ts +155 -0
- package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +7 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +123 -0
- package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +81 -3
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +109 -7
- package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +5 -2
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -2
- package/dist/extensions/pi-subagents/test/unit/extension-bindings.test.ts +79 -0
- package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/external-job-runner.test.ts +961 -0
- package/dist/extensions/pi-subagents/test/unit/file-system-retry.test.ts +64 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +1110 -0
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +1664 -0
- package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +81 -1
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +138 -11
- package/dist/extensions/pi-subagents/test/unit/herdr-status-bridge.test.ts +41 -2
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +210 -79
- package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +95 -0
- package/dist/extensions/pi-subagents/test/unit/inspect-rpc.test.ts +596 -0
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +10 -1
- package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +3 -5
- package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +167 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +319 -18
- package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +78 -0
- package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +19 -5
- package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +129 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +231 -2
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +28 -1
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +106 -2
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +204 -7
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +59 -4
- package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +2 -6
- package/dist/extensions/pi-subagents/test/unit/pi-args-permission-system.test.ts +34 -2
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +116 -6
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +12 -3
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +56 -61
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +416 -1
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +9 -2
- package/dist/extensions/pi-subagents/test/unit/project-panes-public-api.test.ts +42 -0
- package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +21 -30
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +86 -11
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +49 -1
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +233 -5
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +28 -5
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +323 -20
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +66 -10
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +68 -5
- package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +174 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +679 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-store-root.test.ts +67 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +55 -44
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +399 -84
- package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +13 -0
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +15 -10
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +11 -7
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +40 -1
- package/dist/extensions/pi-subagents/test/unit/subagent-action-recovery.test.ts +10 -2
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +38 -3
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +110 -12
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +204 -19
- package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +25 -0
- package/dist/extensions/pi-subagents/test/unit/thinking-ceiling.test.ts +73 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +90 -50
- package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +1 -2
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +234 -7
- package/dist/extensions/pi-subagents/test/unit/watch-strategy.test.ts +24 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-permission-arbiter.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +37 -9
- package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +302 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +167 -7
- package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +113 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +6 -6
- package/dist/extensions/pi-web-agent/package.json +31 -31
- package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -205
- package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -136
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -152
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -25
- package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -28
- package/dist/extensions/pi-web-agent/src/changelog-notice.ts +136 -136
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -946
- package/dist/extensions/pi-web-agent/src/extension.ts +126 -126
- package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -118
- package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -199
- package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -100
- package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -117
- package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -67
- package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -60
- package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -50
- package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -52
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -105
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -45
- package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -28
- package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -47
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -376
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -64
- package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -181
- package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -101
- package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -81
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +210 -210
- package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -75
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -61
- package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -54
- package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -41
- package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -20
- package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -63
- package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -114
- package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -72
- package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -96
- package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -71
- package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -31
- package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -31
- package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -157
- package/dist/extensions/pi-web-agent/src/types.ts +88 -88
- package/dist/extensions/ponytail/index.js +31 -25
- package/dist/extensions/ponytail/package.json +8 -8
- package/dist/extensions/ponytail/ponytail-config.cjs +34 -7
- package/dist/extensions/ponytail/ponytail-instructions.cjs +7 -9
- package/dist/extensions/ponytail/test/extension.test.js +7 -8
- package/dist/extensions/ponytail/test/helpers.test.js +1 -1
- package/dist/extensions/question/batch.ts +103 -103
- package/dist/extensions/question/constants.ts +30 -30
- package/dist/extensions/question/helpers.ts +58 -58
- package/dist/extensions/question/navigation.ts +14 -14
- package/dist/extensions/question/package.json +19 -19
- package/dist/extensions/question/schemas.ts +43 -43
- package/dist/extensions/question/selection-mode.ts +46 -46
- package/dist/extensions/question/shortcuts.ts +44 -44
- package/dist/extensions/question/types.ts +132 -132
- package/dist/extensions/test-resolve-hook-impl.mjs +6 -6
- package/dist/extensions/test-resolve-hook.mjs +6 -6
- package/dist/extensions/web-agent-onboarding.ts +222 -222
- package/dist/extensions/workflow/package.json +17 -17
- package/dist/package-manager-cli.js +71 -71
- package/dist/rpc-entry.js +0 -0
- package/dist/skills/agent-browser/SKILL.md +52 -52
- package/dist/skills/batch-grill-me/SKILL.md +19 -19
- package/dist/skills/brandkit/SKILL.md +799 -0
- package/dist/skills/design-taste-frontend/SKILL.md +1207 -0
- package/dist/skills/design-taste-frontend-v1/SKILL.md +227 -0
- package/dist/skills/full-output-enforcement/SKILL.md +50 -0
- package/dist/skills/gpt-taste/SKILL.md +75 -0
- package/dist/skills/grill-me/SKILL.md +10 -10
- package/dist/skills/handoff/SKILL.md +16 -16
- package/dist/skills/handoff-text/SKILL.md +14 -14
- package/dist/skills/high-end-visual-design/SKILL.md +99 -0
- package/dist/skills/image-to-code/SKILL.md +1229 -0
- package/dist/skills/imagegen-frontend-mobile/SKILL.md +1466 -0
- package/dist/skills/imagegen-frontend-web/SKILL.md +988 -0
- package/dist/skills/implanger/SKILL.md +69 -69
- package/dist/skills/improve-codebase/REFERENCE.md +78 -78
- package/dist/skills/improve-codebase/SKILL.md +178 -178
- package/dist/skills/industrial-brutalist-ui/SKILL.md +93 -0
- package/dist/skills/minimalist-ui/SKILL.md +86 -0
- package/dist/skills/pi-subagents/SKILL.md +18 -17
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +31 -9
- package/dist/skills/pi-subagents/references/execution-controls.md +95 -143
- package/dist/skills/pi-subagents/references/management-authoring-rpc.md +9 -12
- package/dist/skills/pi-subagents/references/multi-lane-orchestration.md +39 -0
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +45 -94
- package/dist/skills/planger/SKILL.md +166 -166
- package/dist/skills/ponytail/SKILL.md +120 -116
- package/dist/skills/ponytail-audit/SKILL.md +41 -42
- package/dist/skills/ponytail-debt/SKILL.md +44 -45
- package/dist/skills/ponytail-gain/SKILL.md +50 -51
- package/dist/skills/ponytail-help/SKILL.md +71 -70
- package/dist/skills/ponytail-review/SKILL.md +57 -58
- package/dist/skills/redesign-existing-projects/SKILL.md +179 -0
- package/dist/skills/selesai-handoff/SKILL.md +20 -20
- package/dist/skills/stitch-design-taste/SKILL.md +185 -0
- package/dist/skills/unlazy/CHANGELOG.md +100 -0
- package/dist/skills/unlazy/CONTRIBUTING.md +66 -0
- package/dist/skills/unlazy/LICENSE +21 -0
- package/dist/skills/unlazy/README.md +242 -0
- package/dist/skills/unlazy/SECURITY.md +70 -0
- package/dist/skills/unlazy/SKILL.md +100 -0
- package/dist/skills/unlazy/package.json +13 -0
- package/dist/skills/unlazy/references/dispatch.md +82 -0
- package/dist/skills/unlazy/references/gates.md +132 -0
- package/dist/skills/unlazy/references/method.md +49 -0
- package/dist/skills/unlazy/references/orchestration.md +99 -0
- package/dist/skills/unlazy/references/parallel.md +123 -0
- package/dist/skills/unlazy/references/token-economy.md +39 -0
- package/dist/skills/unlazy/research/validation-protocol.md +81 -0
- package/dist/skills/unlazy/scripts/dispatch-check.mjs +108 -0
- package/dist/skills/unlazy/scripts/gate-check.mjs +894 -0
- package/dist/skills/unlazy/scripts/gate-lint.mjs +178 -0
- package/dist/skills/unlazy/scripts/install-hooks.mjs +206 -0
- package/dist/skills/unlazy/scripts/lib/check-supervisor.mjs +46 -0
- package/dist/skills/unlazy/scripts/lib/dispatch.mjs +283 -0
- package/dist/skills/unlazy/scripts/lib/gates.mjs +629 -0
- package/dist/skills/unlazy/scripts/lib/process-tree.mjs +161 -0
- package/dist/skills/unlazy/scripts/lib/regex-worker.mjs +9 -0
- package/dist/skills/unlazy/scripts/stop-hook.mjs +174 -0
- package/dist/skills/unlazy/templates/PLAN.md +65 -0
- package/dist/skills/unlazy/templates/gates-leaf.md +51 -0
- package/dist/skills/unlazy/templates/gates-node.md +50 -0
- package/dist/skills/unlazy/tests/contract-tests.mjs +127 -0
- package/dist/skills/unlazy/tests/dispatch-tests.mjs +481 -0
- package/dist/skills/unlazy/tests/hardening-tests.mjs +903 -0
- package/dist/skills/unlazy/tests/lint-tests.mjs +242 -0
- package/dist/skills/unlazy/tests/run-tests.mjs +554 -0
- package/dist/skills/unlazy/tests/self-check.mjs +165 -0
- package/dist/skills/unlazy/tests/stress-tests.mjs +356 -0
- package/dist/skills/web-design-guidelines/SKILL.md +40 -0
- package/dist/skills/workflow-creation/SKILL.md +73 -73
- package/dist/themes/powerline-footer/theme.json +33 -33
- package/dist/utils/shell.d.ts +3 -0
- package/dist/utils/shell.js +17 -5
- package/docs/compaction.md +396 -396
- package/docs/containerization.md +111 -111
- package/docs/development.md +71 -71
- package/docs/docs.json +164 -164
- package/docs/environment-variables.md +86 -86
- package/docs/index.md +83 -83
- package/docs/json.md +82 -82
- package/docs/models.md +502 -502
- package/docs/packages.md +227 -227
- package/docs/plans/subagent-delegation/phase-0-correctness.md +264 -264
- package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +485 -485
- package/docs/plans/subagent-delegation/phase-2-context-controls.md +281 -281
- package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +361 -361
- package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +380 -380
- package/docs/prompt-templates.md +95 -95
- package/docs/providers.md +293 -293
- package/docs/sdk.md +1144 -1143
- package/docs/security.md +59 -59
- package/docs/session-format.md +414 -414
- package/docs/sessions.md +145 -145
- package/docs/settings.md +0 -5
- package/docs/shared-host-extensions.md +109 -109
- package/docs/shell-aliases.md +13 -13
- package/docs/skills.md +231 -231
- package/docs/terminal-setup.md +142 -142
- package/docs/termux.md +127 -127
- package/docs/themes.md +295 -295
- package/docs/tmux.md +63 -63
- package/docs/tui.md +927 -927
- package/docs/windows.md +17 -17
- package/examples/README.md +25 -25
- package/examples/extensions/README.md +211 -211
- package/examples/extensions/auto-commit-on-exit.ts +49 -49
- package/examples/extensions/bash-spawn-hook.ts +30 -30
- package/examples/extensions/bookmark.ts +50 -50
- package/examples/extensions/border-status-editor.ts +150 -150
- package/examples/extensions/built-in-tool-renderer.ts +249 -249
- package/examples/extensions/claude-rules.ts +86 -86
- package/examples/extensions/commands.ts +72 -72
- package/examples/extensions/confirm-destructive.ts +59 -59
- package/examples/extensions/custom-compaction.ts +130 -130
- package/examples/extensions/custom-footer.ts +64 -64
- package/examples/extensions/custom-header.ts +73 -73
- package/examples/extensions/custom-provider-anthropic/index.ts +610 -610
- package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -24
- package/examples/extensions/custom-provider-anthropic/package.json +19 -19
- package/examples/extensions/custom-provider-gitlab-duo/index.ts +404 -404
- package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -16
- package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -82
- package/examples/extensions/dirty-repo-guard.ts +56 -56
- package/examples/extensions/doom-overlay/README.md +46 -46
- package/examples/extensions/doom-overlay/doom/build/doom.js +21 -21
- package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
- package/examples/extensions/doom-overlay/doom/build.sh +152 -152
- package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -72
- package/examples/extensions/doom-overlay/doom-component.ts +132 -132
- package/examples/extensions/doom-overlay/doom-engine.ts +173 -173
- package/examples/extensions/doom-overlay/doom-keys.ts +104 -104
- package/examples/extensions/doom-overlay/index.ts +74 -74
- package/examples/extensions/doom-overlay/wad-finder.ts +51 -51
- package/examples/extensions/dynamic-resources/SKILL.md +8 -8
- package/examples/extensions/dynamic-resources/dynamic.json +79 -79
- package/examples/extensions/dynamic-resources/dynamic.md +5 -5
- package/examples/extensions/dynamic-resources/index.ts +15 -15
- package/examples/extensions/dynamic-tools.ts +74 -74
- package/examples/extensions/event-bus.ts +43 -43
- package/examples/extensions/file-trigger.ts +41 -41
- package/examples/extensions/git-checkpoint.ts +53 -53
- package/examples/extensions/git-merge-and-resolve.ts +115 -115
- package/examples/extensions/github-issue-autocomplete.ts +185 -185
- package/examples/extensions/gondolin/index.ts +531 -531
- package/examples/extensions/gondolin/package-lock.json +185 -185
- package/examples/extensions/gondolin/package.json +19 -19
- package/examples/extensions/handoff.ts +199 -199
- package/examples/extensions/hello.ts +26 -26
- package/examples/extensions/hidden-thinking-label.ts +53 -53
- package/examples/extensions/inline-bash.ts +94 -94
- package/examples/extensions/input-transform-streaming.ts +39 -39
- package/examples/extensions/input-transform.ts +43 -43
- package/examples/extensions/interactive-shell.ts +196 -196
- package/examples/extensions/mac-system-theme.ts +47 -47
- package/examples/extensions/message-renderer.ts +59 -59
- package/examples/extensions/minimal-mode.ts +426 -426
- package/examples/extensions/modal-editor.ts +85 -85
- package/examples/extensions/model-status.ts +31 -31
- package/examples/extensions/notify.ts +55 -55
- package/examples/extensions/overlay-qa-tests.ts +1450 -1450
- package/examples/extensions/overlay-test.ts +153 -153
- package/examples/extensions/permission-gate.ts +34 -34
- package/examples/extensions/pirate.ts +47 -47
- package/examples/extensions/plan-mode/README.md +66 -66
- package/examples/extensions/plan-mode/index.ts +390 -390
- package/examples/extensions/plan-mode/utils.ts +168 -168
- package/examples/extensions/preset.ts +436 -436
- package/examples/extensions/project-trust.ts +64 -64
- package/examples/extensions/prompt-customizer.ts +97 -97
- package/examples/extensions/protected-paths.ts +30 -30
- package/examples/extensions/provider-payload.ts +18 -18
- package/examples/extensions/qna.ts +122 -122
- package/examples/extensions/question.ts +285 -285
- package/examples/extensions/questionnaire.ts +448 -448
- package/examples/extensions/rainbow-editor.ts +88 -88
- package/examples/extensions/reload-runtime.ts +37 -37
- package/examples/extensions/rpc-demo.ts +118 -118
- package/examples/extensions/sandbox/index.ts +321 -321
- package/examples/extensions/sandbox/package-lock.json +92 -92
- package/examples/extensions/sandbox/package.json +19 -19
- package/examples/extensions/send-user-message.ts +97 -97
- package/examples/extensions/session-name.ts +27 -27
- package/examples/extensions/shutdown-command.ts +63 -63
- package/examples/extensions/snake.ts +343 -343
- package/examples/extensions/space-invaders.ts +560 -560
- package/examples/extensions/ssh.ts +220 -220
- package/examples/extensions/status-line.ts +32 -32
- package/examples/extensions/structured-output.ts +65 -65
- package/examples/extensions/subagent/README.md +175 -175
- package/examples/extensions/subagent/agents/planner.md +37 -37
- package/examples/extensions/subagent/agents/reviewer.md +35 -35
- package/examples/extensions/subagent/agents/scout.md +50 -50
- package/examples/extensions/subagent/agents/worker.md +24 -24
- package/examples/extensions/subagent/agents.ts +126 -126
- package/examples/extensions/subagent/index.ts +1015 -1015
- package/examples/extensions/subagent/prompts/implement-and-review.md +10 -10
- package/examples/extensions/subagent/prompts/implement.md +10 -10
- package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -9
- package/examples/extensions/summarize.ts +209 -209
- package/examples/extensions/system-prompt-header.ts +17 -17
- package/examples/extensions/tic-tac-toe.ts +1008 -1008
- package/examples/extensions/timed-confirm.ts +70 -70
- package/examples/extensions/titlebar-spinner.ts +58 -58
- package/examples/extensions/todo.ts +297 -297
- package/examples/extensions/tool-override.ts +144 -144
- package/examples/extensions/tools.ts +146 -146
- package/examples/extensions/trigger-compact.ts +50 -50
- package/examples/extensions/truncated-tool.ts +195 -195
- package/examples/extensions/widget-placement.ts +9 -9
- package/examples/extensions/with-deps/index.ts +32 -32
- package/examples/extensions/with-deps/package-lock.json +31 -31
- package/examples/extensions/with-deps/package.json +22 -22
- package/examples/extensions/working-indicator.ts +123 -123
- package/examples/extensions/working-message-test.ts +25 -25
- package/examples/rpc-extension-ui.ts +632 -632
- package/examples/sdk/01-minimal.ts +26 -26
- package/examples/sdk/02-custom-model.ts +53 -53
- package/examples/sdk/03-custom-prompt.ts +75 -75
- package/examples/sdk/04-skills.ts +55 -55
- package/examples/sdk/05-tools.ts +48 -48
- package/examples/sdk/06-extensions.ts +99 -99
- package/examples/sdk/07-context-files.ts +47 -47
- package/examples/sdk/08-prompt-templates.ts +51 -51
- package/examples/sdk/09-api-keys-and-oauth.ts +52 -52
- package/examples/sdk/10-settings.ts +53 -53
- package/examples/sdk/11-sessions.ts +52 -52
- package/examples/sdk/12-full-control.ts +79 -79
- package/examples/sdk/13-session-runtime.ts +67 -67
- package/examples/sdk/README.md +144 -144
- package/package.json +4 -4
- package/dist/core/tools/schema-prune.d.ts +0 -16
- package/dist/core/tools/schema-prune.js +0 -39
- package/dist/core/tools/schema-prune.test.js +0 -111
- package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +0 -172
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +0 -55
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +0 -61
- package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +0 -132
- package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +0 -181
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +0 -1354
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +0 -1581
- package/dist/extensions/pi-subagents/src/shared/env.ts +0 -16
- package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +0 -71
- package/dist/extensions/pi-subagents/src/slash/inline-subagents.ts +0 -136
- package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +0 -41
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +0 -466
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +0 -1859
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +0 -733
- package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +0 -63
- package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +0 -326
- package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +0 -103
- package/dist/extensions/pi-subagents/test/unit/inline-subagents.test.ts +0 -127
- package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +0 -73
- package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +0 -213
- package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +0 -65
- /package/dist/core/{tools/schema-prune.test.d.ts → extensions/loader.test.d.ts} +0 -0
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
tryImport,
|
|
28
28
|
} from "../support/helpers.ts";
|
|
29
29
|
import registerSubagentExtension from "../../src/extension/index.ts";
|
|
30
|
+
import { handleSubagentControlNotice } from "../../src/extension/control-notices.ts";
|
|
30
31
|
import { discoverAgents } from "../../src/agents/agents.ts";
|
|
31
32
|
import {
|
|
32
33
|
SUBAGENT_DELEGATION_REQUEST_EVENT,
|
|
@@ -36,10 +37,9 @@ import {
|
|
|
36
37
|
type SubagentDelegationResponse,
|
|
37
38
|
type SubagentDelegationStarted,
|
|
38
39
|
} from "../../src/api/delegation.ts";
|
|
39
|
-
import { CHAIN_RUNS_DIR, DIRS, INTERCOM_DETACH_REQUEST_EVENT, INTERCOM_DETACH_RESPONSE_EVENT, type AsyncStatus, type SubagentState } from "../../src/shared/types.ts";
|
|
40
|
+
import { CHAIN_RUNS_DIR, DIRS, INTERCOM_DETACH_REQUEST_EVENT, INTERCOM_DETACH_RESPONSE_EVENT, SUBAGENT_CONTROL_EVENT, TEMP_ARTIFACTS_DIR, type AsyncStatus, type ControlEvent, type SubagentState } from "../../src/shared/types.ts";
|
|
40
41
|
import { ACTIVE_RUN_INDEX_DIR } from "../../src/runs/background/active-run-index.ts";
|
|
41
|
-
import {
|
|
42
|
-
import { missionStatePath } from "../../src/missions/workflow-state.ts";
|
|
42
|
+
import { listAsyncRuns } from "../../src/runs/background/async-status.ts";
|
|
43
43
|
import { CHILD_WATCHDOG_STATUS_EVENT } from "../../src/watchdog/child-status.ts";
|
|
44
44
|
import { WAIT_TOOL_ENABLED_ENV } from "../../src/runs/background/wait-config.ts";
|
|
45
45
|
import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV } from "../../src/runs/shared/tool-budget.ts";
|
|
@@ -47,6 +47,7 @@ import { createRunFanoutBudget, encodeRunFanoutBudgetDescriptor, RUN_FANOUT_BUDG
|
|
|
47
47
|
import { MainWatchdogRuntime } from "../../src/watchdog/runtime.ts";
|
|
48
48
|
import { MAX_CHILD_PENDING_LINE_BYTES, MAX_CHILD_STDERR_BYTES } from "../../src/runs/shared/child-protocol.ts";
|
|
49
49
|
import {
|
|
50
|
+
SUBAGENT_CHILD_ENV,
|
|
50
51
|
SUBAGENT_FANOUT_CHILD_ENV,
|
|
51
52
|
SUBAGENT_STEER_ACK_DIR_ENV,
|
|
52
53
|
SUBAGENT_STEER_CAPABILITY_ENV,
|
|
@@ -58,6 +59,11 @@ import {
|
|
|
58
59
|
SUBAGENT_PARENT_RUN_ID_ENV,
|
|
59
60
|
} from "../../src/runs/shared/pi-args.ts";
|
|
60
61
|
import { createNestedRoute, nestedRouteEnv, parseNestedEventRecords } from "../../src/runs/shared/nested-events.ts";
|
|
62
|
+
import { resolveMissionStoreLocation } from "../../src/missions/store.ts";
|
|
63
|
+
import { missionStatePath } from "../../src/missions/workflow-state.ts";
|
|
64
|
+
import { discardPreservedWorktrees } from "../../src/runs/shared/parallel-handoff.ts";
|
|
65
|
+
import { resolveAsyncResumeTarget } from "../../src/runs/background/async-resume.ts";
|
|
66
|
+
import { clearExclusions } from "../../src/runs/shared/model-exclusions.ts";
|
|
61
67
|
|
|
62
68
|
interface ModelAttempt {
|
|
63
69
|
success?: boolean;
|
|
@@ -117,6 +123,7 @@ interface RunSyncResult {
|
|
|
117
123
|
skillsWarning?: string;
|
|
118
124
|
attemptedModels?: string[];
|
|
119
125
|
modelAttempts?: ModelAttempt[];
|
|
126
|
+
contextOverflow?: boolean;
|
|
120
127
|
usage: { turns: number; input: number; output: number };
|
|
121
128
|
progress: ProgressSummary;
|
|
122
129
|
controlEvents?: Array<{ type?: string; message: string; reason?: string; turns?: number; tokens?: number; currentPath?: string; recentFailureSummary?: string }>;
|
|
@@ -276,6 +283,10 @@ function escapeRegExp(value: string): string {
|
|
|
276
283
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
277
284
|
}
|
|
278
285
|
|
|
286
|
+
function pathContainsSegments(filePath: string, ...segments: string[]): boolean {
|
|
287
|
+
return segments.every((segment) => filePath.split(path.sep).includes(segment));
|
|
288
|
+
}
|
|
289
|
+
|
|
279
290
|
function writePackageSkill(packageRoot: string, skillName: string): void {
|
|
280
291
|
const skillDir = path.join(packageRoot, "skills", skillName);
|
|
281
292
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
@@ -293,7 +304,9 @@ function writePackageSkill(packageRoot: string, skillName: string): void {
|
|
|
293
304
|
|
|
294
305
|
describe("single sync execution", { skip: !available ? "pi packages not available" : undefined }, () => {
|
|
295
306
|
let tempDir: string;
|
|
307
|
+
let agentDir: string;
|
|
296
308
|
let mockPi: MockPi;
|
|
309
|
+
let previousAgentDir: string | undefined;
|
|
297
310
|
|
|
298
311
|
before(() => {
|
|
299
312
|
mockPi = createMockPi();
|
|
@@ -306,10 +319,18 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
306
319
|
|
|
307
320
|
beforeEach(() => {
|
|
308
321
|
tempDir = createTempDir();
|
|
322
|
+
agentDir = createTempDir("pi-subagent-agent-");
|
|
323
|
+
previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
324
|
+
process.env.SELESAI_CODING_AGENT_DIR = agentDir;
|
|
309
325
|
mockPi.reset();
|
|
326
|
+
clearExclusions();
|
|
310
327
|
});
|
|
311
328
|
|
|
312
329
|
afterEach(() => {
|
|
330
|
+
clearExclusions();
|
|
331
|
+
if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
332
|
+
else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
|
|
333
|
+
removeTempDir(agentDir);
|
|
313
334
|
removeTempDir(tempDir);
|
|
314
335
|
});
|
|
315
336
|
|
|
@@ -345,6 +366,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
345
366
|
workflowControllers?: Map<string, AbortController>,
|
|
346
367
|
handleScheduledRunAction?: Parameters<typeof createSubagentExecutor>[0]["handleScheduledRunAction"],
|
|
347
368
|
piEvents = createEventBus(),
|
|
369
|
+
discoverAgentsForCwd?: (cwd: string) => typeof agents,
|
|
348
370
|
) {
|
|
349
371
|
return createSubagentExecutor!({
|
|
350
372
|
pi: { events: piEvents, getSessionName: () => undefined },
|
|
@@ -362,7 +384,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
362
384
|
tempArtifactsDir: tempDir,
|
|
363
385
|
getSubagentSessionRoot: () => path.join(tempDir, ".pi-subagents", "sessions"),
|
|
364
386
|
expandTilde: (value: string) => value,
|
|
365
|
-
discoverAgents: () => ({ agents }),
|
|
387
|
+
discoverAgents: (cwd: string) => ({ agents: discoverAgentsForCwd ? discoverAgentsForCwd(cwd) : agents }),
|
|
366
388
|
allowMutatingManagementActions,
|
|
367
389
|
...(handleScheduledRunAction ? { handleScheduledRunAction } : {}),
|
|
368
390
|
});
|
|
@@ -384,20 +406,188 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
384
406
|
assert.equal(output, "Hello from mock agent");
|
|
385
407
|
});
|
|
386
408
|
|
|
387
|
-
it("
|
|
409
|
+
it("runs public structured single-child requests directly", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
410
|
+
mockPi.onCall({ output: "Structured child completed" });
|
|
388
411
|
const executor = makeExecutor([makeAgent("echo")]);
|
|
389
412
|
|
|
390
413
|
const result = await executor.executePublic(
|
|
391
|
-
"single
|
|
392
|
-
{
|
|
414
|
+
"structured-single",
|
|
415
|
+
{ agent: "echo", task: "Run through workflow", async: false, context: "fresh" },
|
|
416
|
+
new AbortController().signal,
|
|
417
|
+
undefined,
|
|
418
|
+
makeMinimalCtx(tempDir),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "");
|
|
422
|
+
assert.equal(result.details.mode, "single");
|
|
423
|
+
assert.equal(mockPi.callCount(), 1);
|
|
424
|
+
assert.doesNotMatch(result.content[0]?.text ?? "", /Console:/);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("keeps public structured children alive when tool results backfill without execution_end", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
428
|
+
mockPi.onCall({
|
|
429
|
+
steps: [
|
|
430
|
+
{ jsonl: [{ type: "tool_execution_start", toolCallId: "bash-1", toolName: "bash", args: { command: "echo PROBE_OK" } }] },
|
|
431
|
+
{ delay: 25, jsonl: [
|
|
432
|
+
{ type: "tool_result_end", message: { role: "toolResult", toolCallId: "bash-1", toolName: "bash", isError: false, content: [{ type: "text", text: "PROBE_OK" }] } },
|
|
433
|
+
events.assistantMessage("PROBE_OK"),
|
|
434
|
+
] },
|
|
435
|
+
],
|
|
436
|
+
keepAliveAfterFinalMessageMs: 400,
|
|
437
|
+
});
|
|
438
|
+
const executor = makeExecutor([makeAgent("bash-worker")]);
|
|
439
|
+
|
|
440
|
+
const result = await executor.executePublic(
|
|
441
|
+
"structured-single-tool-backfill",
|
|
442
|
+
{ agent: "bash-worker", task: "Run exactly one tool: bash with command echo PROBE_OK.", async: false, toolTimeoutMs: 100, timeoutMs: 5_000 },
|
|
443
|
+
new AbortController().signal,
|
|
444
|
+
undefined,
|
|
445
|
+
makeMinimalCtx(tempDir),
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
449
|
+
assert.match(result.content[0]?.text ?? "", /PROBE_OK/);
|
|
450
|
+
assert.equal(result.details.results[0]?.timedOut, undefined);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("keeps public structured single-child calls foreground when async is disabled by default", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
454
|
+
mockPi.onCall({ output: "Structured child used the foreground default" });
|
|
455
|
+
const executor = makeExecutor([makeAgent("echo")], {}, false);
|
|
456
|
+
|
|
457
|
+
const result = await executor.executePublic(
|
|
458
|
+
"structured-single-foreground-default",
|
|
459
|
+
{ agent: "echo", task: "Run through workflow" },
|
|
460
|
+
new AbortController().signal,
|
|
461
|
+
undefined,
|
|
462
|
+
makeMinimalCtx(tempDir),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "");
|
|
466
|
+
assert.match(result.content[0]?.text ?? "", /Structured child used the foreground default/);
|
|
467
|
+
assert.equal(result.details.asyncId, undefined);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("does not override structured single output unless configured by the agent", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
471
|
+
for (const params of [
|
|
472
|
+
{ agent: "echo", task: "Use the task output path", async: false },
|
|
473
|
+
{ agent: "echo", task: "Disable file output", output: false, async: false },
|
|
474
|
+
] as const) {
|
|
475
|
+
mockPi.onCall({ output: "Structured child completed" });
|
|
476
|
+
const result = await makeExecutor([makeAgent("echo")]).executePublic(
|
|
477
|
+
"structured-single-output",
|
|
478
|
+
params,
|
|
479
|
+
new AbortController().signal,
|
|
480
|
+
undefined,
|
|
481
|
+
makeMinimalCtx(tempDir),
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
485
|
+
assert.doesNotMatch(readCallArgs().join("\n"), /This path is authoritative for this run/);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
mockPi.onCall({ output: "Agent report" });
|
|
489
|
+
const configuredPath = path.join(tempDir, "agent-report.md");
|
|
490
|
+
const configured = await makeExecutor([makeAgent("echo", { output: configuredPath })]).executePublic(
|
|
491
|
+
"structured-single-agent-output",
|
|
492
|
+
{ agent: "echo", task: "Use agent output", async: false },
|
|
493
|
+
new AbortController().signal,
|
|
494
|
+
undefined,
|
|
495
|
+
makeMinimalCtx(tempDir),
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
assert.equal(configured.isError, undefined, configured.content[0]?.text ?? "workflow failed");
|
|
499
|
+
const configuredTask = readCallArgs().join("\n");
|
|
500
|
+
assert.match(configuredTask, new RegExp(escapeRegExp(configuredPath)));
|
|
501
|
+
assert.match(configuredTask, /This path is authoritative for this run/);
|
|
502
|
+
assert.equal(fs.readFileSync(configuredPath, "utf-8"), "Agent report");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("does not inject a workflow child output without an aggregate or explicit output", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
506
|
+
mockPi.onCall({ output: "Workflow child completed" });
|
|
507
|
+
const result = await makeExecutor([makeAgent("echo")]).execute(
|
|
508
|
+
"workflow-omitted-output",
|
|
509
|
+
{ async: false, workflowScript: `return runs.run("main", { agent: "echo", task: "Use the task output path" });` },
|
|
510
|
+
new AbortController().signal,
|
|
511
|
+
undefined,
|
|
512
|
+
makeMinimalCtx(tempDir),
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
516
|
+
assert.doesNotMatch(readCallArgs().join("\n"), /This path is authoritative for this run/);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("resolves workflow child profile context from its agent default", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
520
|
+
mockPi.onCall({ output: "Workflow child completed" });
|
|
521
|
+
const result = await makeExecutor([makeAgent("echo", { defaultContext: "fresh" })], { defaultSubagentContext: "fork" }).execute(
|
|
522
|
+
"workflow-profile-context",
|
|
523
|
+
{ async: false, workflowScript: `return runs.run("main", { agent: "echo", task: "Use profile context", context: "profile" });` },
|
|
524
|
+
new AbortController().signal,
|
|
525
|
+
undefined,
|
|
526
|
+
makeMinimalCtx(tempDir),
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
530
|
+
assert.equal(result.details?.results?.[0]?.context, "fresh");
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("reports a user-requested foreground detach without supervisor guidance", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
534
|
+
mockPi.onCall({ steps: [{ delay: 500, jsonl: [events.assistantMessage("completed after user detach")] }] });
|
|
535
|
+
const state: SubagentState = {
|
|
536
|
+
baseCwd: tempDir,
|
|
537
|
+
currentSessionId: null,
|
|
538
|
+
asyncJobs: new Map(),
|
|
539
|
+
foregroundControls: new Map(),
|
|
540
|
+
lastForegroundControlId: null,
|
|
541
|
+
};
|
|
542
|
+
const executor = createSubagentExecutor!({
|
|
543
|
+
pi: { events: createEventBus(), getSessionName: () => undefined },
|
|
544
|
+
state,
|
|
545
|
+
config: {},
|
|
546
|
+
asyncByDefault: false,
|
|
547
|
+
tempArtifactsDir: tempDir,
|
|
548
|
+
getSubagentSessionRoot: () => path.join(tempDir, ".pi-subagents", "sessions"),
|
|
549
|
+
expandTilde: (value: string) => value,
|
|
550
|
+
discoverAgents: () => ({ agents: [makeAgent("echo")] }),
|
|
551
|
+
allowMutatingManagementActions: true,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const pending = executor.execute(
|
|
555
|
+
"user-detach-guidance",
|
|
556
|
+
{ agent: "echo", task: "Keep working", async: false },
|
|
393
557
|
new AbortController().signal,
|
|
394
558
|
undefined,
|
|
395
559
|
makeMinimalCtx(tempDir),
|
|
396
560
|
);
|
|
561
|
+
let control = state.lastForegroundControlId ? state.foregroundControls.get(state.lastForegroundControlId) : undefined;
|
|
562
|
+
for (let attempt = 0; attempt < 100 && !control?.detach; attempt++) {
|
|
563
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
564
|
+
control = state.lastForegroundControlId ? state.foregroundControls.get(state.lastForegroundControlId) : undefined;
|
|
565
|
+
}
|
|
566
|
+
assert.ok(control?.detach, "foreground detach control should become available");
|
|
567
|
+
assert.equal(control.detach(), true);
|
|
568
|
+
|
|
569
|
+
const result = await pending;
|
|
570
|
+
const text = result.content.map((part) => part.type === "text" ? part.text : "").join("\n");
|
|
571
|
+
assert.equal(result.details.results[0]?.detachedReason, "user request");
|
|
572
|
+
assert.match(text, /Detached at user request/);
|
|
573
|
+
assert.match(text, /subagent_wait\(\{ id: "[^"]+", nonBlocking: true \}\)/);
|
|
574
|
+
assert.doesNotMatch(text, /intercom coordination|supervisor request|Wait with subagent_wait/);
|
|
575
|
+
assert.doesNotMatch(text, /subagent_wait\(\{ id: "[^"]+" \}\)/);
|
|
576
|
+
|
|
577
|
+
let terminalChild = state.foregroundRuns?.get(control.runId)?.children[0];
|
|
578
|
+
for (let attempt = 0; attempt < 250 && terminalChild?.status !== "completed"; attempt++) {
|
|
579
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
580
|
+
terminalChild = state.foregroundRuns?.get(control.runId)?.children[0];
|
|
581
|
+
}
|
|
582
|
+
assert.equal(terminalChild?.status, "completed", "detached child should reach its terminal callback before teardown");
|
|
583
|
+
assert.equal(terminalChild.finalOutput, "completed after user detach");
|
|
584
|
+
});
|
|
397
585
|
|
|
586
|
+
it("rejects action='single' with execution fields", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
587
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
588
|
+
const result = await executor.executePublic("single-alias", { action: "single", agent: "echo", task: "work" }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
|
|
398
589
|
assert.equal(result.isError, true);
|
|
399
|
-
assert.match(result.content[0]?.text ?? "", /is not
|
|
400
|
-
assert.equal(mockPi.callCount(), 0);
|
|
590
|
+
assert.match(result.content[0]?.text ?? "", /action='single' is not supported/);
|
|
401
591
|
});
|
|
402
592
|
|
|
403
593
|
it("rejects internal fan-out fields from public workflows", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
@@ -413,6 +603,53 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
413
603
|
assert.equal(mockPi.callCount(), 0);
|
|
414
604
|
});
|
|
415
605
|
|
|
606
|
+
it("runs isolation none outside Git and keeps worktree isolation strict", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
607
|
+
assert.equal(fs.existsSync(path.join(tempDir, ".git")), false);
|
|
608
|
+
mockPi.onCall({ output: "shared cwd" });
|
|
609
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
610
|
+
const script = `return runs.run("main", { agent: "echo", task: "work" })`;
|
|
611
|
+
|
|
612
|
+
const shared = await executor.executePublic(
|
|
613
|
+
"isolation-none",
|
|
614
|
+
{ async: false, isolation: "none", workflowScript: script },
|
|
615
|
+
new AbortController().signal,
|
|
616
|
+
undefined,
|
|
617
|
+
makeMinimalCtx(tempDir),
|
|
618
|
+
);
|
|
619
|
+
assert.equal(shared.isError, undefined, shared.content[0]?.text ?? "shared workflow failed");
|
|
620
|
+
|
|
621
|
+
const isolated = await executor.executePublic(
|
|
622
|
+
"isolation-worktree",
|
|
623
|
+
{ async: false, isolation: "worktree", workflowScript: script },
|
|
624
|
+
new AbortController().signal,
|
|
625
|
+
undefined,
|
|
626
|
+
makeMinimalCtx(tempDir),
|
|
627
|
+
);
|
|
628
|
+
assert.equal(isolated.isError, true);
|
|
629
|
+
assert.match(isolated.content[0]?.text ?? "", /worktree isolation requires a git repository/i);
|
|
630
|
+
assert.equal(mockPi.callCount(), 1);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("allows schedule.create to carry the required workflowScript target", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
634
|
+
let forwarded;
|
|
635
|
+
const executor = makeExecutor([makeAgent("echo")], {}, false, undefined, true, new Map(), undefined, async (params) => {
|
|
636
|
+
forwarded = params;
|
|
637
|
+
return { content: [{ type: "text", text: "created" }], details: { mode: "management", results: [] } };
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
const result = await executor.execute(
|
|
641
|
+
"schedule-create",
|
|
642
|
+
{ action: "schedule.create", id: "nightly", every: "1h", workflowScript: "return runs.run('main', { agent: 'echo' })" },
|
|
643
|
+
new AbortController().signal,
|
|
644
|
+
undefined,
|
|
645
|
+
makeMinimalCtx(tempDir),
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
assert.equal(result.isError, undefined);
|
|
649
|
+
assert.equal(result.content[0]?.text, "created");
|
|
650
|
+
assert.equal(forwarded?.workflowScript, "return runs.run('main', { agent: 'echo' })");
|
|
651
|
+
});
|
|
652
|
+
|
|
416
653
|
it("starts workflow scripts asynchronously with a portable internal run id", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
417
654
|
mockPi.onCall({ echoEnv: [SUBAGENT_STEER_INBOX_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_ACK_DIR_ENV] });
|
|
418
655
|
const asyncJobs: SubagentState["asyncJobs"] = new Map();
|
|
@@ -420,6 +657,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
420
657
|
const workflowCwd = path.join(tempDir, "workflow-cwd");
|
|
421
658
|
fs.mkdirSync(workflowCwd);
|
|
422
659
|
const toolCallId = "call_demo|fc_demo";
|
|
660
|
+
const context = makeMinimalCtx(tempDir);
|
|
661
|
+
context.sessionManager.getSessionFile = () => path.join(tempDir, "parent-session.jsonl");
|
|
423
662
|
|
|
424
663
|
const result = await executor.execute(
|
|
425
664
|
toolCallId,
|
|
@@ -430,7 +669,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
430
669
|
},
|
|
431
670
|
new AbortController().signal,
|
|
432
671
|
undefined,
|
|
433
|
-
|
|
672
|
+
context,
|
|
434
673
|
);
|
|
435
674
|
|
|
436
675
|
assert.equal(result.isError, undefined);
|
|
@@ -444,11 +683,12 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
444
683
|
assert.equal(path.basename(result.details.asyncDir!), workflowRunId);
|
|
445
684
|
assert.equal(asyncJobs.has(workflowRunId), true);
|
|
446
685
|
assert.equal(asyncJobs.get(workflowRunId)?.cwd, workflowCwd);
|
|
686
|
+
assert.equal(asyncJobs.get(workflowRunId)?.sessionRoot, path.join(tempDir, ".pi-subagents", "sessions"));
|
|
447
687
|
assert.equal(asyncJobs.has(toolCallId), false);
|
|
448
688
|
assert.equal(fs.existsSync(path.join(DIRS.async, toolCallId)), false);
|
|
449
689
|
assert.match(result.content[0]?.text ?? "", /Async workflow/);
|
|
450
690
|
const statusPath = path.join(result.details.asyncDir!, "status.json");
|
|
451
|
-
let status: { runId?: string; toolCallId?: string; cwd?: string; state?: string; steps?: Array<{ agent?: string; label?: string; workflowKey?: string; parentWorkflowRunId?: string }>; workflow?: { value?: unknown; emits?: unknown[]; trace?: Array<{ key?: string; agent?: string; state?: string }> } } = {};
|
|
691
|
+
let status: { runId?: string; toolCallId?: string; cwd?: string; sessionRoot?: string; state?: string; steps?: Array<{ agent?: string; label?: string; workflowKey?: string; parentWorkflowRunId?: string }>; workflow?: { value?: unknown; emits?: unknown[]; trace?: Array<{ key?: string; agent?: string; state?: string }> } } = {};
|
|
452
692
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
453
693
|
status = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
|
|
454
694
|
if (status.state === "complete" || status.state === "failed") break;
|
|
@@ -458,6 +698,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
458
698
|
assert.equal(status.runId, workflowRunId);
|
|
459
699
|
assert.equal(status.toolCallId, toolCallId);
|
|
460
700
|
assert.equal(status.cwd, workflowCwd);
|
|
701
|
+
assert.equal(status.sessionRoot, path.join(tempDir, ".pi-subagents", "sessions"));
|
|
461
702
|
assert.equal(status.steps?.length, 1);
|
|
462
703
|
assert.deepEqual(status.steps?.map(({ agent, label, workflowKey }) => ({ agent, label, workflowKey })), [
|
|
463
704
|
{ agent: "echo", label: "work", workflowKey: "work" },
|
|
@@ -479,7 +720,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
479
720
|
{ key: "work", state: "completed" },
|
|
480
721
|
]);
|
|
481
722
|
const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
|
|
482
|
-
const persistedResult = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { id?: string; runId?: string; toolCallId?: string; agent?: string; cwd?: string; summary?: string; workflow?: { value?: unknown }; results?: Array<{ agent?: string; workflowKey?: string; runId?: string; output?: string }> };
|
|
723
|
+
const persistedResult = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { id?: string; runId?: string; toolCallId?: string; agent?: string; cwd?: string; summary?: string; workflow?: { value?: unknown; receipt?: unknown }; workflowReceipt?: { path?: string; receipt?: { workflowRunId?: string; entries?: Record<string, { key?: string; agent?: string; latestRunId?: string; resumability?: { state?: string }; continuation?: { runIds?: string[] } }> } }; results?: Array<{ agent?: string; workflowKey?: string; runId?: string; output?: string }> };
|
|
483
724
|
assert.equal(persistedResult.id, workflowRunId);
|
|
484
725
|
assert.equal(persistedResult.runId, workflowRunId);
|
|
485
726
|
assert.equal(persistedResult.toolCallId, toolCallId);
|
|
@@ -495,6 +736,16 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
495
736
|
assert.equal(fs.existsSync(path.join(result.details.asyncDir!, "control", "workflow-foreground", persistedResult.results?.[0]?.runId ?? "missing")), false);
|
|
496
737
|
assert.match(persistedResult.summary ?? "", /Return: \{\n "answer": 42\n\}/);
|
|
497
738
|
assert.deepEqual(persistedResult.workflow?.value, { answer: 42 });
|
|
739
|
+
assert.equal(persistedResult.workflow?.receipt, undefined, "status/result workflow projection must stay receipt-free");
|
|
740
|
+
assert.equal(persistedResult.workflowReceipt?.path, path.join(result.details.asyncDir!, "workflow-receipt.json"));
|
|
741
|
+
assert.equal(persistedResult.workflowReceipt?.receipt?.workflowRunId, workflowRunId);
|
|
742
|
+
assert.equal(persistedResult.workflowReceipt?.receipt?.entries?.work?.key, "work");
|
|
743
|
+
assert.equal(persistedResult.workflowReceipt?.receipt?.entries?.work?.agent, "echo");
|
|
744
|
+
assert.equal(persistedResult.workflowReceipt?.receipt?.entries?.work?.latestRunId, persistedResult.results?.[0]?.runId);
|
|
745
|
+
assert.equal(fs.existsSync(path.join(DIRS.async, persistedResult.results?.[0]?.runId ?? "missing")), false);
|
|
746
|
+
assert.deepEqual(persistedResult.workflowReceipt?.receipt?.entries?.work?.continuation?.runIds, [persistedResult.results?.[0]?.runId]);
|
|
747
|
+
assert.equal(persistedResult.workflowReceipt?.receipt?.entries?.work?.resumability?.state, "resumable");
|
|
748
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(persistedResult.workflowReceipt!.path!, "utf-8")), persistedResult.workflowReceipt?.receipt);
|
|
498
749
|
assert.equal(fs.existsSync(path.join(DIRS.results, `${toolCallId}.json`)), false);
|
|
499
750
|
fs.rmSync(result.details.asyncDir!, { recursive: true, force: true });
|
|
500
751
|
fs.rmSync(resultPath, { force: true });
|
|
@@ -535,153 +786,439 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
535
786
|
assert.equal(fs.readFileSync(markerPath, "utf-8"), "started");
|
|
536
787
|
assert.equal(mockPi.callCount(), 0);
|
|
537
788
|
|
|
538
|
-
fs.rmSync(started.details.asyncDir!, { recursive: true, force: true });
|
|
789
|
+
fs.rmSync(started.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
539
790
|
fs.rmSync(workflowResultPath, { force: true });
|
|
540
791
|
});
|
|
541
792
|
|
|
542
|
-
it("
|
|
793
|
+
it("awaits omitted external CLI workflow children through their async result", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
794
|
+
const markerPath = path.join(tempDir, "external-awaited-started");
|
|
543
795
|
const executor = makeExecutor([
|
|
544
796
|
makeAgent("external", {
|
|
545
|
-
runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write(
|
|
797
|
+
runner: { type: "external-cli", command: process.execPath, args: ["-e", `require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "started"); process.stdout.write("awaited external result")`] },
|
|
798
|
+
model: "mock/default-model",
|
|
799
|
+
modelSource: { type: "subagents.defaultModel", scope: "user", path: "/settings.json", model: "mock/default-model" },
|
|
546
800
|
}),
|
|
547
801
|
]);
|
|
548
802
|
const result = await executor.execute(
|
|
549
|
-
"external-
|
|
550
|
-
{ agent: "external", task: "Run external"
|
|
803
|
+
"external-awaited-workflow",
|
|
804
|
+
{ workflowScript: `return await runs.run("external", { agent: "external", task: "Run external" });`, async: false },
|
|
551
805
|
new AbortController().signal,
|
|
552
806
|
undefined,
|
|
553
|
-
makeMinimalCtx(tempDir),
|
|
807
|
+
{ ...makeMinimalCtx(tempDir), model: { provider: "mock", id: "parent-model" } },
|
|
554
808
|
);
|
|
555
809
|
|
|
556
|
-
assert.equal(result.isError,
|
|
557
|
-
assert.
|
|
810
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
811
|
+
assert.equal(result.details.mode, "workflow");
|
|
812
|
+
assert.match(result.content[0]?.text ?? "", /awaited external result/);
|
|
813
|
+
assert.equal(fs.readFileSync(markerPath, "utf-8"), "started");
|
|
558
814
|
assert.equal(mockPi.callCount(), 0);
|
|
559
815
|
});
|
|
560
816
|
|
|
561
|
-
it("
|
|
817
|
+
it("starts omitted external CLI single-child calls in async mode", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
818
|
+
const markerPath = path.join(tempDir, "external-single-omitted-async-started");
|
|
562
819
|
const executor = makeExecutor([
|
|
563
820
|
makeAgent("external", {
|
|
564
|
-
runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write(
|
|
565
|
-
model: "mock/
|
|
821
|
+
runner: { type: "external-cli", command: process.execPath, args: ["-e", `require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "started"); process.stdout.write("single async result")`] },
|
|
822
|
+
model: "mock/default-model",
|
|
566
823
|
modelSource: { type: "subagents.defaultModel", scope: "user", path: "/settings.json", model: "mock/default-model" },
|
|
567
824
|
}),
|
|
568
825
|
]);
|
|
569
826
|
const result = await executor.execute(
|
|
570
|
-
"external-
|
|
571
|
-
{ agent: "external", task: "Run external"
|
|
827
|
+
"external-single-omitted-async",
|
|
828
|
+
{ agent: "external", task: "Run external" },
|
|
572
829
|
new AbortController().signal,
|
|
573
830
|
undefined,
|
|
574
|
-
makeMinimalCtx(tempDir),
|
|
831
|
+
{ ...makeMinimalCtx(tempDir), model: { provider: "mock", id: "parent-model" } },
|
|
575
832
|
);
|
|
576
833
|
|
|
577
|
-
assert.equal(result.isError,
|
|
578
|
-
assert.
|
|
834
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "launch failed");
|
|
835
|
+
assert.ok(result.details.asyncId);
|
|
836
|
+
assert.match(result.content[0]?.text ?? "", /Async: external/);
|
|
837
|
+
for (let attempt = 0; attempt < 100 && !fs.existsSync(markerPath); attempt++) {
|
|
838
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
839
|
+
}
|
|
840
|
+
assert.equal(fs.readFileSync(markerPath, "utf-8"), "started");
|
|
579
841
|
assert.equal(mockPi.callCount(), 0);
|
|
842
|
+
|
|
843
|
+
const resultPath = path.join(DIRS.results, `${result.details.asyncId}.json`);
|
|
844
|
+
let runResult: { state?: string } = {};
|
|
845
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
846
|
+
if (fs.existsSync(resultPath)) runResult = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
847
|
+
if (runResult.state === "complete" || runResult.state === "failed") break;
|
|
848
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
849
|
+
}
|
|
850
|
+
assert.equal(runResult.state, "complete");
|
|
851
|
+
|
|
852
|
+
fs.rmSync(result.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
853
|
+
fs.rmSync(resultPath, { force: true });
|
|
580
854
|
});
|
|
581
855
|
|
|
582
|
-
it("
|
|
856
|
+
it("lets explicit fast false opt out external CLI agents from inherited fast mode", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
857
|
+
const markerPath = path.join(tempDir, "external-fast-false-started");
|
|
583
858
|
const executor = makeExecutor([
|
|
584
859
|
makeAgent("external", {
|
|
585
|
-
|
|
586
|
-
|
|
860
|
+
fast: true,
|
|
861
|
+
runner: { type: "external-cli", command: process.execPath, args: ["-e", `require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "started"); process.stdout.write("external fast false result")`] },
|
|
587
862
|
}),
|
|
588
863
|
]);
|
|
589
|
-
|
|
590
|
-
|
|
864
|
+
|
|
865
|
+
const rejected = await executor.execute(
|
|
866
|
+
"external-fast-inherited",
|
|
591
867
|
{ agent: "external", task: "Run external", async: true },
|
|
592
868
|
new AbortController().signal,
|
|
593
869
|
undefined,
|
|
594
870
|
makeMinimalCtx(tempDir),
|
|
595
871
|
);
|
|
596
|
-
|
|
597
|
-
assert.
|
|
598
|
-
assert.
|
|
599
|
-
assert.equal(mockPi.callCount(), 0);
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
it("projects live child activity into async workflow status", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
603
|
-
mockPi.onCall({
|
|
604
|
-
steps: [
|
|
605
|
-
{ jsonl: [events.toolStart("read", { path: "src/example.ts" })] },
|
|
606
|
-
{ delay: 2_500, jsonl: [events.toolEnd("read"), events.toolResult("read", "contents")] },
|
|
607
|
-
{ jsonl: [events.assistantMessage("Done")] },
|
|
608
|
-
],
|
|
609
|
-
});
|
|
610
|
-
const asyncJobs: SubagentState["asyncJobs"] = new Map();
|
|
611
|
-
const executor = makeExecutor([makeAgent("echo")], {
|
|
612
|
-
control: {
|
|
613
|
-
enabled: true,
|
|
614
|
-
needsAttentionAfterMs: 100,
|
|
615
|
-
activeNoticeAfterMs: 100,
|
|
616
|
-
activeNoticeAfterTurns: 999_999,
|
|
617
|
-
activeNoticeAfterTokens: 999_999,
|
|
618
|
-
notifyOn: ["active_long_running", "needs_attention"],
|
|
619
|
-
notifyChannels: ["event"],
|
|
620
|
-
},
|
|
621
|
-
}, false, undefined, true, asyncJobs);
|
|
872
|
+
assert.equal(rejected.isError, true);
|
|
873
|
+
assert.match(rejected.content[0]?.text ?? "", /does not support fast mode/);
|
|
874
|
+
assert.equal(fs.existsSync(markerPath), false);
|
|
622
875
|
|
|
623
876
|
const result = await executor.execute(
|
|
624
|
-
"
|
|
625
|
-
{
|
|
877
|
+
"external-fast-false",
|
|
878
|
+
{ agent: "external", task: "Run external", async: true, fast: false },
|
|
626
879
|
new AbortController().signal,
|
|
627
880
|
undefined,
|
|
628
881
|
makeMinimalCtx(tempDir),
|
|
629
882
|
);
|
|
630
|
-
const { asyncId: workflowRunId, asyncDir } = result.details;
|
|
631
|
-
assert.ok(workflowRunId);
|
|
632
|
-
assert.ok(asyncDir);
|
|
633
|
-
const statusPath = path.join(asyncDir, "status.json");
|
|
634
|
-
const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
|
|
635
|
-
const activeMarkerPath = path.join(DIRS.async, ACTIVE_RUN_INDEX_DIR, workflowRunId);
|
|
636
|
-
assert.equal(fs.existsSync(activeMarkerPath), true);
|
|
637
|
-
let liveStatus: AsyncStatus | undefined;
|
|
638
|
-
const activityDeadline = Date.now() + 5_000;
|
|
639
|
-
while (Date.now() < activityDeadline && !fs.existsSync(resultPath)) {
|
|
640
|
-
const candidate = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
641
|
-
if (candidate.activityState === "needs_attention" && candidate.steps?.[0]?.currentTool === "read") {
|
|
642
|
-
liveStatus = candidate;
|
|
643
|
-
break;
|
|
644
|
-
}
|
|
645
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
646
|
-
}
|
|
647
883
|
|
|
648
|
-
assert.
|
|
649
|
-
assert.
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
assert.equal(
|
|
654
|
-
assert.equal(
|
|
655
|
-
assert.equal(liveStatus.steps?.[0]?.agent, "echo");
|
|
656
|
-
assert.match(liveStatus.steps?.[0]?.sessionFile ?? "", /session\.jsonl$/);
|
|
657
|
-
assert.equal(fs.existsSync(liveStatus.steps?.[0]?.sessionFile ?? ""), true);
|
|
658
|
-
assert.equal(liveStatus.steps?.[0]?.activityState, "needs_attention");
|
|
659
|
-
assert.equal(typeof liveStatus.steps?.[0]?.lastActivityAt, "number");
|
|
660
|
-
assert.equal(liveStatus.steps?.[0]?.toolCount, 1);
|
|
661
|
-
assert.equal(asyncJobs.get(workflowRunId)?.activityState, "needs_attention");
|
|
662
|
-
assert.equal(asyncJobs.get(workflowRunId)?.steps?.[0]?.currentTool, "read");
|
|
884
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "launch failed");
|
|
885
|
+
assert.ok(result.details.asyncId);
|
|
886
|
+
for (let attempt = 0; attempt < 100 && !fs.existsSync(markerPath); attempt++) {
|
|
887
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
888
|
+
}
|
|
889
|
+
assert.equal(fs.readFileSync(markerPath, "utf-8"), "started");
|
|
890
|
+
assert.equal(mockPi.callCount(), 0);
|
|
663
891
|
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
892
|
+
const resultPath = path.join(DIRS.results, `${result.details.asyncId}.json`);
|
|
893
|
+
let runResult: { state?: string } = {};
|
|
894
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
895
|
+
if (fs.existsSync(resultPath)) runResult = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
896
|
+
if (runResult.state === "complete" || runResult.state === "failed") break;
|
|
897
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
668
898
|
}
|
|
669
|
-
assert.equal(
|
|
670
|
-
|
|
899
|
+
assert.equal(runResult.state, "complete");
|
|
900
|
+
|
|
901
|
+
fs.rmSync(result.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
671
902
|
fs.rmSync(resultPath, { force: true });
|
|
672
903
|
});
|
|
673
904
|
|
|
674
|
-
it("
|
|
675
|
-
const
|
|
676
|
-
const executor = makeExecutor([
|
|
677
|
-
|
|
678
|
-
|
|
905
|
+
it("runs external CLI agents with fallback models without registry validation", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
906
|
+
const markerPath = path.join(tempDir, "external-fallback-started");
|
|
907
|
+
const executor = makeExecutor([
|
|
908
|
+
makeAgent("external", {
|
|
909
|
+
runner: { type: "external-cli", command: process.execPath, args: ["-e", `require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "started")`] },
|
|
910
|
+
fallbackModels: ["mock/fallback"],
|
|
911
|
+
}),
|
|
912
|
+
]);
|
|
679
913
|
const result = await executor.execute(
|
|
680
|
-
|
|
681
|
-
{
|
|
914
|
+
"external-fallback-model",
|
|
915
|
+
{ agent: "external", task: "Run external", async: true },
|
|
682
916
|
new AbortController().signal,
|
|
683
917
|
undefined,
|
|
684
|
-
|
|
918
|
+
{
|
|
919
|
+
...makeMinimalCtx(tempDir),
|
|
920
|
+
modelRegistry: { getAvailable: () => [{ provider: "other", id: "known" }] },
|
|
921
|
+
},
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
assert.equal(result.isError, undefined);
|
|
925
|
+
assert.doesNotMatch(result.content[0]?.text ?? "", /Unknown subagent model/);
|
|
926
|
+
for (let attempt = 0; attempt < 100 && !fs.existsSync(markerPath); attempt++) {
|
|
927
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
928
|
+
}
|
|
929
|
+
assert.equal(fs.readFileSync(markerPath, "utf-8"), "started");
|
|
930
|
+
assert.equal(mockPi.callCount(), 0);
|
|
931
|
+
|
|
932
|
+
assert.ok(result.details.asyncId);
|
|
933
|
+
const resultPath = path.join(DIRS.results, `${result.details.asyncId}.json`);
|
|
934
|
+
let runResult: { state?: string } = {};
|
|
935
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
936
|
+
if (fs.existsSync(resultPath)) runResult = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
937
|
+
if (runResult.state === "complete" || runResult.state === "failed") break;
|
|
938
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
939
|
+
}
|
|
940
|
+
assert.equal(runResult.state, "complete");
|
|
941
|
+
|
|
942
|
+
fs.rmSync(result.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
943
|
+
fs.rmSync(resultPath, { force: true });
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it("rejects external CLI fork context before fallback model validation", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
947
|
+
const markerPath = path.join(tempDir, "external-fork-started");
|
|
948
|
+
const parentSessionFile = path.join(mockPi.dir, "external-fork-parent.jsonl");
|
|
949
|
+
fs.writeFileSync(parentSessionFile, `${JSON.stringify({ type: "session", version: 3, id: "parent", cwd: tempDir })}\n`, "utf-8");
|
|
950
|
+
const ctx = makeMinimalCtx(tempDir);
|
|
951
|
+
Object.assign(ctx.sessionManager, {
|
|
952
|
+
getSessionFile: () => parentSessionFile,
|
|
953
|
+
getLeafId: () => "parent-leaf",
|
|
954
|
+
openSession: () => ({
|
|
955
|
+
createBranchedSession: () => parentSessionFile,
|
|
956
|
+
}),
|
|
957
|
+
});
|
|
958
|
+
const executor = makeExecutor([
|
|
959
|
+
makeAgent("external", {
|
|
960
|
+
runner: { type: "external-cli", command: process.execPath, args: ["-e", `require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "started")`] },
|
|
961
|
+
defaultContext: "fork",
|
|
962
|
+
fallbackModels: ["mock/fallback"],
|
|
963
|
+
}),
|
|
964
|
+
]);
|
|
965
|
+
const result = await executor.execute(
|
|
966
|
+
"external-fork-fallback",
|
|
967
|
+
{ agent: "external", task: "Run external", async: true },
|
|
968
|
+
new AbortController().signal,
|
|
969
|
+
undefined,
|
|
970
|
+
{
|
|
971
|
+
...ctx,
|
|
972
|
+
modelRegistry: { getAvailable: () => [{ provider: "other", id: "known" }] },
|
|
973
|
+
},
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
assert.equal(result.isError, true);
|
|
977
|
+
assert.match(result.content[0]?.text ?? "", /does not support: fork context/);
|
|
978
|
+
assert.doesNotMatch(result.content[0]?.text ?? "", /Unknown subagent model/);
|
|
979
|
+
assert.equal(mockPi.callCount(), 0);
|
|
980
|
+
assert.equal(fs.existsSync(markerPath), false);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
it("rejects explicit model overrides for external CLI agents", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
984
|
+
const executor = makeExecutor([
|
|
985
|
+
makeAgent("external", {
|
|
986
|
+
runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
|
|
987
|
+
}),
|
|
988
|
+
]);
|
|
989
|
+
const result = await executor.execute(
|
|
990
|
+
"external-explicit-model",
|
|
991
|
+
{ agent: "external", task: "Run external", async: true, model: "mock/override" },
|
|
992
|
+
new AbortController().signal,
|
|
993
|
+
undefined,
|
|
994
|
+
{
|
|
995
|
+
...makeMinimalCtx(tempDir),
|
|
996
|
+
modelRegistry: { getAvailable: () => [{ provider: "other", id: "known" }] },
|
|
997
|
+
},
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
assert.equal(result.isError, true);
|
|
1001
|
+
assert.match(result.content[0]?.text ?? "", /does not support: model override/);
|
|
1002
|
+
assert.doesNotMatch(result.content[0]?.text ?? "", /Unknown subagent model/);
|
|
1003
|
+
assert.equal(mockPi.callCount(), 0);
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
it("rejects external CLI agent models that differ from inherited subagents.defaultModel", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1007
|
+
const executor = makeExecutor([
|
|
1008
|
+
makeAgent("external", {
|
|
1009
|
+
runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
|
|
1010
|
+
model: "mock/override-model",
|
|
1011
|
+
modelSource: { type: "subagents.defaultModel", scope: "user", path: "/settings.json", model: "mock/default-model" },
|
|
1012
|
+
}),
|
|
1013
|
+
]);
|
|
1014
|
+
const result = await executor.execute(
|
|
1015
|
+
"external-agent-override-model",
|
|
1016
|
+
{ agent: "external", task: "Run external", async: true },
|
|
1017
|
+
new AbortController().signal,
|
|
1018
|
+
undefined,
|
|
1019
|
+
makeMinimalCtx(tempDir),
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
assert.equal(result.isError, true);
|
|
1023
|
+
assert.match(result.content[0]?.text ?? "", /does not support: model override/);
|
|
1024
|
+
assert.equal(mockPi.callCount(), 0);
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it("rejects external CLI agent models that equal inherited subagents.defaultModel without provenance", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1028
|
+
const executor = makeExecutor([
|
|
1029
|
+
makeAgent("external", {
|
|
1030
|
+
runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
|
|
1031
|
+
model: "mock/default-model",
|
|
1032
|
+
}),
|
|
1033
|
+
]);
|
|
1034
|
+
const result = await executor.execute(
|
|
1035
|
+
"external-agent-same-value-override-model",
|
|
1036
|
+
{ agent: "external", task: "Run external", async: true },
|
|
1037
|
+
new AbortController().signal,
|
|
1038
|
+
undefined,
|
|
1039
|
+
makeMinimalCtx(tempDir),
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
assert.equal(result.isError, true);
|
|
1043
|
+
assert.match(result.content[0]?.text ?? "", /does not support: model override/);
|
|
1044
|
+
assert.equal(mockPi.callCount(), 0);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
it("projects live child activity into async workflow status", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1048
|
+
mockPi.onCall({
|
|
1049
|
+
steps: [
|
|
1050
|
+
{ jsonl: [events.toolStart("read", { path: "src/example.ts" })] },
|
|
1051
|
+
{ delay: 2_500, jsonl: [events.toolEnd("read"), events.toolResult("read", "contents")] },
|
|
1052
|
+
{ jsonl: [events.assistantMessage("Done")] },
|
|
1053
|
+
],
|
|
1054
|
+
});
|
|
1055
|
+
const asyncJobs: SubagentState["asyncJobs"] = new Map();
|
|
1056
|
+
const executor = makeExecutor([makeAgent("echo")], {
|
|
1057
|
+
control: {
|
|
1058
|
+
enabled: true,
|
|
1059
|
+
needsAttentionAfterMs: 100,
|
|
1060
|
+
activeNoticeAfterMs: 100,
|
|
1061
|
+
activeNoticeAfterTurns: 999_999,
|
|
1062
|
+
activeNoticeAfterTokens: 999_999,
|
|
1063
|
+
notifyOn: ["active_long_running", "needs_attention"],
|
|
1064
|
+
notifyChannels: ["event"],
|
|
1065
|
+
},
|
|
1066
|
+
}, false, undefined, true, asyncJobs);
|
|
1067
|
+
|
|
1068
|
+
const result = await executor.execute(
|
|
1069
|
+
"workflow-live-activity",
|
|
1070
|
+
{ workflowScript: `return runs.run("main", { agent: "echo", task: "Inspect the file" });` },
|
|
1071
|
+
new AbortController().signal,
|
|
1072
|
+
undefined,
|
|
1073
|
+
makeMinimalCtx(tempDir),
|
|
1074
|
+
);
|
|
1075
|
+
const { asyncId: workflowRunId, asyncDir } = result.details;
|
|
1076
|
+
assert.ok(workflowRunId);
|
|
1077
|
+
assert.ok(asyncDir);
|
|
1078
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
1079
|
+
const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
|
|
1080
|
+
const activeMarkerPath = path.join(DIRS.async, ACTIVE_RUN_INDEX_DIR, workflowRunId);
|
|
1081
|
+
assert.equal(fs.existsSync(activeMarkerPath), true);
|
|
1082
|
+
let liveStatus: AsyncStatus | undefined;
|
|
1083
|
+
const activityDeadline = Date.now() + 5_000;
|
|
1084
|
+
while (Date.now() < activityDeadline && !fs.existsSync(resultPath)) {
|
|
1085
|
+
const candidate = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
1086
|
+
if (candidate.activityState === "needs_attention" && candidate.steps?.[0]?.currentTool === "read") {
|
|
1087
|
+
liveStatus = candidate;
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
assert.ok(liveStatus, "expected workflow status to expose live child activity");
|
|
1094
|
+
assert.equal(liveStatus.activityState, "needs_attention");
|
|
1095
|
+
assert.equal(typeof liveStatus.lastActivityAt, "number");
|
|
1096
|
+
assert.equal(liveStatus.currentTool, "read");
|
|
1097
|
+
assert.match(liveStatus.currentPath ?? "", /src[/\\]example\.ts$/);
|
|
1098
|
+
assert.equal(liveStatus.toolCount, 1);
|
|
1099
|
+
assert.equal(liveStatus.steps?.[0]?.status, "running");
|
|
1100
|
+
assert.equal(liveStatus.steps?.[0]?.agent, "echo");
|
|
1101
|
+
assert.match(liveStatus.steps?.[0]?.sessionFile ?? "", /session\.jsonl$/);
|
|
1102
|
+
assert.equal(fs.existsSync(liveStatus.steps?.[0]?.sessionFile ?? ""), true);
|
|
1103
|
+
assert.equal(liveStatus.steps?.[0]?.activityState, "needs_attention");
|
|
1104
|
+
assert.equal(typeof liveStatus.steps?.[0]?.lastActivityAt, "number");
|
|
1105
|
+
assert.equal(liveStatus.steps?.[0]?.toolCount, 1);
|
|
1106
|
+
assert.equal(asyncJobs.get(workflowRunId)?.activityState, "needs_attention");
|
|
1107
|
+
assert.equal(asyncJobs.get(workflowRunId)?.steps?.[0]?.currentTool, "read");
|
|
1108
|
+
|
|
1109
|
+
const completionDeadline = Date.now() + 5_000;
|
|
1110
|
+
while (!fs.existsSync(resultPath)) {
|
|
1111
|
+
if (Date.now() > completionDeadline) assert.fail("Timed out waiting for async workflow completion");
|
|
1112
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1113
|
+
}
|
|
1114
|
+
assert.equal(fs.existsSync(activeMarkerPath), false);
|
|
1115
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
1116
|
+
fs.rmSync(resultPath, { force: true });
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
it("notifies the parent when an async workflow child needs attention", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1120
|
+
mockPi.onCall({
|
|
1121
|
+
steps: [
|
|
1122
|
+
{ jsonl: [events.toolStart("read", { path: "src/example.ts" }), events.toolEnd("read"), events.toolResult("read", "contents")] },
|
|
1123
|
+
{ delay: 2_500, jsonl: [events.assistantMessage("Done")] },
|
|
1124
|
+
],
|
|
1125
|
+
});
|
|
1126
|
+
const asyncJobs: SubagentState["asyncJobs"] = new Map();
|
|
1127
|
+
const piEvents = createEventBus();
|
|
1128
|
+
const controlPayloads: Array<{ event?: ControlEvent; source?: string }> = [];
|
|
1129
|
+
piEvents.on(SUBAGENT_CONTROL_EVENT, (payload) => {
|
|
1130
|
+
controlPayloads.push(payload as { event?: ControlEvent; source?: string });
|
|
1131
|
+
});
|
|
1132
|
+
const executor = makeExecutor([makeAgent("echo")], {
|
|
1133
|
+
control: {
|
|
1134
|
+
enabled: true,
|
|
1135
|
+
needsAttentionAfterMs: 100,
|
|
1136
|
+
activeNoticeAfterMs: 999_999,
|
|
1137
|
+
activeNoticeAfterTurns: 999_999,
|
|
1138
|
+
activeNoticeAfterTokens: 999_999,
|
|
1139
|
+
notifyOn: ["needs_attention"],
|
|
1140
|
+
notifyChannels: ["event"],
|
|
1141
|
+
},
|
|
1142
|
+
}, false, undefined, true, asyncJobs, undefined, undefined, piEvents);
|
|
1143
|
+
|
|
1144
|
+
const result = await executor.execute(
|
|
1145
|
+
"workflow-child-attention-notice",
|
|
1146
|
+
{ workflowScript: `return runs.run("stalled-review", { agent: "echo", task: "Inspect the file" });` },
|
|
1147
|
+
new AbortController().signal,
|
|
1148
|
+
undefined,
|
|
1149
|
+
makeMinimalCtx(tempDir),
|
|
1150
|
+
);
|
|
1151
|
+
const { asyncId: workflowRunId, asyncDir } = result.details;
|
|
1152
|
+
assert.ok(workflowRunId);
|
|
1153
|
+
assert.ok(asyncDir);
|
|
1154
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
1155
|
+
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
1156
|
+
const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
|
|
1157
|
+
let liveStatus: AsyncStatus | undefined;
|
|
1158
|
+
const activityDeadline = Date.now() + 5_000;
|
|
1159
|
+
while (Date.now() < activityDeadline && !fs.existsSync(resultPath)) {
|
|
1160
|
+
if (fs.existsSync(statusPath)) {
|
|
1161
|
+
const candidate = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
1162
|
+
if (candidate.activityState === "needs_attention" && !candidate.steps?.[0]?.currentTool) {
|
|
1163
|
+
liveStatus = candidate;
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
assert.ok(liveStatus, "expected workflow status to expose idle child attention");
|
|
1171
|
+
assert.equal(liveStatus.activityState, "needs_attention");
|
|
1172
|
+
assert.equal(liveStatus.steps?.[0]?.activityState, "needs_attention");
|
|
1173
|
+
assert.equal(liveStatus.steps?.[0]?.workflowKey, "stalled-review");
|
|
1174
|
+
|
|
1175
|
+
const attentionPayload = controlPayloads.find((payload) => payload.event?.type === "needs_attention");
|
|
1176
|
+
assert.ok(attentionPayload, "expected a live parent control event");
|
|
1177
|
+
assert.equal(attentionPayload.source, "async");
|
|
1178
|
+
assert.equal(attentionPayload.event?.workflowKey, "stalled-review");
|
|
1179
|
+
assert.equal(attentionPayload.event?.reason, "idle");
|
|
1180
|
+
const sent: Array<{ options?: { triggerTurn?: boolean } }> = [];
|
|
1181
|
+
handleSubagentControlNotice({
|
|
1182
|
+
pi: { sendMessage(_message, options) { sent.push({ options: options as { triggerTurn?: boolean } }); } },
|
|
1183
|
+
state: { asyncJobs } as SubagentState,
|
|
1184
|
+
visibleControlNotices: new Set(),
|
|
1185
|
+
details: { event: attentionPayload.event!, source: "async" },
|
|
1186
|
+
});
|
|
1187
|
+
assert.equal(sent.length, 1);
|
|
1188
|
+
assert.deepEqual(sent[0]?.options, { triggerTurn: true });
|
|
1189
|
+
|
|
1190
|
+
assert.equal(fs.existsSync(eventsPath), true);
|
|
1191
|
+
const controlRecords = fs.readFileSync(eventsPath, "utf-8")
|
|
1192
|
+
.split("\n")
|
|
1193
|
+
.filter((line) => line.trim())
|
|
1194
|
+
.map((line) => JSON.parse(line) as { type?: string; event?: ControlEvent; runId?: string })
|
|
1195
|
+
.filter((record) => record.type === "subagent.control");
|
|
1196
|
+
const persisted = controlRecords.find((record) => record.event?.type === "needs_attention");
|
|
1197
|
+
assert.ok(persisted, "expected a persisted workflow control event");
|
|
1198
|
+
assert.equal(persisted.runId, workflowRunId);
|
|
1199
|
+
assert.equal(persisted.event?.workflowKey, "stalled-review");
|
|
1200
|
+
assert.equal(controlRecords.filter((record) => record.event?.type === "needs_attention").length, 1);
|
|
1201
|
+
|
|
1202
|
+
const completionDeadline = Date.now() + 5_000;
|
|
1203
|
+
while (!fs.existsSync(resultPath)) {
|
|
1204
|
+
if (Date.now() > completionDeadline) assert.fail("Timed out waiting for async workflow completion");
|
|
1205
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1206
|
+
}
|
|
1207
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
1208
|
+
fs.rmSync(resultPath, { force: true });
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
it("rejects an invalid async workflow usage budget before creating run state", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1212
|
+
const asyncJobs: SubagentState["asyncJobs"] = new Map();
|
|
1213
|
+
const executor = makeExecutor([makeAgent("echo")], {}, false, undefined, true, asyncJobs);
|
|
1214
|
+
const runId = `scripted-workflow-invalid-budget-${Date.now()}`;
|
|
1215
|
+
|
|
1216
|
+
const result = await executor.execute(
|
|
1217
|
+
runId,
|
|
1218
|
+
{ workflowScript: `return "unreachable";`, usageBudget: { tokens: { hard: 0 } } },
|
|
1219
|
+
new AbortController().signal,
|
|
1220
|
+
undefined,
|
|
1221
|
+
makeMinimalCtx(tempDir),
|
|
685
1222
|
);
|
|
686
1223
|
|
|
687
1224
|
assert.equal(result.isError, true);
|
|
@@ -787,7 +1324,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
787
1324
|
fs.writeFileSync(path.join(tempDir, "base.txt"), "base\n", "utf-8");
|
|
788
1325
|
execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
|
|
789
1326
|
execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
|
|
790
|
-
mockPi.onCall({ output: "async child done" });
|
|
1327
|
+
mockPi.onCall({ output: "async child done", writeFiles: [{ path: "feature.txt", content: "feature\n" }] });
|
|
791
1328
|
const executor = makeExecutor([makeAgent("echo")]);
|
|
792
1329
|
const toolCallId = `scripted-workflow-parent-${Date.now()}`;
|
|
793
1330
|
const started = await executor.execute(
|
|
@@ -815,16 +1352,26 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
815
1352
|
assert.match(workflowStepSessionFile, /session\.jsonl$/);
|
|
816
1353
|
const childDir = path.join(DIRS.async, childRunId);
|
|
817
1354
|
const childStatusPath = path.join(childDir, "status.json");
|
|
818
|
-
let childStatus: { state?: string; mode?: string; parentWorkflowRunId?: string; workflowKey?: string; parallelHandoff?: { path?: string } } = {};
|
|
1355
|
+
let childStatus: { state?: string; mode?: string; parentWorkflowRunId?: string; workflowKey?: string; parallelHandoff?: { path?: string; changedPatches?: number } } = {};
|
|
819
1356
|
for (let attempt = 0; attempt < 200; attempt++) {
|
|
820
1357
|
if (fs.existsSync(childStatusPath)) childStatus = JSON.parse(fs.readFileSync(childStatusPath, "utf-8"));
|
|
821
1358
|
if (["complete", "failed", "stopped"].includes(childStatus.state ?? "")) break;
|
|
822
1359
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
823
1360
|
}
|
|
824
|
-
assert.equal(childStatus.mode, "
|
|
1361
|
+
assert.equal(childStatus.mode, "single");
|
|
825
1362
|
assert.equal(childStatus.parentWorkflowRunId, workflowRunId);
|
|
826
1363
|
assert.equal(childStatus.workflowKey, "background");
|
|
827
1364
|
assert.equal(typeof childStatus.parallelHandoff?.path, "string");
|
|
1365
|
+
assert.equal(childStatus.parallelHandoff?.changedPatches, 1);
|
|
1366
|
+
assert.equal(fs.existsSync(path.join(tempDir, "feature.txt")), false);
|
|
1367
|
+
const handoff = JSON.parse(fs.readFileSync(childStatus.parallelHandoff!.path!, "utf-8")) as { groups?: Array<{ children?: Array<{ patch?: { changed?: boolean; filesChanged?: number } }>; cleanup?: { state?: string; tasks?: Array<{ path?: string; preserved?: boolean; worktreeRemoved?: boolean; reason?: string }> } }> };
|
|
1368
|
+
assert.equal(handoff.groups?.[0]?.children?.[0]?.patch?.changed, true);
|
|
1369
|
+
assert.equal(handoff.groups?.[0]?.children?.[0]?.patch?.filesChanged, 1);
|
|
1370
|
+
assert.equal(handoff.groups?.[0]?.cleanup?.state, "partial");
|
|
1371
|
+
assert.equal(handoff.groups?.[0]?.cleanup?.tasks?.[0]?.preserved, true);
|
|
1372
|
+
assert.equal(handoff.groups?.[0]?.cleanup?.tasks?.[0]?.worktreeRemoved, false);
|
|
1373
|
+
assert.equal(handoff.groups?.[0]?.cleanup?.tasks?.[0]?.reason, "retained child resume requires managed worktree cwd");
|
|
1374
|
+
assert.equal(fs.existsSync(handoff.groups?.[0]?.cleanup?.tasks?.[0]?.path ?? ""), true);
|
|
828
1375
|
const childResultPath = path.join(DIRS.results, `${childRunId}.json`);
|
|
829
1376
|
for (let attempt = 0; attempt < 200 && !fs.existsSync(childResultPath); attempt++) {
|
|
830
1377
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
@@ -833,6 +1380,12 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
833
1380
|
assert.equal(childResult.parentWorkflowRunId, workflowRunId);
|
|
834
1381
|
assert.equal(childResult.workflowKey, "background");
|
|
835
1382
|
assert.equal(fs.existsSync(workflowStepSessionFile), true);
|
|
1383
|
+
const retainedCwd = handoff.groups?.[0]?.cleanup?.tasks?.[0]?.path;
|
|
1384
|
+
assert.ok(retainedCwd);
|
|
1385
|
+
const resumeTarget = resolveAsyncResumeTarget({ id: childRunId }, { asyncDirRoot: DIRS.async, resultsDir: DIRS.results });
|
|
1386
|
+
assert.equal(resumeTarget.recoveryDescriptor?.sourceRunId, childRunId);
|
|
1387
|
+
assert.equal(path.resolve(resumeTarget.cwd ?? ""), path.resolve(retainedCwd));
|
|
1388
|
+
discardPreservedWorktrees(childStatus.parallelHandoff!.path!, { kind: "confirmed" });
|
|
836
1389
|
fs.rmSync(started.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
837
1390
|
fs.rmSync(workflowResultPath, { force: true });
|
|
838
1391
|
fs.rmSync(childDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
@@ -854,26 +1407,168 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
854
1407
|
assert.match(result.content[0]?.text ?? "", /Stop requested for async workflow workflow-stop/);
|
|
855
1408
|
});
|
|
856
1409
|
|
|
857
|
-
it("
|
|
858
|
-
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1410
|
+
it("persists parent-stopped workflow children as stopped instead of failed", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1411
|
+
mockPi.onCall({ delay: 5_000, output: "too late" });
|
|
1412
|
+
const workflowControllers = new Map<string, AbortController>();
|
|
1413
|
+
const executor = makeExecutor([makeAgent("echo")], {}, false, undefined, true, new Map(), workflowControllers);
|
|
1414
|
+
const started = await executor.execute(
|
|
1415
|
+
`workflow-stop-child-${Date.now()}`,
|
|
1416
|
+
{ workflowScript: `return await runs.run("review", { agent: "echo", task: "Wait" });` },
|
|
1417
|
+
new AbortController().signal,
|
|
1418
|
+
undefined,
|
|
1419
|
+
makeMinimalCtx(tempDir),
|
|
1420
|
+
);
|
|
1421
|
+
assert.ok(started.details.asyncId);
|
|
1422
|
+
const workflowRunId = started.details.asyncId;
|
|
1423
|
+
const statusPath = path.join(started.details.asyncDir!, "status.json");
|
|
1424
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
1425
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
1426
|
+
if (status.steps?.some((step) => step.workflowKey === "review" && step.status === "running")) break;
|
|
1427
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1428
|
+
}
|
|
1429
|
+
let childCall: string | undefined;
|
|
1430
|
+
for (let attempt = 0; attempt < 100 && !childCall; attempt++) {
|
|
1431
|
+
childCall = fs.readdirSync(mockPi.dir).find((name) => /^call-\d+-\d+-/.test(name));
|
|
1432
|
+
if (!childCall) await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1433
|
+
}
|
|
1434
|
+
const childPid = Number(childCall?.match(/^call-\d+-(\d+)-/)?.[1]);
|
|
1435
|
+
assert.equal(Number.isInteger(childPid) && childPid > 0, true);
|
|
1436
|
+
|
|
1437
|
+
const stopped = await executor.execute(
|
|
1438
|
+
"stop-workflow-child",
|
|
1439
|
+
{ action: "stop", id: workflowRunId },
|
|
1440
|
+
new AbortController().signal,
|
|
1441
|
+
undefined,
|
|
1442
|
+
makeMinimalCtx(tempDir),
|
|
1443
|
+
);
|
|
1444
|
+
assert.equal(stopped.isError, undefined);
|
|
1445
|
+
|
|
1446
|
+
let status: AsyncStatus = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
|
|
1447
|
+
for (let attempt = 0; attempt < 100 && status.state !== "stopped"; attempt++) {
|
|
1448
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1449
|
+
status = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
|
|
1450
|
+
}
|
|
1451
|
+
assert.equal(status.state, "stopped");
|
|
1452
|
+
assert.equal(status.error, "Workflow stopped by user.");
|
|
1453
|
+
assert.equal(status.steps?.[0]?.status, "stopped");
|
|
1454
|
+
assert.equal(status.steps?.[0]?.stopped, true);
|
|
1455
|
+
assert.equal(status.steps?.[0]?.error, "Workflow stopped by user.");
|
|
1456
|
+
assert.equal(status.workflow?.trace.some((entry) => entry.key === "review" && entry.state === "stopped"), true);
|
|
1457
|
+
assert.equal(status.workflow?.trace.some((entry) => entry.key === "review" && entry.state === "failed"), false);
|
|
1458
|
+
|
|
1459
|
+
let childSettled = false;
|
|
1460
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
1461
|
+
try {
|
|
1462
|
+
process.kill(childPid, 0);
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
if ((error as NodeJS.ErrnoException).code === "ESRCH") {
|
|
1465
|
+
childSettled = true;
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
throw error;
|
|
1469
|
+
}
|
|
1470
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1471
|
+
}
|
|
1472
|
+
assert.equal(childSettled, true, "child process must settle after the workflow stop");
|
|
1473
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1474
|
+
status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
1475
|
+
assert.equal(status.steps?.[0]?.status, "stopped");
|
|
1476
|
+
assert.equal(status.steps?.[0]?.stopped, true);
|
|
1477
|
+
assert.equal(status.steps?.[0]?.error, "Workflow stopped by user.");
|
|
1478
|
+
|
|
1479
|
+
fs.rmSync(started.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
1480
|
+
fs.rmSync(path.join(DIRS.results, `${workflowRunId}.json`), { force: true });
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
it("stops one live async workflow child without stopping the parent or sibling", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1484
|
+
mockPi.onCall({ matchArgIncludes: "Slow child", delay: 5_000, output: "slow late" });
|
|
1485
|
+
mockPi.onCall({ matchArgIncludes: "Fast child", delay: 250, output: "fast done" });
|
|
1486
|
+
const asyncJobs: SubagentState["asyncJobs"] = new Map();
|
|
1487
|
+
const executor = makeExecutor([makeAgent("echo")], {}, false, undefined, true, asyncJobs);
|
|
1488
|
+
const started = await executor.execute(
|
|
1489
|
+
`workflow-child-stop-${Date.now()}`,
|
|
1490
|
+
{
|
|
1491
|
+
workflowScript: `
|
|
1492
|
+
const results = await runs.all([
|
|
1493
|
+
{ key: "slow", agent: "echo", task: "Slow child" },
|
|
1494
|
+
{ key: "fast", agent: "echo", task: "Fast child" }
|
|
1495
|
+
]);
|
|
1496
|
+
return results.map((result) => result.key + ":" + (result.stopped ? "stopped" : result.ok ? "ok" : "failed")).join(",");
|
|
1497
|
+
`,
|
|
1498
|
+
},
|
|
1499
|
+
new AbortController().signal,
|
|
1500
|
+
undefined,
|
|
1501
|
+
makeMinimalCtx(tempDir),
|
|
1502
|
+
);
|
|
1503
|
+
assert.equal(started.isError, undefined);
|
|
1504
|
+
assert.ok(started.details.asyncDir);
|
|
1505
|
+
const workflowRunId = started.details.asyncId!;
|
|
1506
|
+
const statusPath = path.join(started.details.asyncDir, "status.json");
|
|
1507
|
+
const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
|
|
1508
|
+
let status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
1509
|
+
for (let attempt = 0; attempt < 150 && !status.steps?.some((step) => step.workflowKey === "slow" && step.status === "running"); attempt++) {
|
|
1510
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1511
|
+
status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
1512
|
+
}
|
|
1513
|
+
const listed = listAsyncRuns(DIRS.async, { sessionId: "session-123", runId: workflowRunId, exactRunId: true })
|
|
1514
|
+
.find((run) => run.id === workflowRunId);
|
|
1515
|
+
assert.equal(listed?.steps?.find((step) => step.workflowKey === "slow")?.childId, "slow");
|
|
1516
|
+
|
|
1517
|
+
const stop = await executor.execute(
|
|
1518
|
+
"stop-workflow-child-only",
|
|
1519
|
+
{ action: "stop", id: workflowRunId, childId: "slow" },
|
|
1520
|
+
new AbortController().signal,
|
|
1521
|
+
undefined,
|
|
1522
|
+
makeMinimalCtx(tempDir),
|
|
1523
|
+
);
|
|
1524
|
+
assert.equal(stop.isError, undefined, stop.content[0]?.text ?? "");
|
|
1525
|
+
assert.match(stop.content[0]?.text ?? "", /Stop requested for child slow/);
|
|
1526
|
+
|
|
1527
|
+
for (let attempt = 0; attempt < 150 && status.state !== "complete"; attempt++) {
|
|
1528
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1529
|
+
status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
1530
|
+
}
|
|
1531
|
+
assert.equal(status.state, "complete", status.error);
|
|
1532
|
+
assert.equal(status.stopped, undefined);
|
|
1533
|
+
assert.equal(status.steps?.find((step) => step.workflowKey === "slow")?.status, "stopped");
|
|
1534
|
+
assert.equal(status.steps?.find((step) => step.workflowKey === "slow")?.stopped, true);
|
|
1535
|
+
assert.equal(status.steps?.find((step) => step.workflowKey === "fast")?.status, "completed");
|
|
1536
|
+
assert.equal(status.steps?.find((step) => step.workflowKey === "fast")?.stopped, undefined);
|
|
1537
|
+
const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as { state?: string; stopped?: boolean; results?: Array<{ workflowKey?: string; success?: boolean; stopped?: boolean }> };
|
|
1538
|
+
assert.equal(payload.state, "complete");
|
|
1539
|
+
assert.equal(payload.stopped, undefined);
|
|
1540
|
+
assert.equal(payload.results?.find((entry) => entry.workflowKey === "slow")?.stopped, true);
|
|
1541
|
+
assert.equal(payload.results?.find((entry) => entry.workflowKey === "fast")?.success, true);
|
|
1542
|
+
const childStatusEvents = fs.readFileSync(path.join(started.details.asyncDir, "events.jsonl"), "utf-8")
|
|
1543
|
+
.trim()
|
|
1544
|
+
.split("\n")
|
|
1545
|
+
.map((line) => JSON.parse(line) as { type?: string; childId?: string; status?: string });
|
|
1546
|
+
assert.ok(childStatusEvents.some((event) => event.type === "subagent.child-status" && event.childId === "slow" && event.status === "stopping"));
|
|
1547
|
+
assert.ok(childStatusEvents.some((event) => event.type === "subagent.child-status" && event.childId === "slow" && event.status === "stopped"));
|
|
1548
|
+
fs.rmSync(started.details.asyncDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
1549
|
+
fs.rmSync(resultPath, { force: true });
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
it("reports completed async workflows as not running when stopped after completion", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1553
|
+
const runId = `workflow-stop-complete-${Date.now()}`;
|
|
1554
|
+
const asyncDir = path.join(DIRS.async, runId);
|
|
1555
|
+
fs.mkdirSync(asyncDir, { recursive: true });
|
|
1556
|
+
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
|
|
1557
|
+
runId,
|
|
1558
|
+
sessionId: "session-123",
|
|
1559
|
+
mode: "workflow",
|
|
1560
|
+
state: "complete",
|
|
1561
|
+
startedAt: Date.now(),
|
|
1562
|
+
lastUpdate: Date.now(),
|
|
1563
|
+
cwd: tempDir,
|
|
1564
|
+
pid: process.pid,
|
|
1565
|
+
}), "utf-8");
|
|
1566
|
+
const asyncJobs: SubagentState["asyncJobs"] = new Map([[runId, {
|
|
1567
|
+
asyncId: runId,
|
|
1568
|
+
asyncDir,
|
|
1569
|
+
cwd: tempDir,
|
|
1570
|
+
status: "complete",
|
|
1571
|
+
mode: "workflow",
|
|
877
1572
|
agents: [],
|
|
878
1573
|
steps: [],
|
|
879
1574
|
startedAt: Date.now(),
|
|
@@ -939,10 +1634,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
939
1634
|
const mission = JSON.parse(fs.readFileSync(path.join(agentDir, "missions", "projects", missionFiles[0]!), "utf-8")) as { objective?: string };
|
|
940
1635
|
assert.equal(mission.objective, utils.PROMPT_REDACTED);
|
|
941
1636
|
assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state === "completed").map((entry) => entry.key), ["scan", "review"]);
|
|
942
|
-
|
|
943
|
-
// tree keeps untracked runtime state; only that dir may appear.
|
|
944
|
-
const porcelain = execFileSync("git", ["status", "--porcelain"], { cwd: projectDir, encoding: "utf-8" });
|
|
945
|
-
assert.match(porcelain, /^\?\? \.pi-subagents\/\n$/);
|
|
1637
|
+
assert.equal(execFileSync("git", ["status", "--porcelain"], { cwd: projectDir, encoding: "utf-8" }), "");
|
|
946
1638
|
} finally {
|
|
947
1639
|
if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
948
1640
|
else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
|
|
@@ -954,7 +1646,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
954
1646
|
mockPi.onCall({ output: "reviewed auth" });
|
|
955
1647
|
const blockedIndex = path.join(tempDir, "blocked-mission-index");
|
|
956
1648
|
fs.writeFileSync(blockedIndex, "not a directory", "utf-8");
|
|
957
|
-
const executor = makeExecutor([makeAgent("echo")], { missions: { directory: ".pi
|
|
1649
|
+
const executor = makeExecutor([makeAgent("echo")], { missions: { directory: ".pi-subagents/missions", globalIndexDir: blockedIndex } });
|
|
958
1650
|
|
|
959
1651
|
const result = await executor.execute(
|
|
960
1652
|
"scripted-workflow-mission-warning",
|
|
@@ -975,7 +1667,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
975
1667
|
assert.equal(result.details.missionId, undefined);
|
|
976
1668
|
assert.match(result.details.missionWarning ?? "", /Mission tracking unavailable/);
|
|
977
1669
|
assert.equal(result.details.results.length, 2);
|
|
978
|
-
const missionDir = path.join(tempDir, ".pi
|
|
1670
|
+
const missionDir = path.join(tempDir, ".pi-subagents", "missions");
|
|
979
1671
|
const missionFiles = fs.existsSync(missionDir) ? fs.readdirSync(missionDir).filter((entry) => entry.endsWith(".json")) : [];
|
|
980
1672
|
assert.equal(missionFiles.length, 1);
|
|
981
1673
|
});
|
|
@@ -1041,7 +1733,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1041
1733
|
execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
|
|
1042
1734
|
execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
|
|
1043
1735
|
mockPi.onCall({ output: "isolated feature", writeFiles: [{ path: "feature.txt", content: "feature\n" }] });
|
|
1044
|
-
const executor = makeExecutor([makeAgent("worker")]);
|
|
1736
|
+
const executor = makeExecutor([makeAgent("worker", { completionGuard: false })]);
|
|
1045
1737
|
|
|
1046
1738
|
const result = await executor.execute(
|
|
1047
1739
|
"direct-worktree",
|
|
@@ -1051,7 +1743,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1051
1743
|
makeMinimalCtx(tempDir),
|
|
1052
1744
|
);
|
|
1053
1745
|
|
|
1054
|
-
assert.equal(result.isError, undefined);
|
|
1746
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "managed worktree child failed");
|
|
1055
1747
|
assert.equal(mockPi.callCount(), 1);
|
|
1056
1748
|
assert.equal(fs.existsSync(path.join(tempDir, "feature.txt")), false);
|
|
1057
1749
|
const handoffPath = (result.content[0]?.text ?? "").match(/([^\s]+\/handoffs\/[^\s]+\.json)/)?.[1];
|
|
@@ -1092,38 +1784,664 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1092
1784
|
const executor = makeExecutor([makeAgent("worker", { defaultContext: "fork" })]);
|
|
1093
1785
|
|
|
1094
1786
|
const result = await executor.execute(
|
|
1095
|
-
"forked-worktree-workflow",
|
|
1096
|
-
{ async: false, workflowScript: `return runs.run("isolated", { agent: "worker", task: "Work in isolation", worktree: true });` },
|
|
1787
|
+
"forked-worktree-workflow",
|
|
1788
|
+
{ async: false, workflowScript: `return runs.run("isolated", { agent: "worker", task: "Work in isolation", worktree: true });` },
|
|
1789
|
+
new AbortController().signal,
|
|
1790
|
+
undefined,
|
|
1791
|
+
ctx,
|
|
1792
|
+
);
|
|
1793
|
+
|
|
1794
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
1795
|
+
const workflowValue = result.details.workflow?.value as { artifactPaths?: string[] } | undefined;
|
|
1796
|
+
const handoffPath = workflowValue?.artifactPaths?.find((candidate) => candidate.endsWith(".json") && candidate.includes("handoffs"));
|
|
1797
|
+
assert.ok(handoffPath, JSON.stringify(workflowValue));
|
|
1798
|
+
const handoff = JSON.parse(fs.readFileSync(handoffPath, "utf-8")) as {
|
|
1799
|
+
groups: Array<{ cleanup: { tasks: Array<{ path: string }> } }>;
|
|
1800
|
+
};
|
|
1801
|
+
const managedWorktreeCwd = handoff.groups[0]?.cleanup.tasks[0]?.path;
|
|
1802
|
+
assert.ok(managedWorktreeCwd);
|
|
1803
|
+
const callCwd = readCall().cwd;
|
|
1804
|
+
assert.ok(callCwd);
|
|
1805
|
+
assert.notEqual(path.resolve(callCwd), path.resolve(tempDir));
|
|
1806
|
+
assert.equal(path.basename(callCwd), path.basename(managedWorktreeCwd));
|
|
1807
|
+
const sessionHeader = JSON.parse(fs.readFileSync(childSessionFile, "utf-8").split("\n", 1)[0]!) as { cwd?: string };
|
|
1808
|
+
assert.ok(sessionHeader.cwd);
|
|
1809
|
+
assert.equal(path.basename(sessionHeader.cwd), path.basename(callCwd));
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
it("rejects workflowScript implementation children under a read-only capability ceiling before spawn", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1813
|
+
mockPi.onCall({ output: "completed without edits" });
|
|
1814
|
+
const executor = makeExecutor([makeAgent("worker")]);
|
|
1815
|
+
|
|
1816
|
+
const result = await executor.execute(
|
|
1817
|
+
"workflow-readonly-implementation-contract",
|
|
1818
|
+
{
|
|
1819
|
+
async: false,
|
|
1820
|
+
workflowScript: `return await runs.run("impl", { agent: "worker", task: "Implement the requested source fix" });`,
|
|
1821
|
+
capabilityCeiling: { version: 1, allowedTools: ["read", "grep", "find", "ls", "contact_supervisor"], denyExtensions: true, sources: ["test"] },
|
|
1822
|
+
},
|
|
1823
|
+
new AbortController().signal,
|
|
1824
|
+
undefined,
|
|
1825
|
+
makeMinimalCtx(tempDir),
|
|
1826
|
+
);
|
|
1827
|
+
|
|
1828
|
+
assert.equal(result.isError, true);
|
|
1829
|
+
assert.match(result.content[0]?.text ?? "", /no mutation-capable tools/);
|
|
1830
|
+
assert.doesNotMatch(result.content[0]?.text ?? "", /completed without making edits/);
|
|
1831
|
+
assert.equal(mockPi.callCount(), 0);
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
it("stringifies workflow child results without object placeholders", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1835
|
+
mockPi.onCall({ output: "first report", matchArgIncludes: "Review" });
|
|
1836
|
+
mockPi.onCall({ output: "second report", matchArgIncludes: "Monitor" });
|
|
1837
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
1838
|
+
|
|
1839
|
+
const result = await executor.execute(
|
|
1840
|
+
"scripted-workflow-stringified-child-results",
|
|
1841
|
+
{
|
|
1842
|
+
async: false,
|
|
1843
|
+
workflowScript: `
|
|
1844
|
+
const [review, monitor] = await runs.all([
|
|
1845
|
+
{ key: "review", agent: "echo", task: "Review" },
|
|
1846
|
+
{ key: "monitor", agent: "echo", task: "Monitor" }
|
|
1847
|
+
]);
|
|
1848
|
+
return "## Lane 1\\n" + review + "\\n\\n---\\n\\n## Lane 2\\n" + monitor;
|
|
1849
|
+
`,
|
|
1850
|
+
},
|
|
1851
|
+
new AbortController().signal,
|
|
1852
|
+
undefined,
|
|
1853
|
+
makeMinimalCtx(tempDir),
|
|
1854
|
+
);
|
|
1855
|
+
|
|
1856
|
+
const text = result.content[0]?.text ?? "";
|
|
1857
|
+
assert.equal(result.isError, undefined, text || "workflow failed");
|
|
1858
|
+
assert.doesNotMatch(text, /\[object Object\]/);
|
|
1859
|
+
assert.match(text, /## Lane 1\nfirst report/);
|
|
1860
|
+
assert.match(text, /## Lane 2\nsecond report/);
|
|
1861
|
+
assert.equal(result.details.workflow?.value, "## Lane 1\nfirst report\n\n---\n\n## Lane 2\nsecond report");
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
it("stringifies awaited workflow child results without object placeholders", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1865
|
+
mockPi.onCall({ output: "single report", matchArgIncludes: "Review" });
|
|
1866
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
1867
|
+
|
|
1868
|
+
const result = await executor.execute(
|
|
1869
|
+
"scripted-workflow-stringified-single-child-result",
|
|
1870
|
+
{
|
|
1871
|
+
async: false,
|
|
1872
|
+
workflowScript: `
|
|
1873
|
+
const review = await runs.run("review", { agent: "echo", task: "Review" });
|
|
1874
|
+
return "## Lane\\n" + review;
|
|
1875
|
+
`,
|
|
1876
|
+
},
|
|
1877
|
+
new AbortController().signal,
|
|
1878
|
+
undefined,
|
|
1879
|
+
makeMinimalCtx(tempDir),
|
|
1880
|
+
);
|
|
1881
|
+
|
|
1882
|
+
const text = result.content[0]?.text ?? "";
|
|
1883
|
+
assert.equal(result.isError, undefined, text || "workflow failed");
|
|
1884
|
+
assert.doesNotMatch(text, /\[object Object\]/);
|
|
1885
|
+
assert.match(text, /## Lane\nsingle report/);
|
|
1886
|
+
assert.equal(result.details.workflow?.value, "## Lane\nsingle report");
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
it("derives workflow child output paths from the workflow output", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1890
|
+
mockPi.onCall({ output: "first report", matchArgIncludes: "Review" });
|
|
1891
|
+
mockPi.onCall({ output: "second report", matchArgIncludes: "Monitor" });
|
|
1892
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
1893
|
+
const workflowOutput = path.join(tempDir, "workflow-report.md");
|
|
1894
|
+
|
|
1895
|
+
const result = await executor.execute(
|
|
1896
|
+
"scripted-workflow-child-output-defaults",
|
|
1897
|
+
{
|
|
1898
|
+
async: false,
|
|
1899
|
+
output: workflowOutput,
|
|
1900
|
+
workflowScript: `
|
|
1901
|
+
const children = await runs.all([
|
|
1902
|
+
{ key: "review", agent: "echo", task: "Review" },
|
|
1903
|
+
{ key: "monitor", agent: "echo", task: "Monitor" }
|
|
1904
|
+
]);
|
|
1905
|
+
return children.map(({ key, artifactPaths }) => ({ key, artifactPaths }));
|
|
1906
|
+
`,
|
|
1907
|
+
},
|
|
1908
|
+
new AbortController().signal,
|
|
1909
|
+
undefined,
|
|
1910
|
+
makeMinimalCtx(tempDir),
|
|
1911
|
+
);
|
|
1912
|
+
|
|
1913
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
1914
|
+
assert.match(fs.readFileSync(workflowOutput, "utf-8"), /Workflow completed\./);
|
|
1915
|
+
const value = result.details.workflow?.value as Array<{ key: string; artifactPaths: string[] }>;
|
|
1916
|
+
const childOutputs = value.map((child) => child.artifactPaths.find((candidate) => candidate.endsWith(".md")) ?? "").sort();
|
|
1917
|
+
assert.deepEqual(childOutputs, [
|
|
1918
|
+
path.join(tempDir, "workflow-report.monitor.md"),
|
|
1919
|
+
path.join(tempDir, "workflow-report.review.md"),
|
|
1920
|
+
]);
|
|
1921
|
+
assert.equal(fs.readFileSync(path.join(tempDir, "workflow-report.review.md"), "utf-8"), "first report");
|
|
1922
|
+
assert.equal(fs.readFileSync(path.join(tempDir, "workflow-report.monitor.md"), "utf-8"), "second report");
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
it("uses child-cwd agent output defaults for omitted workflow child output", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1926
|
+
mockPi.onCall({ output: "app report" });
|
|
1927
|
+
const appDir = path.join(tempDir, "packages", "app");
|
|
1928
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
1929
|
+
const rootAgents = [makeAgent("echo", { output: "root-report.md" })];
|
|
1930
|
+
const appAgents = [makeAgent("echo", { output: "app-report.md" })];
|
|
1931
|
+
const executor = makeExecutor(rootAgents, {}, false, undefined, true, new Map(), undefined, undefined, createEventBus(), (cwd) => path.resolve(cwd) === path.resolve(appDir) ? appAgents : rootAgents);
|
|
1932
|
+
|
|
1933
|
+
const result = await executor.execute(
|
|
1934
|
+
"scripted-workflow-child-cwd-omitted-output-default",
|
|
1935
|
+
{
|
|
1936
|
+
async: false,
|
|
1937
|
+
workflowScript: `return await runs.run("app", { agent: "echo", task: "Review app", cwd: "packages/app" });`,
|
|
1938
|
+
},
|
|
1939
|
+
new AbortController().signal,
|
|
1940
|
+
undefined,
|
|
1941
|
+
makeMinimalCtx(tempDir),
|
|
1942
|
+
);
|
|
1943
|
+
|
|
1944
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
1945
|
+
assert.equal(fs.existsSync(path.join(appDir, "app-report.md")), false);
|
|
1946
|
+
assert.ok(result.details.results[0]?.savedOutputPath && pathContainsSegments(result.details.results[0].savedOutputPath, "artifacts", "outputs", "scripted-workflow-child-cwd-omitted-output-default"));
|
|
1947
|
+
assert.equal(path.basename(result.details.results[0]?.savedOutputPath ?? ""), "app-report.md");
|
|
1948
|
+
assert.equal(fs.readFileSync(result.details.results[0]?.savedOutputPath ?? "", "utf-8"), "app report");
|
|
1949
|
+
assert.equal(fs.existsSync(path.join(tempDir, "root-report.md")), false);
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
it("uses child-cwd agent output defaults for workflow output true", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1953
|
+
mockPi.onCall({ output: "app report" });
|
|
1954
|
+
const appDir = path.join(tempDir, "packages", "app");
|
|
1955
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
1956
|
+
const rootAgents = [makeAgent("echo", { output: "root-report.md" })];
|
|
1957
|
+
const appAgents = [makeAgent("echo", { output: "app-report.md" })];
|
|
1958
|
+
const executor = makeExecutor(rootAgents, {}, false, undefined, true, new Map(), undefined, undefined, createEventBus(), (cwd) => path.resolve(cwd) === path.resolve(appDir) ? appAgents : rootAgents);
|
|
1959
|
+
|
|
1960
|
+
const result = await executor.execute(
|
|
1961
|
+
"scripted-workflow-child-cwd-output-default",
|
|
1962
|
+
{
|
|
1963
|
+
async: false,
|
|
1964
|
+
workflowScript: `return await runs.run("app", { agent: "echo", task: "Review app", cwd: "packages/app", output: true });`,
|
|
1965
|
+
},
|
|
1966
|
+
new AbortController().signal,
|
|
1967
|
+
undefined,
|
|
1968
|
+
makeMinimalCtx(tempDir),
|
|
1969
|
+
);
|
|
1970
|
+
|
|
1971
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
1972
|
+
assert.equal(fs.existsSync(path.join(appDir, "app-report.md")), false);
|
|
1973
|
+
assert.ok(result.details.results[0]?.savedOutputPath && pathContainsSegments(result.details.results[0].savedOutputPath, "artifacts", "outputs", "scripted-workflow-child-cwd-output-default"));
|
|
1974
|
+
assert.equal(path.basename(result.details.results[0]?.savedOutputPath ?? ""), "app-report.md");
|
|
1975
|
+
assert.equal(fs.readFileSync(result.details.results[0]?.savedOutputPath ?? "", "utf-8"), "app report");
|
|
1976
|
+
assert.equal(fs.existsSync(path.join(tempDir, "root-report.md")), false);
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1979
|
+
it("reports workflow aggregate output write failures without throwing", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1980
|
+
const outputDir = path.join(tempDir, "aggregate-dir");
|
|
1981
|
+
fs.mkdirSync(outputDir);
|
|
1982
|
+
const result = await makeExecutor([makeAgent("echo")]).execute(
|
|
1983
|
+
"scripted-workflow-aggregate-output-write-error",
|
|
1984
|
+
{
|
|
1985
|
+
async: false,
|
|
1986
|
+
output: outputDir,
|
|
1987
|
+
workflowScript: `return "ok";`,
|
|
1988
|
+
},
|
|
1989
|
+
new AbortController().signal,
|
|
1990
|
+
undefined,
|
|
1991
|
+
makeMinimalCtx(tempDir),
|
|
1992
|
+
);
|
|
1993
|
+
|
|
1994
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
1995
|
+
assert.match(result.content[0]?.text ?? "", /Workflow completed\./);
|
|
1996
|
+
assert.match(result.content[0]?.text ?? "", /Output file error:/);
|
|
1997
|
+
assert.match(result.content[0]?.text ?? "", new RegExp(escapeRegExp(outputDir)));
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
it("routes workflow relative outputs to the run output artifact directory", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2001
|
+
mockPi.onCall({ output: "child report" });
|
|
2002
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
2003
|
+
|
|
2004
|
+
const result = await executor.execute(
|
|
2005
|
+
"scripted-workflow-relative-output-base",
|
|
2006
|
+
{
|
|
2007
|
+
async: false,
|
|
2008
|
+
output: "workflow-summary.md",
|
|
2009
|
+
workflowScript: `return await runs.run("review", { agent: "echo", task: "Review", output: "plans/review.md" });`,
|
|
2010
|
+
},
|
|
2011
|
+
new AbortController().signal,
|
|
2012
|
+
undefined,
|
|
2013
|
+
makeMinimalCtx(tempDir),
|
|
2014
|
+
);
|
|
2015
|
+
|
|
2016
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
2017
|
+
assert.equal(fs.existsSync(path.join(tempDir, "workflow-summary.md")), false);
|
|
2018
|
+
assert.equal(fs.existsSync(path.join(tempDir, "plans", "review.md")), false);
|
|
2019
|
+
const workflowOutputPath = path.join(TEMP_ARTIFACTS_DIR, "outputs", "scripted-workflow-relative-output-base", "workflow-summary.md");
|
|
2020
|
+
assert.match(fs.readFileSync(workflowOutputPath, "utf-8"), /Workflow completed\./);
|
|
2021
|
+
assert.ok(result.details.results[0]?.savedOutputPath && pathContainsSegments(result.details.results[0].savedOutputPath, "artifacts", "outputs", "scripted-workflow-relative-output-base", "plans"));
|
|
2022
|
+
assert.equal(path.basename(result.details.results[0]?.savedOutputPath ?? ""), "review.md");
|
|
2023
|
+
assert.equal(fs.readFileSync(result.details.results[0]?.savedOutputPath ?? "", "utf-8"), "child report");
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
it("rejects workflow child output collisions before launch", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2027
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
2028
|
+
const sharedOutput = path.join(tempDir, "shared.md");
|
|
2029
|
+
|
|
2030
|
+
const duplicate = await executor.execute(
|
|
2031
|
+
"scripted-workflow-duplicate-child-output",
|
|
2032
|
+
{
|
|
2033
|
+
async: false,
|
|
2034
|
+
workflowScript: `return await runs.all([
|
|
2035
|
+
{ key: "review", agent: "echo", task: "Review", output: ${JSON.stringify(sharedOutput)} },
|
|
2036
|
+
{ key: "monitor", agent: "echo", task: "Monitor", output: ${JSON.stringify(sharedOutput)} }
|
|
2037
|
+
]);`,
|
|
2038
|
+
},
|
|
2039
|
+
new AbortController().signal,
|
|
2040
|
+
undefined,
|
|
2041
|
+
makeMinimalCtx(tempDir),
|
|
2042
|
+
);
|
|
2043
|
+
|
|
2044
|
+
assert.equal(duplicate.isError, undefined, duplicate.content[0]?.text ?? "workflow failed");
|
|
2045
|
+
const duplicateChildren = duplicate.details.workflow?.value as Array<{ ok: boolean; error?: string }>;
|
|
2046
|
+
assert.deepEqual(duplicateChildren.map(({ ok }) => ok), [false, false]);
|
|
2047
|
+
for (const child of duplicateChildren) {
|
|
2048
|
+
assert.match(child.error ?? "", /Workflow children 'review' and 'monitor' resolve output to the same path/);
|
|
2049
|
+
assert.match(child.error ?? "", new RegExp(escapeRegExp(sharedOutput)));
|
|
2050
|
+
}
|
|
2051
|
+
assert.equal(mockPi.callCount(), 0);
|
|
2052
|
+
|
|
2053
|
+
const relativeDuplicateOutput = "relative-shared.md";
|
|
2054
|
+
const relativeDuplicate = await executor.execute(
|
|
2055
|
+
"scripted-workflow-relative-duplicate-child-output",
|
|
2056
|
+
{
|
|
2057
|
+
async: false,
|
|
2058
|
+
workflowScript: `return await runs.all([
|
|
2059
|
+
{ key: "review", agent: "echo", task: "Review", output: ${JSON.stringify(relativeDuplicateOutput)} },
|
|
2060
|
+
{ key: "monitor", agent: "echo", task: "Monitor", output: ${JSON.stringify(relativeDuplicateOutput)} }
|
|
2061
|
+
]);`,
|
|
2062
|
+
},
|
|
2063
|
+
new AbortController().signal,
|
|
2064
|
+
undefined,
|
|
2065
|
+
makeMinimalCtx(tempDir),
|
|
2066
|
+
);
|
|
2067
|
+
|
|
2068
|
+
assert.equal(relativeDuplicate.isError, undefined, relativeDuplicate.content[0]?.text ?? "workflow failed");
|
|
2069
|
+
const relativeDuplicateChildren = relativeDuplicate.details.workflow?.value as Array<{ ok: boolean; error?: string }>;
|
|
2070
|
+
assert.deepEqual(relativeDuplicateChildren.map(({ ok }) => ok), [false, false]);
|
|
2071
|
+
for (const child of relativeDuplicateChildren) {
|
|
2072
|
+
assert.match(child.error ?? "", /Workflow children 'review' and 'monitor' resolve output to the same path/);
|
|
2073
|
+
assert.match(child.error ?? "", new RegExp(`${escapeRegExp(TEMP_ARTIFACTS_DIR)}.*outputs.*${escapeRegExp(relativeDuplicateOutput)}`));
|
|
2074
|
+
}
|
|
2075
|
+
assert.equal(mockPi.callCount(), 0);
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
it("isolates colliding inherited agent-default outputs for parallel workflow children", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2079
|
+
mockPi.onCall({ output: "review report", matchArgIncludes: "Review" });
|
|
2080
|
+
mockPi.onCall({ output: "monitor report", matchArgIncludes: "Monitor" });
|
|
2081
|
+
const result = await makeExecutor([makeAgent("echo", { output: "context.md" })]).execute(
|
|
2082
|
+
"scripted-workflow-parallel-inherited-output-collision",
|
|
2083
|
+
{
|
|
2084
|
+
async: false,
|
|
2085
|
+
workflowScript: `return await runs.all([
|
|
2086
|
+
{ key: "review", agent: "echo", task: "Review" },
|
|
2087
|
+
{ key: "monitor", agent: "echo", task: "Monitor" }
|
|
2088
|
+
]);`,
|
|
2089
|
+
},
|
|
2090
|
+
new AbortController().signal,
|
|
2091
|
+
undefined,
|
|
2092
|
+
makeMinimalCtx(tempDir),
|
|
2093
|
+
);
|
|
2094
|
+
|
|
2095
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
2096
|
+
const children = result.details.workflow?.value as Array<{ ok: boolean }>;
|
|
2097
|
+
assert.deepEqual(children.map(({ ok }) => ok), [true, true]);
|
|
2098
|
+
const outputPaths = result.details.results.map(({ savedOutputPath }) => savedOutputPath ?? "").sort();
|
|
2099
|
+
assert.equal(outputPaths.length, 2);
|
|
2100
|
+
assert.notEqual(outputPaths[0], outputPaths[1]);
|
|
2101
|
+
assert.ok(pathContainsSegments(outputPaths[0]!, "artifacts", "outputs"));
|
|
2102
|
+
assert.ok(pathContainsSegments(outputPaths[1]!, "artifacts", "outputs"));
|
|
2103
|
+
assert.match(path.basename(outputPaths[0]!), /^(monitor|review)\.md$/);
|
|
2104
|
+
assert.match(path.basename(outputPaths[1]!), /^(monitor|review)\.md$/);
|
|
2105
|
+
assert.deepEqual(outputPaths.map((outputPath) => fs.readFileSync(outputPath, "utf-8")).sort(), ["monitor report", "review report"]);
|
|
2106
|
+
assert.equal(fs.existsSync(path.join(tempDir, "context.md")), false);
|
|
2107
|
+
});
|
|
2108
|
+
|
|
2109
|
+
it("isolates inherited outputs that collide with a resumed child output", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2110
|
+
const retainedRunId = `retained-output-${Date.now()}`;
|
|
2111
|
+
const retainedAsyncDir = path.join(DIRS.async, retainedRunId);
|
|
2112
|
+
const retainedSessionFile = path.join(tempDir, "retained-session.jsonl");
|
|
2113
|
+
const retainedOutputPath = path.join(tempDir, "context.md");
|
|
2114
|
+
const runFanoutBudget = createRunFanoutBudget(retainedRunId, 10);
|
|
2115
|
+
fs.mkdirSync(retainedAsyncDir, { recursive: true });
|
|
2116
|
+
fs.writeFileSync(retainedSessionFile, "{}\n", "utf-8");
|
|
2117
|
+
fs.writeFileSync(path.join(retainedAsyncDir, "status.json"), JSON.stringify({
|
|
2118
|
+
runId: retainedRunId,
|
|
2119
|
+
sessionId: "session-123",
|
|
2120
|
+
state: "failed",
|
|
2121
|
+
cwd: tempDir,
|
|
2122
|
+
sessionFile: retainedSessionFile,
|
|
2123
|
+
steps: [
|
|
2124
|
+
{ agent: "echo", status: "failed", sessionFile: retainedSessionFile },
|
|
2125
|
+
{ agent: "echo", status: "failed", sessionFile: retainedSessionFile },
|
|
2126
|
+
],
|
|
2127
|
+
}), "utf-8");
|
|
2128
|
+
fs.writeFileSync(path.join(retainedAsyncDir, "recovery-descriptor.json"), JSON.stringify({
|
|
2129
|
+
version: 1,
|
|
2130
|
+
runFanoutBudget,
|
|
2131
|
+
sourceRunId: retainedRunId,
|
|
2132
|
+
agent: "echo",
|
|
2133
|
+
cwd: tempDir,
|
|
2134
|
+
systemPromptMode: "append",
|
|
2135
|
+
inheritProjectContext: true,
|
|
2136
|
+
inheritSkills: true,
|
|
2137
|
+
outputPath: retainedOutputPath,
|
|
2138
|
+
outputMode: "inline",
|
|
2139
|
+
maxSubagentDepth: 1,
|
|
2140
|
+
share: false,
|
|
2141
|
+
}), "utf-8");
|
|
2142
|
+
mockPi.onCall({ output: "resumed report", matchArgIncludes: "Resume" });
|
|
2143
|
+
mockPi.onCall({ output: "review report", matchArgIncludes: "Review" });
|
|
2144
|
+
|
|
2145
|
+
try {
|
|
2146
|
+
const result = await makeExecutor([makeAgent("echo", { output: "context.md" })]).execute(
|
|
2147
|
+
"scripted-workflow-resumed-inherited-output-collision",
|
|
2148
|
+
{
|
|
2149
|
+
async: false,
|
|
2150
|
+
workflowScript: `return await runs.all([
|
|
2151
|
+
{ key: "resume", resume: ${JSON.stringify(retainedRunId)}, index: 1, task: "Resume" },
|
|
2152
|
+
{ key: "review", agent: "echo", task: "Review" }
|
|
2153
|
+
]);`,
|
|
2154
|
+
},
|
|
2155
|
+
new AbortController().signal,
|
|
2156
|
+
undefined,
|
|
2157
|
+
makeMinimalCtx(tempDir),
|
|
2158
|
+
);
|
|
2159
|
+
|
|
2160
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
2161
|
+
const children = result.details.workflow?.value as Array<{ ok: boolean }>;
|
|
2162
|
+
assert.deepEqual(children.map(({ ok }) => ok), [true, true]);
|
|
2163
|
+
assert.equal(fs.readFileSync(retainedOutputPath, "utf-8"), "resumed report");
|
|
2164
|
+
const outputPaths = result.details.results.map(({ savedOutputPath }) => savedOutputPath ?? "");
|
|
2165
|
+
const inheritedOutputPaths = outputPaths.filter((outputPath) => outputPath && outputPath !== retainedOutputPath).sort();
|
|
2166
|
+
assert.deepEqual(inheritedOutputPaths.map((outputPath) => path.basename(outputPath)), ["context.md"]);
|
|
2167
|
+
assert.ok(inheritedOutputPaths.every((outputPath) => pathContainsSegments(outputPath, "artifacts", "outputs")));
|
|
2168
|
+
} finally {
|
|
2169
|
+
fs.rmSync(retainedAsyncDir, { recursive: true, force: true });
|
|
2170
|
+
fs.rmSync(runFanoutBudget.directory, { recursive: true, force: true });
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2173
|
+
|
|
2174
|
+
it("reroutes a later inherited agent-default output collision", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2175
|
+
mockPi.onCall({ output: "first report", matchArgIncludes: "First" });
|
|
2176
|
+
mockPi.onCall({ output: "second report", matchArgIncludes: "Second" });
|
|
2177
|
+
const result = await makeExecutor([makeAgent("echo", { output: "context.md" })]).execute(
|
|
2178
|
+
"scripted-workflow-sequential-inherited-output-collision",
|
|
2179
|
+
{
|
|
2180
|
+
async: false,
|
|
2181
|
+
workflowScript: `
|
|
2182
|
+
const first = await runs.run("first", { agent: "echo", task: "First" });
|
|
2183
|
+
const second = await runs.run("second", { agent: "echo", task: "Second" });
|
|
2184
|
+
return [first, second];
|
|
2185
|
+
`,
|
|
2186
|
+
},
|
|
2187
|
+
new AbortController().signal,
|
|
2188
|
+
undefined,
|
|
2189
|
+
makeMinimalCtx(tempDir),
|
|
2190
|
+
);
|
|
2191
|
+
|
|
2192
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
2193
|
+
const children = result.details.workflow?.value as Array<{ ok: boolean }>;
|
|
2194
|
+
assert.deepEqual(children.map(({ ok }) => ok), [true, true]);
|
|
2195
|
+
assert.equal(fs.existsSync(path.join(tempDir, "context.md")), false);
|
|
2196
|
+
const outputPaths = result.details.results.map(({ savedOutputPath }) => savedOutputPath ?? "");
|
|
2197
|
+
assert.ok(pathContainsSegments(outputPaths[0]!, "artifacts", "outputs"));
|
|
2198
|
+
assert.equal(path.basename(outputPaths[0]!), "context.md");
|
|
2199
|
+
assert.equal(fs.readFileSync(outputPaths[0]!, "utf-8"), "first report");
|
|
2200
|
+
assert.ok(pathContainsSegments(outputPaths[1]!, "artifacts", "outputs"));
|
|
2201
|
+
assert.equal(path.basename(outputPaths[1]!), "second.md");
|
|
2202
|
+
assert.equal(fs.readFileSync(outputPaths[1]!, "utf-8"), "second report");
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
it("preserves a rejected file-only child report when its path matches workflow output", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2206
|
+
const usefulReport = "# Review findings\n\nThe implementation loses the final report.";
|
|
2207
|
+
const sharedOutput = path.join(tempDir, "review.md");
|
|
2208
|
+
mockPi.onCall({ stdoutRaw: `${JSON.stringify(events.assistantMessage(usefulReport))}\n` });
|
|
2209
|
+
const executor = makeExecutor([makeAgent("reviewer", { tools: ["read"], completionGuard: false })]);
|
|
2210
|
+
|
|
2211
|
+
const result = await executor.execute(
|
|
2212
|
+
"scripted-workflow-file-only-acceptance-collision",
|
|
2213
|
+
{
|
|
2214
|
+
async: false,
|
|
2215
|
+
output: sharedOutput,
|
|
2216
|
+
workflowScript: `
|
|
2217
|
+
const child = await runs.run("review", {
|
|
2218
|
+
agent: "reviewer",
|
|
2219
|
+
task: "Write a structured review report.",
|
|
2220
|
+
output: ${JSON.stringify(sharedOutput)},
|
|
2221
|
+
outputMode: "file-only",
|
|
2222
|
+
acceptance: { level: "checked", criteria: ["Return the structured review report"] }
|
|
2223
|
+
});
|
|
2224
|
+
if (!child.ok) throw new Error(child.error);
|
|
2225
|
+
return child;
|
|
2226
|
+
`,
|
|
2227
|
+
},
|
|
2228
|
+
new AbortController().signal,
|
|
2229
|
+
undefined,
|
|
2230
|
+
makeMinimalCtx(tempDir),
|
|
2231
|
+
);
|
|
2232
|
+
|
|
2233
|
+
assert.equal(result.isError, true);
|
|
2234
|
+
assert.match(result.content[0]?.text ?? "", new RegExp(escapeRegExp(sharedOutput)));
|
|
2235
|
+
assert.equal(result.details.results[0]?.acceptance?.status, "rejected");
|
|
2236
|
+
assert.match(result.details.results[0]?.acceptance?.runtimeChecks[0]?.message ?? "", /Structured acceptance report not found/);
|
|
2237
|
+
assert.equal(result.details.results[0]?.savedOutputPath, sharedOutput);
|
|
2238
|
+
assert.deepEqual(fs.readFileSync(sharedOutput), Buffer.from(usefulReport));
|
|
2239
|
+
});
|
|
2240
|
+
|
|
2241
|
+
it("replaces stale workflow output when a child claims its path but writes no report", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2242
|
+
const sharedOutput = path.join(tempDir, "failed-review.md");
|
|
2243
|
+
fs.writeFileSync(sharedOutput, "stale workflow output", "utf-8");
|
|
2244
|
+
mockPi.onCall({ exitCode: 1, stderr: "review child failed before writing output" });
|
|
2245
|
+
const executor = makeExecutor([makeAgent("reviewer", { completionGuard: false })]);
|
|
2246
|
+
|
|
2247
|
+
const result = await executor.execute(
|
|
2248
|
+
"scripted-workflow-missing-child-output-collision",
|
|
2249
|
+
{
|
|
2250
|
+
async: false,
|
|
2251
|
+
output: sharedOutput,
|
|
2252
|
+
workflowScript: `
|
|
2253
|
+
const child = await runs.run("review", {
|
|
2254
|
+
agent: "reviewer",
|
|
2255
|
+
task: "Write a review report.",
|
|
2256
|
+
output: ${JSON.stringify(sharedOutput)},
|
|
2257
|
+
outputMode: "file-only"
|
|
2258
|
+
});
|
|
2259
|
+
if (!child.ok) throw new Error(child.error);
|
|
2260
|
+
return child;
|
|
2261
|
+
`,
|
|
2262
|
+
},
|
|
2263
|
+
new AbortController().signal,
|
|
2264
|
+
undefined,
|
|
2265
|
+
makeMinimalCtx(tempDir),
|
|
2266
|
+
);
|
|
2267
|
+
|
|
2268
|
+
assert.equal(result.isError, true);
|
|
2269
|
+
assert.match(result.content[0]?.text ?? "", /review child failed before writing output/);
|
|
2270
|
+
const workflowOutput = fs.readFileSync(sharedOutput, "utf-8");
|
|
2271
|
+
assert.match(workflowOutput, /Workflow failed:.*review child failed before writing output/s);
|
|
2272
|
+
assert.doesNotMatch(workflowOutput, /stale workflow output/);
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
it("rejects sequential workflow child output collisions before launch", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2276
|
+
mockPi.onCall({ output: "first report" });
|
|
2277
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
2278
|
+
const sharedOutput = path.join(tempDir, "sequential-shared.md");
|
|
2279
|
+
|
|
2280
|
+
const result = await executor.execute(
|
|
2281
|
+
"scripted-workflow-sequential-output-collision",
|
|
2282
|
+
{
|
|
2283
|
+
async: false,
|
|
2284
|
+
workflowScript: `
|
|
2285
|
+
const first = await runs.run("review", { agent: "echo", task: "Review", output: ${JSON.stringify(sharedOutput)} });
|
|
2286
|
+
const second = await runs.run("monitor", { agent: "echo", task: "Monitor", output: ${JSON.stringify(sharedOutput)} })
|
|
2287
|
+
.catch((error) => ({ ok: false, error: error.message }));
|
|
2288
|
+
return [first, second];
|
|
2289
|
+
`,
|
|
2290
|
+
},
|
|
2291
|
+
new AbortController().signal,
|
|
2292
|
+
undefined,
|
|
2293
|
+
makeMinimalCtx(tempDir),
|
|
2294
|
+
);
|
|
2295
|
+
|
|
2296
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
2297
|
+
const children = result.details.workflow?.value as Array<{ ok: boolean; error?: string }>;
|
|
2298
|
+
assert.deepEqual(children.map(({ ok }) => ok), [true, false]);
|
|
2299
|
+
assert.match(children[1]?.error ?? "", /Workflow children 'review' and 'monitor' resolve output to the same path/);
|
|
2300
|
+
assert.match(children[1]?.error ?? "", new RegExp(escapeRegExp(sharedOutput)));
|
|
2301
|
+
assert.equal(mockPi.callCount(), 1);
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
it("checks workflow child output collisions against configured output base", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2305
|
+
const configuredBase = path.join(tempDir, "configured-outputs");
|
|
2306
|
+
const workflowOutput = "shared.md";
|
|
2307
|
+
|
|
2308
|
+
const resolvedSharedOutput = path.join(configuredBase, workflowOutput);
|
|
2309
|
+
const agentDefaultResult = await makeExecutor([makeAgent("echo", { output: workflowOutput })], { singleRunOutputBaseDir: configuredBase }).execute(
|
|
2310
|
+
"scripted-workflow-configured-agent-default-output-collision",
|
|
2311
|
+
{
|
|
2312
|
+
async: false,
|
|
2313
|
+
workflowScript: `return await runs.all([
|
|
2314
|
+
{ key: "review", agent: "echo", task: "Review", output: true },
|
|
2315
|
+
{ key: "monitor", agent: "echo", task: "Monitor", output: true }
|
|
2316
|
+
]);`,
|
|
2317
|
+
},
|
|
2318
|
+
new AbortController().signal,
|
|
2319
|
+
undefined,
|
|
2320
|
+
makeMinimalCtx(tempDir),
|
|
2321
|
+
);
|
|
2322
|
+
|
|
2323
|
+
assert.equal(agentDefaultResult.isError, undefined, agentDefaultResult.content[0]?.text ?? "workflow failed");
|
|
2324
|
+
const agentDefaultChildren = agentDefaultResult.details.workflow?.value as Array<{ ok: boolean; error?: string }>;
|
|
2325
|
+
assert.deepEqual(agentDefaultChildren.map(({ ok }) => ok), [false, false]);
|
|
2326
|
+
for (const child of agentDefaultChildren) {
|
|
2327
|
+
assert.match(child.error ?? "", /Workflow children 'review' and 'monitor' resolve output to the same path/);
|
|
2328
|
+
assert.match(child.error ?? "", new RegExp(escapeRegExp(resolvedSharedOutput)));
|
|
2329
|
+
}
|
|
2330
|
+
assert.equal(mockPi.callCount(), 0);
|
|
2331
|
+
});
|
|
2332
|
+
|
|
2333
|
+
it("lets runs.all siblings settle when one child fails", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2334
|
+
mockPi.onCall({ exitCode: 1, stderr: "first child failed" });
|
|
2335
|
+
mockPi.onCall({ output: "second child completed" });
|
|
2336
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
2337
|
+
|
|
2338
|
+
const result = await executor.execute(
|
|
2339
|
+
"scripted-workflow-settlement",
|
|
2340
|
+
{
|
|
2341
|
+
async: false,
|
|
2342
|
+
workflowScript: `
|
|
2343
|
+
const children = await runs.all([
|
|
2344
|
+
{ key: "first", agent: "echo", task: "First task" },
|
|
2345
|
+
{ key: "second", agent: "echo", task: "Second task" }
|
|
2346
|
+
]);
|
|
2347
|
+
return children.map(({ key, ok, error }) => error === undefined ? { key, ok } : { key, ok, error });
|
|
2348
|
+
`,
|
|
2349
|
+
},
|
|
2350
|
+
new AbortController().signal,
|
|
2351
|
+
undefined,
|
|
2352
|
+
makeMinimalCtx(tempDir),
|
|
2353
|
+
);
|
|
2354
|
+
|
|
2355
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
2356
|
+
assert.equal(mockPi.callCount(), 2);
|
|
2357
|
+
const value = result.details.workflow?.value as Array<{ key: string; ok: boolean; error?: string }>;
|
|
2358
|
+
assert.deepEqual(value.map(({ key }) => key), ["first", "second"]);
|
|
2359
|
+
assert.deepEqual(value.map(({ ok }) => ok).sort(), [false, true]);
|
|
2360
|
+
const failed = value.find(({ ok }) => !ok);
|
|
2361
|
+
const succeeded = value.find(({ ok }) => ok);
|
|
2362
|
+
assert.match(failed?.error ?? "", /first child failed/);
|
|
2363
|
+
assert.equal(succeeded?.error, undefined);
|
|
2364
|
+
assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state !== "started").map(({ state }) => state).sort(), ["completed", "failed"]);
|
|
2365
|
+
});
|
|
2366
|
+
|
|
2367
|
+
it("reports keyed runs.all result access after siblings settle", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2368
|
+
mockPi.onCall({ output: "first child completed", matchArgIncludes: "First task" });
|
|
2369
|
+
mockPi.onCall({ output: "second child completed", matchArgIncludes: "Second task" });
|
|
2370
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
2371
|
+
|
|
2372
|
+
const result = await executor.execute(
|
|
2373
|
+
"scripted-workflow-runs-all-keyed-result-access",
|
|
2374
|
+
{
|
|
2375
|
+
async: false,
|
|
2376
|
+
workflowScript: `
|
|
2377
|
+
const children = await runs.all([
|
|
2378
|
+
{ key: "first", agent: "echo", task: "First task" },
|
|
2379
|
+
{ key: "second", agent: "echo", task: "Second task" }
|
|
2380
|
+
]);
|
|
2381
|
+
return children.first.output;
|
|
2382
|
+
`,
|
|
2383
|
+
},
|
|
2384
|
+
new AbortController().signal,
|
|
2385
|
+
undefined,
|
|
2386
|
+
makeMinimalCtx(tempDir),
|
|
2387
|
+
);
|
|
2388
|
+
|
|
2389
|
+
assert.equal(result.isError, true);
|
|
2390
|
+
assert.equal(mockPi.callCount(), 2);
|
|
2391
|
+
assert.match(result.content[0]?.text ?? "", /runs\.all resolves to an ordered array, not a key map/);
|
|
2392
|
+
assert.match(result.content[0]?.text ?? "", /Use results\[0\], array destructuring, or results\.map/);
|
|
2393
|
+
assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state === "completed").map(({ key }) => key).sort(), ["first", "second"]);
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
it("keeps array access working when runs.all child keys collide with array properties", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2397
|
+
mockPi.onCall({ output: "length child completed", matchArgIncludes: "Length task" });
|
|
2398
|
+
mockPi.onCall({ output: "map child completed", matchArgIncludes: "Map task" });
|
|
2399
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
2400
|
+
|
|
2401
|
+
const result = await executor.execute(
|
|
2402
|
+
"scripted-workflow-runs-all-colliding-key-access",
|
|
2403
|
+
{
|
|
2404
|
+
async: false,
|
|
2405
|
+
workflowScript: `
|
|
2406
|
+
const children = await runs.all([
|
|
2407
|
+
{ key: "length", agent: "echo", task: "Length task" },
|
|
2408
|
+
{ key: "map", agent: "echo", task: "Map task" }
|
|
2409
|
+
]);
|
|
2410
|
+
const [, second] = children;
|
|
2411
|
+
return {
|
|
2412
|
+
length: children.length,
|
|
2413
|
+
first: children[0].output,
|
|
2414
|
+
second: second.output,
|
|
2415
|
+
outputs: children.map((child) => child.output),
|
|
2416
|
+
children
|
|
2417
|
+
};
|
|
2418
|
+
`,
|
|
2419
|
+
},
|
|
1097
2420
|
new AbortController().signal,
|
|
1098
2421
|
undefined,
|
|
1099
|
-
|
|
2422
|
+
makeMinimalCtx(tempDir),
|
|
1100
2423
|
);
|
|
1101
2424
|
|
|
1102
2425
|
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
1103
|
-
|
|
1104
|
-
const
|
|
1105
|
-
assert.
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
assert.notEqual(path.resolve(callCwd), path.resolve(tempDir));
|
|
1114
|
-
assert.equal(path.basename(callCwd), path.basename(managedWorktreeCwd));
|
|
1115
|
-
const sessionHeader = JSON.parse(fs.readFileSync(childSessionFile, "utf-8").split("\n", 1)[0]!) as { cwd?: string };
|
|
1116
|
-
assert.ok(sessionHeader.cwd);
|
|
1117
|
-
assert.equal(path.basename(sessionHeader.cwd), path.basename(callCwd));
|
|
2426
|
+
assert.equal(mockPi.callCount(), 2);
|
|
2427
|
+
const value = result.details.workflow?.value as { length?: number; first?: string; second?: string; outputs?: string[]; children?: Array<{ key?: string; ok?: boolean; output?: string }> } | undefined;
|
|
2428
|
+
assert.equal(value?.length, 2);
|
|
2429
|
+
assert.equal(value?.first, "length child completed");
|
|
2430
|
+
assert.equal(value?.second, "map child completed");
|
|
2431
|
+
assert.deepEqual(value?.outputs, ["length child completed", "map child completed"]);
|
|
2432
|
+
assert.deepEqual(value?.children?.map(({ key, ok, output }) => ({ key, ok, output })), [
|
|
2433
|
+
{ key: "length", ok: true, output: "length child completed" },
|
|
2434
|
+
{ key: "map", ok: true, output: "map child completed" },
|
|
2435
|
+
]);
|
|
1118
2436
|
});
|
|
1119
2437
|
|
|
1120
|
-
it("
|
|
1121
|
-
mockPi.onCall({
|
|
1122
|
-
mockPi.onCall({ output: "second child completed" });
|
|
2438
|
+
it("emits runs.all results as plain arrays", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2439
|
+
mockPi.onCall({ output: "first child completed", matchArgIncludes: "First task" });
|
|
2440
|
+
mockPi.onCall({ output: "second child completed", matchArgIncludes: "Second task" });
|
|
1123
2441
|
const executor = makeExecutor([makeAgent("echo")]);
|
|
1124
2442
|
|
|
1125
2443
|
const result = await executor.execute(
|
|
1126
|
-
"scripted-workflow-
|
|
2444
|
+
"scripted-workflow-runs-all-emitted-array",
|
|
1127
2445
|
{
|
|
1128
2446
|
async: false,
|
|
1129
2447
|
workflowScript: `
|
|
@@ -1131,7 +2449,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1131
2449
|
{ key: "first", agent: "echo", task: "First task" },
|
|
1132
2450
|
{ key: "second", agent: "echo", task: "Second task" }
|
|
1133
2451
|
]);
|
|
1134
|
-
|
|
2452
|
+
emit(children);
|
|
2453
|
+
return children.map((child) => child.output);
|
|
1135
2454
|
`,
|
|
1136
2455
|
},
|
|
1137
2456
|
new AbortController().signal,
|
|
@@ -1141,14 +2460,11 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1141
2460
|
|
|
1142
2461
|
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
1143
2462
|
assert.equal(mockPi.callCount(), 2);
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
assert.match(failed?.error ?? "", /first child failed/);
|
|
1150
|
-
assert.equal(succeeded?.error, undefined);
|
|
1151
|
-
assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state !== "started").map(({ state }) => state).sort(), ["completed", "failed"]);
|
|
2463
|
+
assert.deepEqual((result.details.workflow?.emits[0] as Array<{ key: string; output: string }>).map(({ key, output }) => ({ key, output })), [
|
|
2464
|
+
{ key: "first", output: "first child completed" },
|
|
2465
|
+
{ key: "second", output: "second child completed" },
|
|
2466
|
+
]);
|
|
2467
|
+
assert.deepEqual(result.details.workflow?.value, ["first child completed", "second child completed"]);
|
|
1152
2468
|
});
|
|
1153
2469
|
|
|
1154
2470
|
it("rejects an over-limit runs.all batch before launching any workflow child", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
@@ -1314,85 +2630,392 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1314
2630
|
assert.match(result.content[0]?.text ?? "", /handoffs/);
|
|
1315
2631
|
});
|
|
1316
2632
|
|
|
1317
|
-
it("preserves a workflow worktree when its child detaches for supervisor coordination", { skip: !createSubagentExecutor
|
|
1318
|
-
execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
|
|
1319
|
-
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
|
|
1320
|
-
execFileSync("git", ["config", "user.name", "Test User"], { cwd: tempDir });
|
|
1321
|
-
fs.writeFileSync(path.join(tempDir, "base.txt"), "base\n", "utf-8");
|
|
1322
|
-
execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
|
|
1323
|
-
execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
|
|
2633
|
+
it("preserves a workflow worktree when its child detaches for supervisor coordination", { skip: !createSubagentExecutor ? "executor unavailable" : undefined }, async () => {
|
|
2634
|
+
execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
|
|
2635
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
|
|
2636
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd: tempDir });
|
|
2637
|
+
fs.writeFileSync(path.join(tempDir, "base.txt"), "base\n", "utf-8");
|
|
2638
|
+
execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
|
|
2639
|
+
execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
|
|
2640
|
+
mockPi.onCall({
|
|
2641
|
+
steps: [
|
|
2642
|
+
{ jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
|
|
2643
|
+
{ delay: 500, jsonl: [events.assistantMessage("done after coordination")] },
|
|
2644
|
+
],
|
|
2645
|
+
});
|
|
2646
|
+
const piEvents = createEventBus();
|
|
2647
|
+
const executor = makeExecutor(
|
|
2648
|
+
[makeAgent("worker", { systemPrompt: "Intercom orchestration channel:" })],
|
|
2649
|
+
{},
|
|
2650
|
+
false,
|
|
2651
|
+
undefined,
|
|
2652
|
+
true,
|
|
2653
|
+
new Map(),
|
|
2654
|
+
undefined,
|
|
2655
|
+
undefined,
|
|
2656
|
+
piEvents,
|
|
2657
|
+
);
|
|
2658
|
+
let detachAccepted = false;
|
|
2659
|
+
piEvents.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => {
|
|
2660
|
+
if ((payload as { requestId?: unknown }).requestId === "workflow-worktree-detach") {
|
|
2661
|
+
detachAccepted ||= (payload as { accepted?: unknown }).accepted === true;
|
|
2662
|
+
}
|
|
2663
|
+
});
|
|
2664
|
+
const detachTimer = setInterval(() => {
|
|
2665
|
+
if (!detachAccepted) piEvents.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "workflow-worktree-detach" });
|
|
2666
|
+
}, 10);
|
|
2667
|
+
detachTimer.unref();
|
|
2668
|
+
|
|
2669
|
+
const result = await executor.execute(
|
|
2670
|
+
"scripted-workflow-detached-worktree",
|
|
2671
|
+
{
|
|
2672
|
+
async: false,
|
|
2673
|
+
workflowScript: `
|
|
2674
|
+
const children = await runs.all([
|
|
2675
|
+
{ key: "detaches", agent: "worker", task: "Ask then continue", worktree: true }
|
|
2676
|
+
]);
|
|
2677
|
+
return children.map(({ key, ok, artifactPaths }) => ({ key, ok, artifactPaths }));
|
|
2678
|
+
`,
|
|
2679
|
+
},
|
|
2680
|
+
new AbortController().signal,
|
|
2681
|
+
undefined,
|
|
2682
|
+
makeMinimalCtx(tempDir),
|
|
2683
|
+
);
|
|
2684
|
+
clearInterval(detachTimer);
|
|
2685
|
+
|
|
2686
|
+
assert.equal(detachAccepted, true);
|
|
2687
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
2688
|
+
assert.match(result.content[0]?.text ?? "", /run detaches: detached/);
|
|
2689
|
+
const workflowValue = result.details.workflow?.value as Array<{ ok: boolean; artifactPaths: string[] }>;
|
|
2690
|
+
assert.equal(workflowValue[0]?.ok, false);
|
|
2691
|
+
const handoffPath = workflowValue[0]?.artifactPaths.find((candidate) => candidate.endsWith(".json"));
|
|
2692
|
+
assert.ok(handoffPath, result.content[0]?.text ?? "missing pending handoff");
|
|
2693
|
+
const handoff = JSON.parse(fs.readFileSync(handoffPath, "utf-8")) as {
|
|
2694
|
+
groups: Array<{
|
|
2695
|
+
cleanup: { state: string; tasks: Array<{ path: string; branch: string; preserved: boolean; worktreeRemoved: boolean; branchRemoved: boolean }> };
|
|
2696
|
+
}>;
|
|
2697
|
+
};
|
|
2698
|
+
const cleanup = handoff.groups[0]?.cleanup;
|
|
2699
|
+
assert.equal(cleanup?.state, "partial");
|
|
2700
|
+
assert.equal(cleanup?.tasks[0]?.preserved, true);
|
|
2701
|
+
assert.equal(cleanup?.tasks[0]?.worktreeRemoved, false);
|
|
2702
|
+
assert.equal(cleanup?.tasks[0]?.branchRemoved, false);
|
|
2703
|
+
const worktreePath = cleanup?.tasks[0]?.path;
|
|
2704
|
+
const branch = cleanup?.tasks[0]?.branch;
|
|
2705
|
+
assert.ok(worktreePath);
|
|
2706
|
+
assert.ok(branch);
|
|
2707
|
+
assert.equal(fs.existsSync(worktreePath), true, "live detached worktree must remain present");
|
|
2708
|
+
|
|
2709
|
+
await new Promise((resolve) => setTimeout(resolve, 750));
|
|
2710
|
+
execFileSync("git", ["worktree", "remove", "--force", worktreePath], { cwd: tempDir });
|
|
2711
|
+
execFileSync("git", ["branch", "-D", branch], { cwd: tempDir, stdio: "ignore" });
|
|
2712
|
+
});
|
|
2713
|
+
|
|
2714
|
+
it("pauses an async workflow when its child detaches for supervisor coordination", { skip: !createSubagentExecutor ? "executor unavailable" : undefined }, async () => {
|
|
2715
|
+
execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
|
|
2716
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
|
|
2717
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd: tempDir });
|
|
2718
|
+
fs.writeFileSync(path.join(tempDir, "base.txt"), "base\n", "utf-8");
|
|
2719
|
+
execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
|
|
2720
|
+
execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
|
|
2721
|
+
mockPi.onCall({
|
|
2722
|
+
steps: [
|
|
2723
|
+
{ jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
|
|
2724
|
+
{ delay: 500, jsonl: [events.assistantMessage("done after coordination")] },
|
|
2725
|
+
],
|
|
2726
|
+
});
|
|
2727
|
+
const piEvents = createEventBus();
|
|
2728
|
+
const asyncJobs: SubagentState["asyncJobs"] = new Map();
|
|
2729
|
+
const executor = makeExecutor(
|
|
2730
|
+
[makeAgent("worker", { systemPrompt: "Intercom orchestration channel:" })],
|
|
2731
|
+
{},
|
|
2732
|
+
false,
|
|
2733
|
+
undefined,
|
|
2734
|
+
true,
|
|
2735
|
+
asyncJobs,
|
|
2736
|
+
undefined,
|
|
2737
|
+
undefined,
|
|
2738
|
+
piEvents,
|
|
2739
|
+
);
|
|
2740
|
+
let detachAccepted = false;
|
|
2741
|
+
piEvents.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => {
|
|
2742
|
+
if ((payload as { requestId?: unknown }).requestId === "async-workflow-detach") {
|
|
2743
|
+
detachAccepted ||= (payload as { accepted?: unknown }).accepted === true;
|
|
2744
|
+
}
|
|
2745
|
+
});
|
|
2746
|
+
const detachTimer = setInterval(() => {
|
|
2747
|
+
if (!detachAccepted) piEvents.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "async-workflow-detach" });
|
|
2748
|
+
}, 10);
|
|
2749
|
+
detachTimer.unref();
|
|
2750
|
+
|
|
2751
|
+
const started = await executor.execute(
|
|
2752
|
+
"async-scripted-workflow-detached-worktree",
|
|
2753
|
+
{
|
|
2754
|
+
workflowScript: `
|
|
2755
|
+
const child = await runs.run("detaches", { agent: "worker", task: "Ask then continue", worktree: true });
|
|
2756
|
+
return child.output;
|
|
2757
|
+
`,
|
|
2758
|
+
},
|
|
2759
|
+
new AbortController().signal,
|
|
2760
|
+
undefined,
|
|
2761
|
+
makeMinimalCtx(tempDir),
|
|
2762
|
+
);
|
|
2763
|
+
assert.equal(started.isError, undefined);
|
|
2764
|
+
assert.ok(started.details.asyncId);
|
|
2765
|
+
assert.ok(started.details.asyncDir);
|
|
2766
|
+
const workflowRunId = started.details.asyncId;
|
|
2767
|
+
const statusPath = path.join(started.details.asyncDir, "status.json");
|
|
2768
|
+
const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
|
|
2769
|
+
const activeMarkerPath = path.join(DIRS.async, ACTIVE_RUN_INDEX_DIR, workflowRunId);
|
|
2770
|
+
|
|
2771
|
+
let status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
2772
|
+
for (let attempt = 0; attempt < 150 && status.state !== "paused" && status.state !== "failed"; attempt++) {
|
|
2773
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
2774
|
+
status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
2775
|
+
}
|
|
2776
|
+
clearInterval(detachTimer);
|
|
2777
|
+
|
|
2778
|
+
assert.equal(detachAccepted, true);
|
|
2779
|
+
assert.equal(status.state, "paused", status.error);
|
|
2780
|
+
assert.equal(status.activityState, "needs_attention");
|
|
2781
|
+
assert.match(status.error ?? "", /detached for intercom coordination/i);
|
|
2782
|
+
assert.equal(status.workflow?.trace.some((entry) => entry.key === "detaches" && entry.state === "detached"), true);
|
|
2783
|
+
assert.equal(status.workflow?.trace.some((entry) => entry.key === "detaches" && entry.state === "failed"), false);
|
|
2784
|
+
assert.equal(status.steps?.[0]?.status, "paused");
|
|
2785
|
+
assert.equal(status.steps?.[0]?.activityState, "needs_attention");
|
|
2786
|
+
assert.equal(asyncJobs.get(workflowRunId)?.status, "paused");
|
|
2787
|
+
assert.equal(asyncJobs.get(workflowRunId)?.activityState, "needs_attention");
|
|
2788
|
+
assert.equal(fs.existsSync(activeMarkerPath), false);
|
|
2789
|
+
|
|
2790
|
+
let persistedResult = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as {
|
|
2791
|
+
state?: string;
|
|
2792
|
+
activityState?: string;
|
|
2793
|
+
error?: string;
|
|
2794
|
+
summary?: string;
|
|
2795
|
+
workflow?: { trace?: Array<{ key?: string; state?: string }> };
|
|
2796
|
+
results?: Array<{ detached?: boolean; artifactPaths?: { outputPath?: string } }>;
|
|
2797
|
+
};
|
|
2798
|
+
assert.equal(persistedResult.state, "paused");
|
|
2799
|
+
assert.equal(persistedResult.activityState, "needs_attention");
|
|
2800
|
+
assert.match(persistedResult.error ?? "", /Reply to the supervisor request first/);
|
|
2801
|
+
assert.match(persistedResult.summary ?? "", /subagent_wait/);
|
|
2802
|
+
assert.equal(persistedResult.workflow?.trace?.some((entry) => entry.key === "detaches" && entry.state === "detached"), true);
|
|
2803
|
+
assert.equal(persistedResult.workflow?.trace?.some((entry) => entry.key === "detaches" && entry.state === "failed"), false);
|
|
2804
|
+
assert.equal(persistedResult.results?.[0]?.detached, true);
|
|
2805
|
+
const handoffPath = persistedResult.results?.[0]?.artifactPaths?.outputPath;
|
|
2806
|
+
assert.ok(handoffPath, "missing preserved worktree handoff path");
|
|
2807
|
+
const handoff = JSON.parse(fs.readFileSync(handoffPath, "utf-8")) as {
|
|
2808
|
+
groups: Array<{
|
|
2809
|
+
cleanup: { state: string; tasks: Array<{ path: string; branch: string; preserved: boolean; worktreeRemoved: boolean; branchRemoved: boolean }> };
|
|
2810
|
+
}>;
|
|
2811
|
+
};
|
|
2812
|
+
const cleanup = handoff.groups[0]?.cleanup;
|
|
2813
|
+
assert.equal(cleanup?.state, "partial");
|
|
2814
|
+
assert.equal(cleanup?.tasks[0]?.preserved, true);
|
|
2815
|
+
assert.equal(cleanup?.tasks[0]?.worktreeRemoved, false);
|
|
2816
|
+
assert.equal(cleanup?.tasks[0]?.branchRemoved, false);
|
|
2817
|
+
const worktreePath = cleanup?.tasks[0]?.path;
|
|
2818
|
+
const branch = cleanup?.tasks[0]?.branch;
|
|
2819
|
+
assert.ok(worktreePath);
|
|
2820
|
+
assert.ok(branch);
|
|
2821
|
+
assert.equal(fs.existsSync(worktreePath), true, "live detached worktree must remain present");
|
|
2822
|
+
|
|
2823
|
+
let reconciled: AsyncStatus | undefined;
|
|
2824
|
+
for (let attempt = 0; attempt < 150; attempt++) {
|
|
2825
|
+
reconciled = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
2826
|
+
if (reconciled.state === "complete" || reconciled.state === "failed") break;
|
|
2827
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
2828
|
+
}
|
|
2829
|
+
assert.equal(reconciled?.state, "failed", reconciled?.error);
|
|
2830
|
+
assert.match(reconciled?.error ?? "", /unsupported-continuation/);
|
|
2831
|
+
assert.equal(reconciled?.activityState, undefined);
|
|
2832
|
+
assert.equal(reconciled?.steps?.[0]?.status, "completed");
|
|
2833
|
+
assert.equal(reconciled?.steps?.[0]?.activityState, undefined);
|
|
2834
|
+
assert.equal(asyncJobs.get(workflowRunId)?.status, "failed");
|
|
2835
|
+
|
|
2836
|
+
persistedResult = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
2837
|
+
assert.equal(persistedResult.state, "failed");
|
|
2838
|
+
assert.equal(persistedResult.activityState, undefined);
|
|
2839
|
+
assert.match(persistedResult.error ?? "", /unsupported-continuation/);
|
|
2840
|
+
execFileSync("git", ["worktree", "remove", "--force", worktreePath], { cwd: tempDir });
|
|
2841
|
+
execFileSync("git", ["branch", "-D", branch], { cwd: tempDir, stdio: "ignore" });
|
|
2842
|
+
fs.rmSync(started.details.asyncDir, { recursive: true, force: true });
|
|
2843
|
+
fs.rmSync(resultPath, { force: true });
|
|
2844
|
+
});
|
|
2845
|
+
|
|
2846
|
+
it("keeps async workflows failed when a detached child is mixed with a real failure", { skip: !createSubagentExecutor ? "executor unavailable" : undefined }, async () => {
|
|
2847
|
+
mockPi.onCall({
|
|
2848
|
+
matchArgIncludes: "Ask then continue",
|
|
2849
|
+
steps: [
|
|
2850
|
+
{ jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
|
|
2851
|
+
{ delay: 500, jsonl: [events.assistantMessage("done after coordination")] },
|
|
2852
|
+
],
|
|
2853
|
+
});
|
|
2854
|
+
mockPi.onCall({ matchArgIncludes: "Fail for real", exitCode: 1, stderr: "real child failure" });
|
|
2855
|
+
const piEvents = createEventBus();
|
|
2856
|
+
const asyncJobs: SubagentState["asyncJobs"] = new Map();
|
|
2857
|
+
const executor = makeExecutor(
|
|
2858
|
+
[makeAgent("worker", { systemPrompt: "Intercom orchestration channel:" })],
|
|
2859
|
+
{},
|
|
2860
|
+
false,
|
|
2861
|
+
undefined,
|
|
2862
|
+
true,
|
|
2863
|
+
asyncJobs,
|
|
2864
|
+
undefined,
|
|
2865
|
+
undefined,
|
|
2866
|
+
piEvents,
|
|
2867
|
+
);
|
|
2868
|
+
let detachAccepted = false;
|
|
2869
|
+
piEvents.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => {
|
|
2870
|
+
if ((payload as { requestId?: unknown }).requestId === "async-workflow-detach-with-failure") {
|
|
2871
|
+
detachAccepted ||= (payload as { accepted?: unknown }).accepted === true;
|
|
2872
|
+
}
|
|
2873
|
+
});
|
|
2874
|
+
const detachTimer = setInterval(() => {
|
|
2875
|
+
if (!detachAccepted) piEvents.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "async-workflow-detach-with-failure" });
|
|
2876
|
+
}, 10);
|
|
2877
|
+
detachTimer.unref();
|
|
2878
|
+
|
|
2879
|
+
const started = await executor.execute(
|
|
2880
|
+
"async-scripted-workflow-detached-and-failed",
|
|
2881
|
+
{
|
|
2882
|
+
workflowScript: `
|
|
2883
|
+
await runs.all([
|
|
2884
|
+
{ key: "detaches", agent: "worker", task: "Ask then continue" },
|
|
2885
|
+
{ key: "fails", agent: "worker", task: "Fail for real" }
|
|
2886
|
+
]);
|
|
2887
|
+
throw new Error("manual hard failure");
|
|
2888
|
+
`,
|
|
2889
|
+
},
|
|
2890
|
+
new AbortController().signal,
|
|
2891
|
+
undefined,
|
|
2892
|
+
makeMinimalCtx(tempDir),
|
|
2893
|
+
);
|
|
2894
|
+
assert.equal(started.isError, undefined);
|
|
2895
|
+
assert.ok(started.details.asyncId);
|
|
2896
|
+
assert.ok(started.details.asyncDir);
|
|
2897
|
+
const workflowRunId = started.details.asyncId;
|
|
2898
|
+
const statusPath = path.join(started.details.asyncDir, "status.json");
|
|
2899
|
+
const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
|
|
2900
|
+
|
|
2901
|
+
let status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
2902
|
+
for (let attempt = 0; attempt < 150 && status.state !== "failed" && status.state !== "paused"; attempt++) {
|
|
2903
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
2904
|
+
status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
2905
|
+
}
|
|
2906
|
+
clearInterval(detachTimer);
|
|
2907
|
+
|
|
2908
|
+
assert.equal(detachAccepted, true);
|
|
2909
|
+
assert.equal(status.state, "failed");
|
|
2910
|
+
assert.equal(status.activityState, undefined);
|
|
2911
|
+
assert.match(status.error ?? "", /manual hard failure/);
|
|
2912
|
+
assert.equal(status.workflow?.trace.some((entry) => entry.key === "detaches" && entry.state === "detached"), true);
|
|
2913
|
+
assert.equal(status.workflow?.trace.some((entry) => entry.key === "fails" && entry.state === "failed"), true);
|
|
2914
|
+
assert.equal(status.steps?.find((step) => step.workflowKey === "detaches")?.status, "paused");
|
|
2915
|
+
assert.equal(status.steps?.find((step) => step.workflowKey === "detaches")?.activityState, "needs_attention");
|
|
2916
|
+
assert.equal(status.steps?.find((step) => step.workflowKey === "fails")?.status, "failed");
|
|
2917
|
+
assert.equal(asyncJobs.get(workflowRunId)?.status, "failed");
|
|
2918
|
+
assert.equal(asyncJobs.get(workflowRunId)?.activityState, undefined);
|
|
2919
|
+
|
|
2920
|
+
let persistedResult = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as {
|
|
2921
|
+
state?: string;
|
|
2922
|
+
activityState?: string;
|
|
2923
|
+
error?: string;
|
|
2924
|
+
workflow?: { trace?: Array<{ key?: string; state?: string }> };
|
|
2925
|
+
results?: Array<{ workflowKey?: string; detached?: boolean; success?: boolean }>;
|
|
2926
|
+
};
|
|
2927
|
+
assert.equal(persistedResult.state, "failed");
|
|
2928
|
+
assert.equal(persistedResult.activityState, undefined);
|
|
2929
|
+
assert.match(persistedResult.error ?? "", /manual hard failure/);
|
|
2930
|
+
assert.equal(persistedResult.workflow?.trace?.some((entry) => entry.key === "detaches" && entry.state === "detached"), true);
|
|
2931
|
+
assert.equal(persistedResult.workflow?.trace?.some((entry) => entry.key === "fails" && entry.state === "failed"), true);
|
|
2932
|
+
assert.equal(persistedResult.results?.find((entry) => entry.workflowKey === "detaches")?.detached, true);
|
|
2933
|
+
assert.equal(persistedResult.results?.find((entry) => entry.workflowKey === "fails")?.success, false);
|
|
2934
|
+
|
|
2935
|
+
await new Promise((resolve) => setTimeout(resolve, 750));
|
|
2936
|
+
persistedResult = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
2937
|
+
assert.equal(persistedResult.state, "failed", "real workflow failure must not be overwritten by detached child completion");
|
|
2938
|
+
fs.rmSync(started.details.asyncDir, { recursive: true, force: true });
|
|
2939
|
+
fs.rmSync(resultPath, { force: true });
|
|
2940
|
+
});
|
|
2941
|
+
|
|
2942
|
+
it("fails closed after a detached child finishes while a sibling is aborted", { skip: !createSubagentExecutor ? "executor unavailable" : undefined }, async () => {
|
|
1324
2943
|
mockPi.onCall({
|
|
2944
|
+
matchArgIncludes: "Ask then continue",
|
|
1325
2945
|
steps: [
|
|
1326
2946
|
{ jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
|
|
1327
2947
|
{ delay: 500, jsonl: [events.assistantMessage("done after coordination")] },
|
|
1328
2948
|
],
|
|
1329
2949
|
});
|
|
2950
|
+
mockPi.onCall({ matchArgIncludes: "Stay running", delay: 10_000 });
|
|
1330
2951
|
const piEvents = createEventBus();
|
|
2952
|
+
const asyncJobs: SubagentState["asyncJobs"] = new Map();
|
|
1331
2953
|
const executor = makeExecutor(
|
|
1332
2954
|
[makeAgent("worker", { systemPrompt: "Intercom orchestration channel:" })],
|
|
1333
2955
|
{},
|
|
1334
2956
|
false,
|
|
1335
2957
|
undefined,
|
|
1336
2958
|
true,
|
|
1337
|
-
|
|
2959
|
+
asyncJobs,
|
|
1338
2960
|
undefined,
|
|
1339
2961
|
undefined,
|
|
1340
2962
|
piEvents,
|
|
1341
2963
|
);
|
|
1342
2964
|
let detachAccepted = false;
|
|
1343
2965
|
piEvents.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => {
|
|
1344
|
-
if ((payload as { requestId?: unknown }).requestId === "workflow-
|
|
2966
|
+
if ((payload as { requestId?: unknown }).requestId === "async-workflow-detach-with-sibling") {
|
|
1345
2967
|
detachAccepted ||= (payload as { accepted?: unknown }).accepted === true;
|
|
1346
2968
|
}
|
|
1347
2969
|
});
|
|
1348
2970
|
const detachTimer = setInterval(() => {
|
|
1349
|
-
if (!detachAccepted) piEvents.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "workflow-
|
|
2971
|
+
if (!detachAccepted) piEvents.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "async-workflow-detach-with-sibling" });
|
|
1350
2972
|
}, 10);
|
|
1351
2973
|
detachTimer.unref();
|
|
1352
2974
|
|
|
1353
|
-
const
|
|
1354
|
-
"scripted-workflow-detached-
|
|
2975
|
+
const started = await executor.execute(
|
|
2976
|
+
"async-scripted-workflow-detached-with-sibling",
|
|
1355
2977
|
{
|
|
1356
|
-
async: false,
|
|
1357
2978
|
workflowScript: `
|
|
1358
|
-
|
|
1359
|
-
|
|
2979
|
+
await Promise.all([
|
|
2980
|
+
runs.run("detaches", { agent: "worker", task: "Ask then continue" }),
|
|
2981
|
+
runs.run("slow", { agent: "worker", task: "Stay running" })
|
|
1360
2982
|
]);
|
|
1361
|
-
return
|
|
2983
|
+
return "should not reach";
|
|
1362
2984
|
`,
|
|
1363
2985
|
},
|
|
1364
2986
|
new AbortController().signal,
|
|
1365
2987
|
undefined,
|
|
1366
2988
|
makeMinimalCtx(tempDir),
|
|
1367
2989
|
);
|
|
1368
|
-
|
|
2990
|
+
assert.equal(started.isError, undefined);
|
|
2991
|
+
assert.ok(started.details.asyncDir);
|
|
2992
|
+
const workflowRunId = started.details.asyncId;
|
|
2993
|
+
const statusPath = path.join(started.details.asyncDir, "status.json");
|
|
1369
2994
|
|
|
2995
|
+
let status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
2996
|
+
for (let attempt = 0; attempt < 150 && status.state !== "paused" && status.state !== "failed" && status.state !== "complete"; attempt++) {
|
|
2997
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
2998
|
+
status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
2999
|
+
}
|
|
3000
|
+
clearInterval(detachTimer);
|
|
1370
3001
|
assert.equal(detachAccepted, true);
|
|
1371
|
-
assert.equal(
|
|
1372
|
-
assert.
|
|
1373
|
-
|
|
1374
|
-
assert.equal(workflowValue[0]?.ok, false);
|
|
1375
|
-
const handoffPath = workflowValue[0]?.artifactPaths.find((candidate) => candidate.endsWith(".json"));
|
|
1376
|
-
assert.ok(handoffPath, result.content[0]?.text ?? "missing pending handoff");
|
|
1377
|
-
const handoff = JSON.parse(fs.readFileSync(handoffPath, "utf-8")) as {
|
|
1378
|
-
groups: Array<{
|
|
1379
|
-
cleanup: { state: string; tasks: Array<{ path: string; branch: string; preserved: boolean; worktreeRemoved: boolean; branchRemoved: boolean }> };
|
|
1380
|
-
}>;
|
|
1381
|
-
};
|
|
1382
|
-
const cleanup = handoff.groups[0]?.cleanup;
|
|
1383
|
-
assert.equal(cleanup?.state, "partial");
|
|
1384
|
-
assert.equal(cleanup?.tasks[0]?.preserved, true);
|
|
1385
|
-
assert.equal(cleanup?.tasks[0]?.worktreeRemoved, false);
|
|
1386
|
-
assert.equal(cleanup?.tasks[0]?.branchRemoved, false);
|
|
1387
|
-
const worktreePath = cleanup?.tasks[0]?.path;
|
|
1388
|
-
const branch = cleanup?.tasks[0]?.branch;
|
|
1389
|
-
assert.ok(worktreePath);
|
|
1390
|
-
assert.ok(branch);
|
|
1391
|
-
assert.equal(fs.existsSync(worktreePath), true, "live detached worktree must remain present");
|
|
3002
|
+
assert.equal(status.state, "paused", status.error);
|
|
3003
|
+
assert.equal(status.steps?.find((step) => step.workflowKey === "detaches")?.status, "paused");
|
|
3004
|
+
assert.equal(status.steps?.find((step) => step.workflowKey === "slow")?.status, "stopped");
|
|
1392
3005
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
3006
|
+
let reconciled: AsyncStatus | undefined;
|
|
3007
|
+
for (let attempt = 0; attempt < 150; attempt++) {
|
|
3008
|
+
reconciled = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
3009
|
+
if (reconciled.state === "complete" || reconciled.state === "failed") break;
|
|
3010
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3011
|
+
}
|
|
3012
|
+
assert.equal(reconciled?.state, "failed", reconciled?.error);
|
|
3013
|
+
assert.match(reconciled?.error ?? "", /unsupported-continuation/);
|
|
3014
|
+
assert.equal(reconciled?.steps?.find((step) => step.workflowKey === "detaches")?.status, "completed");
|
|
3015
|
+
assert.equal(reconciled?.steps?.find((step) => step.workflowKey === "slow")?.status, "stopped");
|
|
3016
|
+
assert.equal(asyncJobs.get(workflowRunId)?.status, "failed");
|
|
3017
|
+
fs.rmSync(started.details.asyncDir, { recursive: true, force: true });
|
|
3018
|
+
fs.rmSync(path.join(DIRS.results, `${workflowRunId}.json`), { force: true });
|
|
1396
3019
|
});
|
|
1397
3020
|
|
|
1398
3021
|
it("inherits workflow-level worktree isolation and allows a child opt-out", { skip: !createSubagentExecutor || process.platform === "win32" ? "executor unavailable or worktree paths differ on Windows" : undefined }, async () => {
|
|
@@ -1663,6 +3286,45 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1663
3286
|
assert.match(result.content[0]?.text ?? "", /Valid:/);
|
|
1664
3287
|
});
|
|
1665
3288
|
|
|
3289
|
+
it("routes watchdog.configure through the management action path", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
3290
|
+
const gpt = { provider: "openai-codex", id: "gpt-5.5", reasoning: true };
|
|
3291
|
+
const opus = { provider: "anthropic", id: "claude-opus-4-8", reasoning: true };
|
|
3292
|
+
const models = [gpt, opus];
|
|
3293
|
+
const watchdog = new MainWatchdogRuntime({ cwd: tempDir });
|
|
3294
|
+
const executor = createSubagentExecutor!({
|
|
3295
|
+
pi: { events: createEventBus(), getSessionName: () => undefined },
|
|
3296
|
+
state: { baseCwd: tempDir, currentSessionId: null, asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null },
|
|
3297
|
+
config: {},
|
|
3298
|
+
asyncByDefault: false,
|
|
3299
|
+
watchdog,
|
|
3300
|
+
tempArtifactsDir: tempDir,
|
|
3301
|
+
getSubagentSessionRoot: () => tempDir,
|
|
3302
|
+
expandTilde: (value: string) => value,
|
|
3303
|
+
discoverAgents: () => ({ agents: [makeAgent("echo")] }),
|
|
3304
|
+
});
|
|
3305
|
+
const ctx = {
|
|
3306
|
+
...makeMinimalCtx(tempDir),
|
|
3307
|
+
model: gpt,
|
|
3308
|
+
modelRegistry: {
|
|
3309
|
+
getAvailable: () => models,
|
|
3310
|
+
find: (provider: string, id: string) => models.find((model) => model.provider === provider && model.id === id),
|
|
3311
|
+
hasConfiguredAuth: (model: unknown) => Boolean(model),
|
|
3312
|
+
},
|
|
3313
|
+
};
|
|
3314
|
+
|
|
3315
|
+
const result = await executor.execute(
|
|
3316
|
+
"watchdog-configure",
|
|
3317
|
+
{ action: "watchdog.configure", model: "recommended" },
|
|
3318
|
+
new AbortController().signal,
|
|
3319
|
+
undefined,
|
|
3320
|
+
ctx,
|
|
3321
|
+
);
|
|
3322
|
+
|
|
3323
|
+
assert.equal(result.isError, undefined);
|
|
3324
|
+
assert.match(result.content[0]?.text ?? "", /session model configured: anthropic\/claude-opus-4-8:high/);
|
|
3325
|
+
assert.equal(watchdog.getSnapshot(tempDir).config.main.model, "anthropic/claude-opus-4-8");
|
|
3326
|
+
});
|
|
3327
|
+
|
|
1666
3328
|
it("rejects duplicate concurrent subagent execution calls", async () => {
|
|
1667
3329
|
mockPi.onCall({ output: "first call completed", delay: 100 });
|
|
1668
3330
|
const executor = makeExecutor([makeAgent("echo")]);
|
|
@@ -1794,7 +3456,14 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1794
3456
|
} satisfies SubagentDelegationRequest;
|
|
1795
3457
|
|
|
1796
3458
|
try {
|
|
1797
|
-
|
|
3459
|
+
const previousChildEnv = process.env[SUBAGENT_CHILD_ENV];
|
|
3460
|
+
delete process.env[SUBAGENT_CHILD_ENV];
|
|
3461
|
+
try {
|
|
3462
|
+
registerSubagentExtension(fakePi as never);
|
|
3463
|
+
} finally {
|
|
3464
|
+
if (previousChildEnv === undefined) delete process.env[SUBAGENT_CHILD_ENV];
|
|
3465
|
+
else process.env[SUBAGENT_CHILD_ENV] = previousChildEnv;
|
|
3466
|
+
}
|
|
1798
3467
|
for (const handler of runtimeHandlers.get("session_start") ?? []) {
|
|
1799
3468
|
await handler({ reason: "startup" }, ctx);
|
|
1800
3469
|
}
|
|
@@ -1913,7 +3582,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1913
3582
|
const claims = fs.readdirSync(path.join(descriptor.directory, "claims"));
|
|
1914
3583
|
assert.equal(claims.length, 1);
|
|
1915
3584
|
const claim = JSON.parse(fs.readFileSync(path.join(descriptor.directory, "claims", claims[0]!), "utf-8")) as { path: string };
|
|
1916
|
-
assert.match(claim.path, /^tasks\[0\]\/[
|
|
3585
|
+
assert.match(claim.path, /^tasks\[0\]\/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\/single$/);
|
|
1917
3586
|
} finally {
|
|
1918
3587
|
if (previous === undefined) delete process.env[RUN_FANOUT_BUDGET_ENV];
|
|
1919
3588
|
else process.env[RUN_FANOUT_BUDGET_ENV] = previous;
|
|
@@ -2134,23 +3803,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2134
3803
|
assert.equal(mockPi.callCount(), 1);
|
|
2135
3804
|
});
|
|
2136
3805
|
|
|
2137
|
-
it("allows intentional parallel tasks inside one subagent execution call", async () => {
|
|
2138
|
-
mockPi.onCall({ output: "first parallel result" });
|
|
2139
|
-
mockPi.onCall({ output: "second parallel result" });
|
|
2140
|
-
const executor = makeExecutor([makeAgent("echo"), makeAgent("second")]);
|
|
2141
|
-
|
|
2142
|
-
const result = await executor.execute(
|
|
2143
|
-
"parallel",
|
|
2144
|
-
{ tasks: [{ agent: "echo", task: "First task" }, { agent: "second", task: "Second task" }] },
|
|
2145
|
-
new AbortController().signal,
|
|
2146
|
-
undefined,
|
|
2147
|
-
makeMinimalCtx(tempDir),
|
|
2148
|
-
);
|
|
2149
3806
|
|
|
2150
|
-
assert.equal(result.isError, undefined);
|
|
2151
|
-
assert.equal(mockPi.callCount(), 2);
|
|
2152
|
-
assert.deepEqual(result.details?.totalCost, { inputTokens: 200, outputTokens: 100, costUsd: 0.002 });
|
|
2153
|
-
});
|
|
2154
3807
|
|
|
2155
3808
|
it("reports total cost for foreground single runs", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2156
3809
|
mockPi.onCall({ output: "single result" });
|
|
@@ -2253,57 +3906,18 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2253
3906
|
}
|
|
2254
3907
|
});
|
|
2255
3908
|
|
|
2256
|
-
it("
|
|
2257
|
-
mockPi.onCall({ output: "
|
|
2258
|
-
const
|
|
2259
|
-
|
|
2260
|
-
const result = await executor.execute(
|
|
2261
|
-
"foreground-usage-budget",
|
|
2262
|
-
{
|
|
2263
|
-
chain: [
|
|
2264
|
-
{ agent: "echo", task: "First task" },
|
|
2265
|
-
{ agent: "second", task: "Second task" },
|
|
2266
|
-
],
|
|
2267
|
-
usageBudget: { tokens: { hard: 10 } },
|
|
2268
|
-
},
|
|
2269
|
-
new AbortController().signal,
|
|
2270
|
-
undefined,
|
|
2271
|
-
makeMinimalCtx(tempDir),
|
|
2272
|
-
);
|
|
2273
|
-
|
|
2274
|
-
assert.equal(result.isError, true);
|
|
2275
|
-
assert.match(result.content[0]?.text ?? "", /Usage budget exhausted/);
|
|
2276
|
-
assert.equal(mockPi.callCount(), 1);
|
|
2277
|
-
assert.equal(result.details?.usageBudget?.exhausted, true);
|
|
2278
|
-
assert.equal(result.details?.usageBudget?.reason, "tokens");
|
|
2279
|
-
});
|
|
2280
|
-
|
|
2281
|
-
it("blocks queued foreground parallel children when hard reported usage is exhausted", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2282
|
-
mockPi.onCall({ output: "first result" });
|
|
2283
|
-
const executor = makeExecutor([makeAgent("echo"), makeAgent("second")]);
|
|
3909
|
+
it("rejects implementation runs without mutation-capable tools before spawn", async () => {
|
|
3910
|
+
mockPi.onCall({ output: "should not spawn" });
|
|
3911
|
+
const agents = [makeAgent("worker", { tools: ["read", "grep", "find", "ls", "contact_supervisor"] })];
|
|
2284
3912
|
|
|
2285
|
-
const result = await
|
|
2286
|
-
"
|
|
2287
|
-
|
|
2288
|
-
tasks: [
|
|
2289
|
-
{ agent: "echo", task: "First task" },
|
|
2290
|
-
{ agent: "second", task: "Second task" },
|
|
2291
|
-
],
|
|
2292
|
-
concurrency: 1,
|
|
2293
|
-
usageBudget: { tokens: { hard: 10 } },
|
|
2294
|
-
},
|
|
2295
|
-
new AbortController().signal,
|
|
2296
|
-
undefined,
|
|
2297
|
-
makeMinimalCtx(tempDir),
|
|
2298
|
-
);
|
|
3913
|
+
const result = await runSync(tempDir, agents, "worker", "Implement the approved file changes", {
|
|
3914
|
+
runId: "readonly-contract-run",
|
|
3915
|
+
});
|
|
2299
3916
|
|
|
2300
|
-
assert.equal(result.
|
|
2301
|
-
assert.
|
|
2302
|
-
assert.equal(
|
|
2303
|
-
|
|
2304
|
-
assert.match(result.details?.results[1]?.error ?? "", /Usage budget exhausted/);
|
|
2305
|
-
assert.equal(result.details?.usageBudget?.exhausted, true);
|
|
2306
|
-
});
|
|
3917
|
+
assert.equal(result.exitCode, 1);
|
|
3918
|
+
assert.match(result.error ?? "", /no mutation-capable tools/);
|
|
3919
|
+
assert.equal(mockPi.callCount(), 0);
|
|
3920
|
+
});
|
|
2307
3921
|
|
|
2308
3922
|
it("fails implementation runs that complete without mutation attempts", async () => {
|
|
2309
3923
|
mockPi.onCall({ output: "Validation:\nlet rawFilename = params.filename.trim();" });
|
|
@@ -2406,6 +4020,133 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2406
4020
|
if (child?.artifactPaths?.outputPath) assert.match(fs.readFileSync(child.artifactPaths.outputPath, "utf-8"), /"note": "captured"/);
|
|
2407
4021
|
});
|
|
2408
4022
|
|
|
4023
|
+
it("workflow children with outputSchema can satisfy inherited checked acceptance", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
4024
|
+
const acceptanceReport = {
|
|
4025
|
+
criteriaSatisfied: [{ id: "proof", status: "satisfied", evidence: "structured output returned ok true" }],
|
|
4026
|
+
changedFiles: ["none"],
|
|
4027
|
+
testsAddedOrUpdated: ["none"],
|
|
4028
|
+
commandsRun: [{ command: "not run", result: "not-run", summary: "mock structured-output child" }],
|
|
4029
|
+
validationOutput: ["mock output validated"],
|
|
4030
|
+
residualRisks: ["none"],
|
|
4031
|
+
noStagedFiles: true,
|
|
4032
|
+
diffSummary: "no file changes",
|
|
4033
|
+
};
|
|
4034
|
+
mockPi.onCall({
|
|
4035
|
+
stdoutRaw: [
|
|
4036
|
+
{ type: "tool_execution_start", toolName: "structured_output", args: { value: { ok: true }, acceptanceReport } },
|
|
4037
|
+
{ type: "tool_result_end", message: { role: "toolResult", toolName: "structured_output", content: [{ type: "text", text: "Structured output captured." }] } },
|
|
4038
|
+
{ type: "tool_execution_end", toolName: "structured_output" },
|
|
4039
|
+
].map((entry) => JSON.stringify(entry)).join("\n") + "\n",
|
|
4040
|
+
structuredOutputCapture: { ok: true },
|
|
4041
|
+
structuredOutputAcceptanceReport: acceptanceReport,
|
|
4042
|
+
});
|
|
4043
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
4044
|
+
|
|
4045
|
+
const result = await executor.execute(
|
|
4046
|
+
"workflow-schema-acceptance-sidecar",
|
|
4047
|
+
{
|
|
4048
|
+
async: false,
|
|
4049
|
+
acceptance: { level: "checked", criteria: [{ id: "proof", must: "Return required proof" }] },
|
|
4050
|
+
workflowScript: `
|
|
4051
|
+
const child = await runs.run("schema", {
|
|
4052
|
+
agent: "echo",
|
|
4053
|
+
task: "Return structured data",
|
|
4054
|
+
outputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } }
|
|
4055
|
+
});
|
|
4056
|
+
if (!child.ok) throw new Error(child.error);
|
|
4057
|
+
return child;
|
|
4058
|
+
`,
|
|
4059
|
+
},
|
|
4060
|
+
new AbortController().signal,
|
|
4061
|
+
undefined,
|
|
4062
|
+
makeMinimalCtx(tempDir),
|
|
4063
|
+
);
|
|
4064
|
+
|
|
4065
|
+
const child = result.details.results[0];
|
|
4066
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
4067
|
+
assert.deepEqual(child?.structuredOutput, { ok: true });
|
|
4068
|
+
assert.equal(child?.acceptance?.status, "checked");
|
|
4069
|
+
assert.equal(child?.acceptance?.runtimeChecks.some((check) => check.status === "failed"), false);
|
|
4070
|
+
});
|
|
4071
|
+
|
|
4072
|
+
it("rejects workflow outputSchema children that omit checked acceptance sidecars", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
4073
|
+
mockPi.onCall({
|
|
4074
|
+
stdoutRaw: [
|
|
4075
|
+
{ type: "tool_execution_start", toolName: "structured_output", args: { value: { ok: true } } },
|
|
4076
|
+
{ type: "tool_result_end", message: { role: "toolResult", toolName: "structured_output", content: [{ type: "text", text: "Structured output captured." }] } },
|
|
4077
|
+
{ type: "tool_execution_end", toolName: "structured_output" },
|
|
4078
|
+
].map((entry) => JSON.stringify(entry)).join("\n") + "\n",
|
|
4079
|
+
structuredOutputCapture: { ok: true },
|
|
4080
|
+
});
|
|
4081
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
4082
|
+
|
|
4083
|
+
const result = await executor.execute(
|
|
4084
|
+
"workflow-schema-acceptance-missing-sidecar",
|
|
4085
|
+
{
|
|
4086
|
+
async: false,
|
|
4087
|
+
acceptance: { level: "checked", criteria: [{ id: "proof", must: "Return required proof" }] },
|
|
4088
|
+
workflowScript: `
|
|
4089
|
+
const child = await runs.run("schema", {
|
|
4090
|
+
agent: "echo",
|
|
4091
|
+
task: "Return structured data",
|
|
4092
|
+
outputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } }
|
|
4093
|
+
});
|
|
4094
|
+
if (!child.ok) throw new Error(child.error);
|
|
4095
|
+
return child;
|
|
4096
|
+
`,
|
|
4097
|
+
},
|
|
4098
|
+
new AbortController().signal,
|
|
4099
|
+
undefined,
|
|
4100
|
+
makeMinimalCtx(tempDir),
|
|
4101
|
+
);
|
|
4102
|
+
|
|
4103
|
+
const child = result.details.results[0];
|
|
4104
|
+
assert.equal(result.isError, true);
|
|
4105
|
+
assert.equal(child?.exitCode, 1);
|
|
4106
|
+
assert.equal(child?.structuredOutput?.ok, true);
|
|
4107
|
+
assert.equal(child?.acceptance?.status, "rejected");
|
|
4108
|
+
assert.match(result.content[0]?.text ?? "", /acceptance/i);
|
|
4109
|
+
});
|
|
4110
|
+
|
|
4111
|
+
it("surfaces corrupt outputSchema acceptance sidecar read errors", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
4112
|
+
mockPi.onCall({
|
|
4113
|
+
stdoutRaw: [
|
|
4114
|
+
{ type: "tool_execution_start", toolName: "structured_output", args: { value: { ok: true } } },
|
|
4115
|
+
{ type: "tool_result_end", message: { role: "toolResult", toolName: "structured_output", content: [{ type: "text", text: "Structured output captured." }] } },
|
|
4116
|
+
{ type: "tool_execution_end", toolName: "structured_output" },
|
|
4117
|
+
].map((entry) => JSON.stringify(entry)).join("\n") + "\n",
|
|
4118
|
+
structuredOutputCapture: { ok: true },
|
|
4119
|
+
structuredOutputAcceptanceReportRaw: "{not-json",
|
|
4120
|
+
});
|
|
4121
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
4122
|
+
|
|
4123
|
+
const result = await executor.execute(
|
|
4124
|
+
"workflow-schema-acceptance-corrupt-sidecar",
|
|
4125
|
+
{
|
|
4126
|
+
async: false,
|
|
4127
|
+
acceptance: { level: "checked", criteria: [{ id: "proof", must: "Return required proof" }] },
|
|
4128
|
+
workflowScript: `
|
|
4129
|
+
const child = await runs.run("schema", {
|
|
4130
|
+
agent: "echo",
|
|
4131
|
+
task: "Return structured data",
|
|
4132
|
+
outputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } }
|
|
4133
|
+
});
|
|
4134
|
+
if (!child.ok) throw new Error(child.error);
|
|
4135
|
+
return child;
|
|
4136
|
+
`,
|
|
4137
|
+
},
|
|
4138
|
+
new AbortController().signal,
|
|
4139
|
+
undefined,
|
|
4140
|
+
makeMinimalCtx(tempDir),
|
|
4141
|
+
);
|
|
4142
|
+
|
|
4143
|
+
const child = result.details.results[0];
|
|
4144
|
+
assert.equal(result.isError, true);
|
|
4145
|
+
assert.equal(child?.structuredOutput?.ok, true);
|
|
4146
|
+
assert.match(child?.error ?? "", /Failed to read structured output acceptance report/);
|
|
4147
|
+
assert.doesNotMatch(child?.error ?? "", /Structured acceptance report not found/);
|
|
4148
|
+
});
|
|
4149
|
+
|
|
2409
4150
|
it("accepts recovered tool errors before valid structured output but rejects later errors", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2410
4151
|
const recoveredError = { type: "tool_result_end", message: { role: "toolResult", toolName: "read", isError: true, content: [{ type: "text", text: "EISDIR" }] } };
|
|
2411
4152
|
const structuredEvents = [
|
|
@@ -2642,7 +4383,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2642
4383
|
|
|
2643
4384
|
const result = await runSync(tempDir, agents, "echo", "Investigate behavior", {
|
|
2644
4385
|
runId: "run-active",
|
|
2645
|
-
controlConfig: { enabled: true, activeNoticeAfterTurns: 2, activeNoticeAfterMs: 999_999, activeNoticeAfterTokens: 999_999, notifyOn: ["active_long_running", "
|
|
4386
|
+
controlConfig: { enabled: true, activeNoticeAfterTurns: 2, activeNoticeAfterMs: 999_999, activeNoticeAfterTokens: 999_999, notifyOn: ["active_long_running", "needs_attention"] },
|
|
2646
4387
|
onControlEvent: (event: NonNullable<RunSyncResult["controlEvents"]>[number]) => controlEvents.push(event),
|
|
2647
4388
|
});
|
|
2648
4389
|
|
|
@@ -2699,7 +4440,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2699
4440
|
|
|
2700
4441
|
const result = await runSync(tempDir, agents, "echo", "Investigate behavior", {
|
|
2701
4442
|
runId: "run-control-disabled",
|
|
2702
|
-
controlConfig: { enabled: false, activeNoticeAfterTurns: 1, activeNoticeAfterMs: 1, activeNoticeAfterTokens: 1, notifyOn: ["active_long_running", "
|
|
4443
|
+
controlConfig: { enabled: false, activeNoticeAfterTurns: 1, activeNoticeAfterMs: 1, activeNoticeAfterTokens: 1, notifyOn: ["active_long_running", "needs_attention"] },
|
|
2703
4444
|
onControlEvent: (event: NonNullable<RunSyncResult["controlEvents"]>[number]) => controlEvents.push(event),
|
|
2704
4445
|
});
|
|
2705
4446
|
|
|
@@ -2783,6 +4524,26 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2783
4524
|
assert.equal(mockPi.callCount(), 1);
|
|
2784
4525
|
});
|
|
2785
4526
|
|
|
4527
|
+
it("stops model fallback and flags contextOverflow when the input exceeds the context window", async () => {
|
|
4528
|
+
mockPi.onCall({ output: "", stderr: "This model's maximum context length is 8192 tokens", exitCode: 1 });
|
|
4529
|
+
mockPi.onCall({ output: "must not run" });
|
|
4530
|
+
const agents = [makeAgent("worker", {
|
|
4531
|
+
model: "openai/gpt-5-mini",
|
|
4532
|
+
fallbackModels: ["anthropic/claude-sonnet-4"],
|
|
4533
|
+
})];
|
|
4534
|
+
|
|
4535
|
+
const result = await runSync(tempDir, agents, "worker", "Summarize a huge file", {
|
|
4536
|
+
runId: "context-overflow-stops-fallback",
|
|
4537
|
+
acceptance: false,
|
|
4538
|
+
});
|
|
4539
|
+
|
|
4540
|
+
assert.equal(result.exitCode, 1);
|
|
4541
|
+
assert.equal(result.contextOverflow, true);
|
|
4542
|
+
assert.equal(result.modelAttempts?.length, 1);
|
|
4543
|
+
assert.equal(mockPi.callCount(), 1);
|
|
4544
|
+
assert.ok(result.error?.includes("context"), "error should mention context overflow");
|
|
4545
|
+
});
|
|
4546
|
+
|
|
2786
4547
|
it("does not retry non-SIGKILL signaled child exits", { skip: process.platform === "win32" ? "POSIX child signal reporting is unavailable on Windows" : false }, async () => {
|
|
2787
4548
|
mockPi.onCall({ signal: "SIGTERM" });
|
|
2788
4549
|
mockPi.onCall({ output: "must not run" });
|
|
@@ -2875,6 +4636,32 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2875
4636
|
assert.equal(result.model, "anthropic/claude-sonnet-4");
|
|
2876
4637
|
});
|
|
2877
4638
|
|
|
4639
|
+
it("fails when a configured provider-qualified model starts on a different child model", async () => {
|
|
4640
|
+
mockPi.onCall({ jsonl: [events.assistantMessage("wrong provider", "openai-codex/gpt-5.6-sol")] });
|
|
4641
|
+
const agents = [makeAgent("echo", { model: "opencode-go/ox-alpha-free:max" })];
|
|
4642
|
+
|
|
4643
|
+
const result = await runSync(tempDir, agents, "echo", "Task", {
|
|
4644
|
+
runId: "foreground-model-verification-mismatch",
|
|
4645
|
+
acceptance: false,
|
|
4646
|
+
availableModels: [
|
|
4647
|
+
{ provider: "opencode-go", id: "ox-alpha-free", fullId: "opencode-go/ox-alpha-free" },
|
|
4648
|
+
{ provider: "openai-codex", id: "gpt-5.6-sol", fullId: "openai-codex/gpt-5.6-sol" },
|
|
4649
|
+
],
|
|
4650
|
+
});
|
|
4651
|
+
|
|
4652
|
+
assert.equal(result.exitCode, 1);
|
|
4653
|
+
assert.equal(result.model, "opencode-go/ox-alpha-free:max");
|
|
4654
|
+
assert.deepEqual(result.attemptedModels, ["opencode-go/ox-alpha-free:max"]);
|
|
4655
|
+
assert.match(result.error ?? "", /model_verification_failed/);
|
|
4656
|
+
assert.match(result.error ?? "", /Expected 'opencode-go\/ox-alpha-free:max'/);
|
|
4657
|
+
assert.match(result.error ?? "", /observed 'openai-codex\/gpt-5\.6-sol'/);
|
|
4658
|
+
assert.equal(result.modelAttempts?.[0]?.success, false);
|
|
4659
|
+
assert.match(result.modelAttempts?.[0]?.error ?? "", /model_verification_failed/);
|
|
4660
|
+
const args = readAllCallArgs()[0]!;
|
|
4661
|
+
assert.equal(args[args.indexOf("--model") + 1], "opencode-go/ox-alpha-free:max");
|
|
4662
|
+
assert.equal(mockPi.callCount(), 1);
|
|
4663
|
+
});
|
|
4664
|
+
|
|
2878
4665
|
it("model override from options takes precedence", async () => {
|
|
2879
4666
|
mockPi.onCall({ output: "Done" });
|
|
2880
4667
|
const agents = [makeAgent("echo", { model: "anthropic/claude-sonnet-4" })];
|
|
@@ -2887,6 +4674,18 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2887
4674
|
assert.equal(result.model, "openai/gpt-4o");
|
|
2888
4675
|
});
|
|
2889
4676
|
|
|
4677
|
+
it("rejects an unresolved agent model before spawning Pi", async () => {
|
|
4678
|
+
const agents = [makeAgent("echo", { model: "fast" })];
|
|
4679
|
+
|
|
4680
|
+
await assert.rejects(
|
|
4681
|
+
runSync(tempDir, agents, "echo", "Task", {
|
|
4682
|
+
availableModels: [{ provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" }],
|
|
4683
|
+
}),
|
|
4684
|
+
/Unknown subagent model 'fast'/,
|
|
4685
|
+
);
|
|
4686
|
+
assert.equal(mockPi.callCount(), 0);
|
|
4687
|
+
});
|
|
4688
|
+
|
|
2890
4689
|
it("prefers the parent session provider for ambiguous bare model ids", async () => {
|
|
2891
4690
|
mockPi.onCall({ output: "Done" });
|
|
2892
4691
|
const agents = [makeAgent("echo", { model: "gpt-5-mini" })];
|
|
@@ -3168,9 +4967,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
3168
4967
|
|
|
3169
4968
|
assert.equal(result.exitCode, 1);
|
|
3170
4969
|
assert.match(result.error ?? "", /429 quota exceeded/);
|
|
3171
|
-
|
|
3172
|
-
// model up to MODEL_RETRY_DELAYS_MS.length times before giving up.
|
|
3173
|
-
assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, false, false, false]);
|
|
4970
|
+
assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false]);
|
|
3174
4971
|
});
|
|
3175
4972
|
|
|
3176
4973
|
it("treats recovered child tool errors as successful foreground runs", async () => {
|
|
@@ -3278,9 +5075,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
3278
5075
|
});
|
|
3279
5076
|
|
|
3280
5077
|
assert.equal(result.exitCode, 1);
|
|
3281
|
-
|
|
3282
|
-
// retried on the same model up to MODEL_RETRY_DELAYS_MS.length times.
|
|
3283
|
-
assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, false, false, false, false]);
|
|
5078
|
+
assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, false]);
|
|
3284
5079
|
assert.match(result.error ?? "", /429 quota exceeded/);
|
|
3285
5080
|
});
|
|
3286
5081
|
|
|
@@ -3425,14 +5220,14 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
3425
5220
|
|
|
3426
5221
|
const result = await runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
|
|
3427
5222
|
runId: "delayed-tool-attention",
|
|
3428
|
-
controlConfig: { enabled: true, needsAttentionAfterMs: 200, activeNoticeAfterMs: 999_999, notifyOn: ["
|
|
5223
|
+
controlConfig: { enabled: true, needsAttentionAfterMs: 200, activeNoticeAfterMs: 999_999, notifyOn: ["needs_attention"] },
|
|
3429
5224
|
onUpdate: (update: { details?: { progress?: ProgressSummary[] } }) => updates.push(update),
|
|
3430
5225
|
onControlEvent: (event: NonNullable<RunSyncResult["controlEvents"]>[number]) => controlEvents.push(event),
|
|
3431
5226
|
});
|
|
3432
5227
|
|
|
3433
5228
|
assert.equal(result.exitCode, 0);
|
|
3434
5229
|
assert.equal(result.progress.activityState, undefined);
|
|
3435
|
-
assert.equal(controlEvents.some((event) => event.type === "
|
|
5230
|
+
assert.equal(controlEvents.some((event) => event.type === "needs_attention"), false);
|
|
3436
5231
|
assert.equal(updates.some((update) => update.details?.progress?.some((progress) => progress.currentTool === "bash")), true);
|
|
3437
5232
|
});
|
|
3438
5233
|
|
|
@@ -3603,29 +5398,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
3603
5398
|
assert.equal(fs.existsSync(path.join(tempDir, ".pi-subagents", "artifacts")), false);
|
|
3604
5399
|
});
|
|
3605
5400
|
|
|
3606
|
-
for (const artifactDir of ["session", "temp"] as const) {
|
|
3607
|
-
it(`keeps foreground chain scratch files out of the project for artifactDir=${artifactDir}`, { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
3608
|
-
mockPi.onCall({ output: `${artifactDir} chain result` });
|
|
3609
|
-
const sessionFile = path.join(tempDir, "sessions", "parent-session", "session.jsonl");
|
|
3610
|
-
const ctx = makeMinimalCtx(tempDir);
|
|
3611
|
-
ctx.sessionManager.getSessionFile = () => sessionFile;
|
|
3612
|
-
const executor = makeExecutor([makeAgent("echo")], { artifactDir });
|
|
3613
|
-
|
|
3614
|
-
const result = await executor.execute(
|
|
3615
|
-
`${artifactDir}-chain-artifact-dir`,
|
|
3616
|
-
{ chain: [{ agent: "echo", task: "Run without project-local scratch files in {chain_dir}" }] },
|
|
3617
|
-
new AbortController().signal,
|
|
3618
|
-
undefined,
|
|
3619
|
-
ctx,
|
|
3620
|
-
);
|
|
3621
5401
|
|
|
3622
|
-
const taskArg = readCallArgs().at(-1) ?? "";
|
|
3623
|
-
assert.equal(result.isError, undefined);
|
|
3624
|
-
assert.ok(taskArg.includes(`${CHAIN_RUNS_DIR}${path.sep}`), taskArg);
|
|
3625
|
-
assert.equal(fs.existsSync(path.join(tempDir, ".pi-subagents", "chain-runs")), false);
|
|
3626
|
-
assert.equal(fs.existsSync(path.join(tempDir, ".pi-subagents", "artifacts")), false);
|
|
3627
|
-
});
|
|
3628
|
-
}
|
|
3629
5402
|
|
|
3630
5403
|
it("writes a failure stub to foreground output artifacts when no output was produced", async () => {
|
|
3631
5404
|
mockPi.onCall({ output: "", stderr: "model unavailable", exitCode: 1 });
|
|
@@ -3745,7 +5518,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
3745
5518
|
|
|
3746
5519
|
const taskArg = readCallArgs().at(-1) ?? "";
|
|
3747
5520
|
assert.equal(result.isError, undefined);
|
|
3748
|
-
assert.match(taskArg, new RegExp(`Write your findings to exactly this path: ${escapeRegExp(path.join(
|
|
5521
|
+
assert.match(taskArg, new RegExp(`Write your findings to exactly this path: ${escapeRegExp(path.join(TEMP_ARTIFACTS_DIR, "outputs"))}.*context\\.md`));
|
|
3749
5522
|
assert.equal(fs.existsSync(path.join(tempDir, "context.md")), false);
|
|
3750
5523
|
});
|
|
3751
5524
|
|
|
@@ -4368,6 +6141,21 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
4368
6141
|
assert.equal(result.modelAttempts?.length, 1);
|
|
4369
6142
|
});
|
|
4370
6143
|
|
|
6144
|
+
it("records blocked mutation effects when foreground implementation tools are missing", async () => {
|
|
6145
|
+
mockPi.onCall({ output: "I cannot edit because fixture_search is missing", missingTools: ["fixture_search"] });
|
|
6146
|
+
const agents = [makeAgent("worker", { tools: ["read", "fixture_search"] })];
|
|
6147
|
+
|
|
6148
|
+
const result = await runSync(tempDir, agents, "worker", "Implement the requested source fix", { runId: "missing-implementation-tool" });
|
|
6149
|
+
|
|
6150
|
+
assert.equal(result.exitCode, 1);
|
|
6151
|
+
assert.match(result.error ?? "", /requested unavailable child tools: fixture_search/);
|
|
6152
|
+
assert.doesNotMatch(result.error ?? "", /completed without making edits/);
|
|
6153
|
+
assert.equal(result.effects?.fileMutation?.status, "blocked");
|
|
6154
|
+
assert.equal(result.effects?.fileMutation?.expected, true);
|
|
6155
|
+
assert.equal(result.effects?.fileMutation?.attempted, false);
|
|
6156
|
+
assert.match(result.effects?.fileMutation?.message ?? "", /requested unavailable child tools: fixture_search/);
|
|
6157
|
+
});
|
|
6158
|
+
|
|
4371
6159
|
it("passes custom tool extensions through even when explicit extensions are allowlisted", { skip: process.platform === "win32" ? "extension path resolution intermittent on Windows CI" : undefined }, async () => {
|
|
4372
6160
|
mockPi.onCall({ output: "Done" });
|
|
4373
6161
|
const agents = [makeAgent("echo", {
|
|
@@ -5591,6 +7379,20 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
5591
7379
|
assert.equal(firstDetachResponse, true);
|
|
5592
7380
|
});
|
|
5593
7381
|
|
|
7382
|
+
it("returns actionable guidance for ambient extension registration conflicts", async () => {
|
|
7383
|
+
mockPi.onCall({
|
|
7384
|
+
exitCode: 1,
|
|
7385
|
+
stderr: 'Error: Failed to load extension "/tmp/pi-mcp-adapter-clone/index.ts": Tool "mcpScript" conflicts with /tmp/pi-mcp-adapter/index.ts',
|
|
7386
|
+
});
|
|
7387
|
+
const agents = makeAgentConfigs(["echo"]);
|
|
7388
|
+
|
|
7389
|
+
const result = await runSync(tempDir, agents, "echo", "Task", {});
|
|
7390
|
+
|
|
7391
|
+
assert.equal(result.exitCode, 1);
|
|
7392
|
+
assert.match(result.error ?? "", /loaded conflicting ambient Pi extensions/);
|
|
7393
|
+
assert.match(result.error ?? "", /"echo":\{"extensions":\[\]\}/);
|
|
7394
|
+
});
|
|
7395
|
+
|
|
5594
7396
|
it("handles stderr without exit code as info (not error)", async () => {
|
|
5595
7397
|
mockPi.onCall({ output: "Success", stderr: "Warning: something", exitCode: 0 });
|
|
5596
7398
|
const agents = makeAgentConfigs(["echo"]);
|