@selesai/code 0.8.3 → 0.8.5
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 +15 -0
- package/README.md +7 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/defaults/models.json +41 -55
- package/dist/defaults/settings.json +7 -8
- package/dist/extensions/handoff-new.ts +1 -1
- package/dist/extensions/inline-skills.test.ts +1 -0
- package/dist/extensions/inline-skills.ts +1 -1
- package/dist/extensions/pi-intercom/CHANGELOG.md +25 -3
- package/dist/extensions/pi-intercom/README.md +34 -15
- package/dist/extensions/pi-intercom/broker/broker.ts +38 -141
- package/dist/extensions/pi-intercom/broker/client-liveness.test.ts +173 -0
- package/dist/extensions/pi-intercom/broker/client.ts +78 -150
- package/dist/extensions/pi-intercom/broker/extension-state.ts +18 -7
- package/dist/extensions/pi-intercom/broker/framing.ts +1 -1
- package/dist/extensions/pi-intercom/broker/protocol.ts +182 -0
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +106 -7
- package/dist/extensions/pi-intercom/broker/spawn.ts +37 -11
- package/dist/extensions/pi-intercom/config.test.ts +8 -10
- package/dist/extensions/pi-intercom/config.ts +3 -3
- package/dist/extensions/pi-intercom/index.ts +218 -147
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +891 -103
- package/dist/extensions/pi-intercom/package.json +3 -2
- package/dist/extensions/pi-intercom/project-agent.test.ts +145 -0
- package/dist/extensions/pi-intercom/project-agent.ts +324 -0
- package/dist/extensions/pi-intercom/reply-tracker.test.ts +86 -1
- package/dist/extensions/pi-intercom/reply-tracker.ts +41 -9
- package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +49 -9
- package/dist/extensions/pi-intercom/test/inline-message.test.ts +9 -9
- package/dist/extensions/pi-intercom/types.ts +3 -1
- package/dist/extensions/pi-intercom/ui/inline-message.ts +7 -7
- package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +2 -2
- package/dist/extensions/pi-powerline-footer/CHANGELOG.md +14 -1
- package/dist/extensions/pi-powerline-footer/README.md +20 -37
- package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +40 -25
- package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +149 -18
- package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +1 -0
- package/dist/extensions/pi-powerline-footer/editor-performance.ts +99 -0
- package/dist/extensions/pi-powerline-footer/git-status.ts +15 -0
- package/dist/extensions/pi-powerline-footer/index.ts +394 -317
- package/dist/extensions/pi-powerline-footer/package.json +1 -1
- package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -12
- package/dist/extensions/pi-powerline-footer/queue/store.ts +8 -56
- package/dist/extensions/pi-powerline-footer/queue/types.ts +1 -2
- package/dist/extensions/pi-powerline-footer/segments.ts +0 -3
- package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +231 -1
- package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +5 -19
- package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +43 -0
- package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +64 -0
- package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +116 -0
- package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +19 -149
- package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +6 -0
- package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +5 -5
- package/dist/extensions/pi-powerline-footer/types.ts +1 -2
- package/dist/extensions/pi-subagents/CHANGELOG.md +225 -14
- package/dist/extensions/pi-subagents/README.md +66 -1613
- package/dist/extensions/pi-subagents/agents/builder.md +0 -1
- package/dist/extensions/pi-subagents/agents/delegate.md +14 -0
- package/dist/extensions/pi-subagents/agents/oracle.md +74 -0
- package/dist/extensions/pi-subagents/agents/reviewer.md +78 -0
- package/dist/extensions/pi-subagents/agents/scout.md +50 -0
- package/dist/extensions/pi-subagents/agents/worker.md +58 -0
- package/dist/extensions/pi-subagents/docs/configuration.md +372 -0
- package/dist/extensions/pi-subagents/docs/extension-api.md +308 -0
- package/dist/extensions/pi-subagents/docs/missions.md +119 -0
- package/dist/extensions/pi-subagents/docs/models.md +192 -0
- package/dist/extensions/pi-subagents/docs/observability.md +174 -0
- package/dist/extensions/pi-subagents/docs/tool-reference.md +343 -0
- package/dist/extensions/pi-subagents/docs/watchdog.md +176 -0
- package/dist/extensions/pi-subagents/docs/workflows.md +163 -0
- package/dist/extensions/pi-subagents/inspector-runner.mjs +11 -0
- package/dist/extensions/pi-subagents/package-lock.json +152 -194
- package/dist/extensions/pi-subagents/package.json +9 -10
- package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
- package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
- package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
- package/dist/extensions/pi-subagents/prompts/review-loop.md +12 -12
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +22 -17
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +77 -72
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +177 -69
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +28 -8
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +88 -66
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +3 -3
- package/dist/extensions/pi-subagents/src/agents/agent-refinements.ts +624 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +38 -19
- package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +7 -3
- package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
- package/dist/extensions/pi-subagents/src/agents/skills.ts +2 -9
- package/dist/extensions/pi-subagents/src/api/delegation.ts +1 -2
- package/dist/extensions/pi-subagents/src/api/project-panes.ts +30 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +62 -8
- package/dist/extensions/pi-subagents/src/extension/control-notices.ts +2 -2
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +51 -26
- package/dist/extensions/pi-subagents/src/extension/index.ts +140 -50
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +77 -0
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +18 -24
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +58 -21
- package/dist/extensions/pi-subagents/src/extension/subagent-guide.ts +39 -0
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +47 -25
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +3 -3
- package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +460 -64
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +102 -4
- package/dist/extensions/pi-subagents/src/missions/actions.ts +77 -16
- package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +162 -0
- package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +65 -17
- package/dist/extensions/pi-subagents/src/missions/store.ts +151 -9
- package/dist/extensions/pi-subagents/src/missions/types.ts +58 -3
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +260 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -8
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +46 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +158 -36
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +294 -134
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +26 -4
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +45 -6
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
- package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +267 -0
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +145 -60
- package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +114 -6
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +33 -0
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +68 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +71 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +26 -3
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +42 -14
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +12 -24
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +7 -5
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +233 -101
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +17 -4
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +146 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +21 -5
- package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +85 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +43 -18
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +20 -19
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +40 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +188 -46
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +137 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1365 -252
- package/dist/extensions/pi-subagents/src/runs/foreground/workflow-foreground-steering.ts +187 -0
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +178 -26
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +32 -5
- package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +12 -2
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +509 -117
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +10 -10
- package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +189 -32
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -5
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +20 -8
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +128 -3
- package/dist/extensions/pi-subagents/src/shared/display-text.ts +100 -0
- package/dist/extensions/pi-subagents/src/shared/file-coalescer.ts +9 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +13 -0
- package/dist/extensions/pi-subagents/src/shared/formatters.ts +4 -6
- package/dist/extensions/pi-subagents/src/shared/prompt-resources.ts +51 -0
- package/dist/extensions/pi-subagents/src/shared/settings.ts +41 -11
- package/dist/extensions/pi-subagents/src/shared/types.ts +192 -3
- package/dist/extensions/pi-subagents/src/shared/utf8.ts +11 -0
- package/dist/extensions/pi-subagents/src/shared/utils.ts +46 -34
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +1 -14
- package/dist/extensions/pi-subagents/src/slash/inline-subagents.ts +1 -1
- package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +14 -12
- package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +176 -21
- package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +12 -6
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +52 -18
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +36 -5
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +37 -21
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +375 -42
- package/dist/extensions/pi-subagents/src/tui/render.ts +201 -68
- package/dist/extensions/pi-subagents/src/types/pi-runtime-compat.d.ts +0 -6
- package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +5 -4
- package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +0 -1
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +332 -27
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +15 -15
- 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 +84 -0
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/package.json +14 -0
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +102 -15
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +507 -306
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +309 -123
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +124 -58
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +62 -16
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +167 -113
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +4 -2
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +139 -58
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +324 -59
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +98 -22
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +106 -106
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +416 -106
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +287 -17
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1122 -96
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +107 -20
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +9 -9
- package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +26 -6
- package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/support/helpers.ts +2 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +1 -1
- package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +0 -8
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +194 -59
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
- package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +334 -248
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +52 -52
- package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +26 -26
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +118 -99
- package/dist/extensions/pi-subagents/test/unit/agent-refinements.test.ts +160 -0
- package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +9 -9
- package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +43 -1
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +36 -10
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +387 -30
- package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +9 -9
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +18 -3
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +99 -40
- package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +5 -3
- package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +7 -7
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +19 -19
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +94 -23
- package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +9 -9
- package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +20 -20
- package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +9 -9
- package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +35 -5
- package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +70 -52
- package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +187 -0
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +125 -71
- package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +18 -3
- package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +12 -12
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +17 -23
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +30 -2
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +25 -25
- package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +91 -47
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +468 -38
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +14 -4
- package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +37 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +79 -2
- package/dist/extensions/pi-subagents/test/unit/herdr-status-bridge.test.ts +102 -49
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +98 -32
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +7 -6
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
- package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +116 -0
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +81 -7
- package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +289 -13
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +42 -1
- package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +11 -2
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +84 -10
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +122 -8
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +8 -8
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +24 -24
- package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +16 -2
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +7 -7
- package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +10 -10
- package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +26 -17
- package/dist/extensions/pi-subagents/test/unit/pi-args-permission-system.test.ts +405 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +440 -92
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +24 -8
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +6 -6
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +43 -43
- package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +13 -13
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +59 -6
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +19 -19
- package/dist/extensions/pi-subagents/test/unit/project-panes-public-api.test.ts +243 -0
- package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +68 -168
- package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +58 -15
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +51 -0
- package/dist/extensions/pi-subagents/test/unit/reads-resolution.test.ts +66 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +47 -28
- package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +12 -12
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +57 -0
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +111 -31
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +65 -50
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +69 -28
- package/dist/extensions/pi-subagents/test/unit/scheduled-store-root.test.ts +67 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +60 -44
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +424 -49
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +23 -1
- package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +32 -5
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +13 -13
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +130 -92
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/subagent-action-recovery.test.ts +85 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +35 -35
- package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +24 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +312 -22
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +150 -15
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +65 -26
- package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +138 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +76 -25
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +5 -5
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +55 -6
- package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +13 -13
- package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +8 -8
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +6 -6
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +138 -11
- package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +8 -8
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +113 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +68 -7
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +6 -6
- package/dist/extensions/workflow/extension.ts +21 -19
- package/dist/extensions/workflow/modes.ts +65 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +48 -1
- package/dist/skills/pi-subagents/SKILL.md +20 -15
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +77 -83
- package/dist/skills/pi-subagents/references/execution-controls.md +215 -106
- package/dist/skills/pi-subagents/references/management-authoring-rpc.md +29 -9
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +58 -43
- package/docs/plans/workflow-autoloop-reference.md +192 -0
- package/docs/plans/workflow-subagent-consolidation-planb.md +374 -0
- package/docs/plans/workflow-subagent-scripted-merge.md +406 -0
- package/docs/quickstart.md +27 -43
- package/docs/settings.md +4 -0
- package/docs/usage.md +1 -0
- package/package.json +2 -1
- package/dist/extensions/workflow/adapter.ts +0 -949
- package/dist/extensions/workflow/modes/loop.ts +0 -74
- package/dist/extensions/workflow/modes/prototype.ts +0 -170
- package/dist/extensions/workflow/modes/quicktype.ts +0 -150
- package/dist/extensions/workflow/modes/task.ts +0 -105
- package/dist/extensions/workflow/run-state.ts +0 -124
- package/dist/extensions/workflow/state-machine.ts +0 -484
- package/dist/extensions/workflow/validators.ts +0 -42
|
@@ -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 { discoverAgents } from "../../src/agents/agents.ts";
|
|
30
31
|
import {
|
|
31
32
|
SUBAGENT_DELEGATION_REQUEST_EVENT,
|
|
32
33
|
SUBAGENT_DELEGATION_RESPONSE_EVENT,
|
|
@@ -35,14 +36,19 @@ import {
|
|
|
35
36
|
type SubagentDelegationResponse,
|
|
36
37
|
type SubagentDelegationStarted,
|
|
37
38
|
} from "../../src/api/delegation.ts";
|
|
38
|
-
import { CHAIN_RUNS_DIR, DIRS, INTERCOM_DETACH_REQUEST_EVENT, INTERCOM_DETACH_RESPONSE_EVENT, type SubagentState } from "../../src/shared/types.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 { ACTIVE_RUN_INDEX_DIR } from "../../src/runs/background/active-run-index.ts";
|
|
39
41
|
import { CHILD_WATCHDOG_STATUS_EVENT } from "../../src/watchdog/child-status.ts";
|
|
40
42
|
import { WAIT_TOOL_ENABLED_ENV } from "../../src/runs/background/wait-config.ts";
|
|
41
43
|
import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV } from "../../src/runs/shared/tool-budget.ts";
|
|
44
|
+
import { createRunFanoutBudget, encodeRunFanoutBudgetDescriptor, RUN_FANOUT_BUDGET_ENV } from "../../src/runs/shared/run-fanout-budget.ts";
|
|
42
45
|
import { MainWatchdogRuntime } from "../../src/watchdog/runtime.ts";
|
|
43
46
|
import { MAX_CHILD_PENDING_LINE_BYTES, MAX_CHILD_STDERR_BYTES } from "../../src/runs/shared/child-protocol.ts";
|
|
44
47
|
import {
|
|
45
48
|
SUBAGENT_FANOUT_CHILD_ENV,
|
|
49
|
+
SUBAGENT_STEER_ACK_DIR_ENV,
|
|
50
|
+
SUBAGENT_STEER_CAPABILITY_ENV,
|
|
51
|
+
SUBAGENT_STEER_INBOX_ENV,
|
|
46
52
|
SUBAGENT_PARENT_CHILD_INDEX_ENV,
|
|
47
53
|
SUBAGENT_PARENT_CONTROL_INBOX_ENV,
|
|
48
54
|
SUBAGENT_PARENT_DEPTH_ENV,
|
|
@@ -61,6 +67,7 @@ interface ProgressSummary {
|
|
|
61
67
|
agent: string;
|
|
62
68
|
index: number;
|
|
63
69
|
status: string;
|
|
70
|
+
task?: string;
|
|
64
71
|
activityState?: string;
|
|
65
72
|
lastActivityAt?: number;
|
|
66
73
|
currentTool?: string;
|
|
@@ -74,6 +81,7 @@ interface ProgressSummary {
|
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
interface ArtifactPaths {
|
|
84
|
+
inputPath?: string;
|
|
77
85
|
outputPath: string;
|
|
78
86
|
transcriptPath?: string;
|
|
79
87
|
metadataPath?: string;
|
|
@@ -98,6 +106,7 @@ interface RuntimeAcknowledgedExtensions {
|
|
|
98
106
|
interface RunSyncResult {
|
|
99
107
|
exitCode: number;
|
|
100
108
|
agent: string;
|
|
109
|
+
task?: string;
|
|
101
110
|
messages: unknown[];
|
|
102
111
|
error?: string;
|
|
103
112
|
protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number };
|
|
@@ -142,6 +151,7 @@ interface RunSyncResult {
|
|
|
142
151
|
|
|
143
152
|
interface MockPiCallRecord {
|
|
144
153
|
args?: string[];
|
|
154
|
+
cwd?: string;
|
|
145
155
|
systemPrompts?: Array<{ mode?: string; path?: string; text?: string; error?: string }>;
|
|
146
156
|
}
|
|
147
157
|
|
|
@@ -235,6 +245,7 @@ interface ExecutorToolResult {
|
|
|
235
245
|
isError?: boolean;
|
|
236
246
|
details?: {
|
|
237
247
|
totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
|
|
248
|
+
controlEvents?: Array<{ type?: string }>;
|
|
238
249
|
asyncId?: string;
|
|
239
250
|
timeoutMs?: number;
|
|
240
251
|
turnBudget?: { maxTurns: number; graceTurns: number };
|
|
@@ -300,7 +311,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
300
311
|
removeTempDir(tempDir);
|
|
301
312
|
});
|
|
302
313
|
|
|
303
|
-
function readCall(): { args: string[]; systemPrompts: NonNullable<MockPiCallRecord["systemPrompts"]> } {
|
|
314
|
+
function readCall(): { args: string[]; cwd?: string; systemPrompts: NonNullable<MockPiCallRecord["systemPrompts"]> } {
|
|
304
315
|
const callFile = fs.readdirSync(mockPi.dir)
|
|
305
316
|
.filter((name) => name.startsWith("call-") && name.endsWith(".json"))
|
|
306
317
|
.sort()
|
|
@@ -308,13 +319,20 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
308
319
|
assert.ok(callFile, "expected a recorded mock pi call");
|
|
309
320
|
const payload = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")) as MockPiCallRecord;
|
|
310
321
|
assert.ok(Array.isArray(payload.args), "expected recorded args");
|
|
311
|
-
return { args: payload.args, systemPrompts: payload.systemPrompts ?? [] };
|
|
322
|
+
return { args: payload.args, cwd: payload.cwd, systemPrompts: payload.systemPrompts ?? [] };
|
|
312
323
|
}
|
|
313
324
|
|
|
314
325
|
function readCallArgs(): string[] {
|
|
315
326
|
return readCall().args;
|
|
316
327
|
}
|
|
317
328
|
|
|
329
|
+
function readAllCallArgs(): string[][] {
|
|
330
|
+
return fs.readdirSync(mockPi.dir)
|
|
331
|
+
.filter((name) => name.startsWith("call-") && name.endsWith(".json"))
|
|
332
|
+
.sort()
|
|
333
|
+
.map((name) => (JSON.parse(fs.readFileSync(path.join(mockPi.dir, name), "utf-8")) as MockPiCallRecord).args);
|
|
334
|
+
}
|
|
335
|
+
|
|
318
336
|
function makeExecutor(
|
|
319
337
|
agents = [makeAgent("echo")],
|
|
320
338
|
config: Record<string, unknown> = {},
|
|
@@ -323,9 +341,11 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
323
341
|
allowMutatingManagementActions = true,
|
|
324
342
|
initialAsyncJobs: SubagentState["asyncJobs"] = new Map(),
|
|
325
343
|
workflowControllers?: Map<string, AbortController>,
|
|
344
|
+
handleScheduledRunAction?: Parameters<typeof createSubagentExecutor>[0]["handleScheduledRunAction"],
|
|
345
|
+
piEvents = createEventBus(),
|
|
326
346
|
) {
|
|
327
347
|
return createSubagentExecutor!({
|
|
328
|
-
pi: { events:
|
|
348
|
+
pi: { events: piEvents, getSessionName: () => undefined },
|
|
329
349
|
state: {
|
|
330
350
|
baseCwd: tempDir,
|
|
331
351
|
currentSessionId: initialSpawnState?.sessionId ?? null,
|
|
@@ -342,6 +362,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
342
362
|
expandTilde: (value: string) => value,
|
|
343
363
|
discoverAgents: () => ({ agents }),
|
|
344
364
|
allowMutatingManagementActions,
|
|
365
|
+
...(handleScheduledRunAction ? { handleScheduledRunAction } : {}),
|
|
345
366
|
});
|
|
346
367
|
}
|
|
347
368
|
|
|
@@ -361,11 +382,10 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
361
382
|
assert.equal(output, "Hello from mock agent");
|
|
362
383
|
});
|
|
363
384
|
|
|
364
|
-
it("
|
|
365
|
-
mockPi.onCall({ output: "single alias finished" });
|
|
385
|
+
it("rejects action='single' with execution fields", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
366
386
|
const executor = makeExecutor([makeAgent("echo")]);
|
|
367
387
|
|
|
368
|
-
const result = await executor.
|
|
388
|
+
const result = await executor.executePublic(
|
|
369
389
|
"single-alias",
|
|
370
390
|
{ action: "single", agent: "echo", task: "Run through alias" },
|
|
371
391
|
new AbortController().signal,
|
|
@@ -373,18 +393,59 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
373
393
|
makeMinimalCtx(tempDir),
|
|
374
394
|
);
|
|
375
395
|
|
|
376
|
-
assert.equal(result.isError,
|
|
377
|
-
assert.match(result.content[0]?.text ?? "", /
|
|
396
|
+
assert.equal(result.isError, true);
|
|
397
|
+
assert.match(result.content[0]?.text ?? "", /is not a management action/);
|
|
398
|
+
assert.equal(mockPi.callCount(), 0);
|
|
378
399
|
});
|
|
379
400
|
|
|
380
|
-
it("
|
|
381
|
-
mockPi.onCall({ output: "async workflow child" });
|
|
401
|
+
it("rejects internal fan-out fields from public workflows", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
382
402
|
const executor = makeExecutor([makeAgent("echo")]);
|
|
383
|
-
const
|
|
403
|
+
for (const params of [
|
|
404
|
+
{ workflowScript: `return runs.run("main", { agent: "echo", task: "work" })`, runFanoutBudget: { version: 1 } },
|
|
405
|
+
{ workflowScript: `return runs.run("main", { agent: "echo", task: "work" })`, runFanoutAdmitted: true },
|
|
406
|
+
] as const) {
|
|
407
|
+
const result = await executor.executePublic("private-fanout", params, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
|
|
408
|
+
assert.equal(result.isError, true);
|
|
409
|
+
assert.match(result.content[0]?.text ?? "", /does not accept internal run fan-out fields/);
|
|
410
|
+
}
|
|
411
|
+
assert.equal(mockPi.callCount(), 0);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("allows schedule.create to carry the required workflowScript target", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
415
|
+
let forwarded;
|
|
416
|
+
const executor = makeExecutor([makeAgent("echo")], {}, false, undefined, true, new Map(), undefined, async (params) => {
|
|
417
|
+
forwarded = params;
|
|
418
|
+
return { content: [{ type: "text", text: "created" }], details: { mode: "management", results: [] } };
|
|
419
|
+
});
|
|
384
420
|
|
|
385
421
|
const result = await executor.execute(
|
|
386
|
-
|
|
387
|
-
{
|
|
422
|
+
"schedule-create",
|
|
423
|
+
{ action: "schedule.create", id: "nightly", every: "1h", workflowScript: "return runs.run('main', { agent: 'echo' })" },
|
|
424
|
+
new AbortController().signal,
|
|
425
|
+
undefined,
|
|
426
|
+
makeMinimalCtx(tempDir),
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
assert.equal(result.isError, undefined);
|
|
430
|
+
assert.equal(result.content[0]?.text, "created");
|
|
431
|
+
assert.equal(forwarded?.workflowScript, "return runs.run('main', { agent: 'echo' })");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("starts workflow scripts asynchronously with a portable internal run id", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
435
|
+
mockPi.onCall({ echoEnv: [SUBAGENT_STEER_INBOX_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_ACK_DIR_ENV] });
|
|
436
|
+
const asyncJobs: SubagentState["asyncJobs"] = new Map();
|
|
437
|
+
const executor = makeExecutor([makeAgent("echo", { aliases: ["helper"] })], { missions: { globalIndex: false } }, false, undefined, true, asyncJobs);
|
|
438
|
+
const workflowCwd = path.join(tempDir, "workflow-cwd");
|
|
439
|
+
fs.mkdirSync(workflowCwd);
|
|
440
|
+
const toolCallId = "call_demo|fc_demo";
|
|
441
|
+
|
|
442
|
+
const result = await executor.execute(
|
|
443
|
+
toolCallId,
|
|
444
|
+
{
|
|
445
|
+
cwd: workflowCwd,
|
|
446
|
+
workflowScript: `emit("starting"); await runs.run("work", { agent: "helper", task: "Async work" }); return { answer: 42 };`,
|
|
447
|
+
mission: { summary: "Review the active backlog", labels: ["github-backlog", "review"] },
|
|
448
|
+
},
|
|
388
449
|
new AbortController().signal,
|
|
389
450
|
undefined,
|
|
390
451
|
makeMinimalCtx(tempDir),
|
|
@@ -392,25 +453,240 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
392
453
|
|
|
393
454
|
assert.equal(result.isError, undefined);
|
|
394
455
|
assert.equal(result.details.mode, "workflow");
|
|
395
|
-
assert.equal(result.details.
|
|
456
|
+
assert.equal(result.details.toolCallId, toolCallId);
|
|
457
|
+
assert.ok(result.details.asyncId);
|
|
458
|
+
const workflowRunId = result.details.asyncId;
|
|
459
|
+
assert.equal(result.details.runId, workflowRunId);
|
|
460
|
+
assert.notEqual(workflowRunId, toolCallId);
|
|
461
|
+
assert.match(workflowRunId, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
462
|
+
assert.equal(path.basename(result.details.asyncDir!), workflowRunId);
|
|
463
|
+
assert.equal(asyncJobs.has(workflowRunId), true);
|
|
464
|
+
assert.equal(asyncJobs.get(workflowRunId)?.cwd, workflowCwd);
|
|
465
|
+
assert.equal(asyncJobs.has(toolCallId), false);
|
|
466
|
+
assert.equal(fs.existsSync(path.join(DIRS.async, toolCallId)), false);
|
|
396
467
|
assert.match(result.content[0]?.text ?? "", /Async workflow/);
|
|
397
468
|
const statusPath = path.join(result.details.asyncDir!, "status.json");
|
|
398
|
-
let status: { state?: string; workflow?: { value?: unknown; emits?: unknown[]; trace?: Array<{ key?: string; state?: string }> } } = {};
|
|
469
|
+
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 }> } } = {};
|
|
399
470
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
400
471
|
status = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
|
|
401
472
|
if (status.state === "complete" || status.state === "failed") break;
|
|
402
473
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
403
474
|
}
|
|
404
475
|
assert.equal(status.state, "complete");
|
|
476
|
+
assert.equal(status.runId, workflowRunId);
|
|
477
|
+
assert.equal(status.toolCallId, toolCallId);
|
|
478
|
+
assert.equal(status.cwd, workflowCwd);
|
|
479
|
+
assert.equal(status.steps?.length, 1);
|
|
480
|
+
assert.deepEqual(status.steps?.map(({ agent, label, workflowKey }) => ({ agent, label, workflowKey })), [
|
|
481
|
+
{ agent: "echo", label: "work", workflowKey: "work" },
|
|
482
|
+
]);
|
|
483
|
+
assert.ok(status.steps?.every((step) => step.parentWorkflowRunId === workflowRunId));
|
|
405
484
|
assert.deepEqual(status.workflow?.value, { answer: 42 });
|
|
406
485
|
assert.deepEqual(status.workflow?.emits, ["starting"]);
|
|
407
|
-
assert.
|
|
408
|
-
|
|
486
|
+
assert.equal(mockPi.callCount(), 1);
|
|
487
|
+
assert.ok(status.workflow?.trace?.some((entry) => entry.key === "work" && entry.agent === "echo" && entry.state === "completed"));
|
|
488
|
+
const traceEvents = fs.readFileSync(path.join(result.details.asyncDir!, "events.jsonl"), "utf-8")
|
|
489
|
+
.trim()
|
|
490
|
+
.split("\n")
|
|
491
|
+
.map((line) => JSON.parse(line) as { type?: string; trace?: Array<{ key?: string; state?: string }> })
|
|
492
|
+
.filter((event) => event.type === "subagent.workflow.trace");
|
|
493
|
+
assert.equal(traceEvents.length, 2);
|
|
494
|
+
assert.deepEqual(traceEvents[0]?.trace?.map(({ key, state }) => ({ key, state })), [{ key: "work", state: "started" }]);
|
|
495
|
+
assert.deepEqual(traceEvents[1]?.trace?.map(({ key, state }) => ({ key, state })), [
|
|
496
|
+
{ key: "work", state: "started" },
|
|
497
|
+
{ key: "work", state: "completed" },
|
|
498
|
+
]);
|
|
499
|
+
const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
|
|
500
|
+
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 }> };
|
|
501
|
+
assert.equal(persistedResult.id, workflowRunId);
|
|
502
|
+
assert.equal(persistedResult.runId, workflowRunId);
|
|
503
|
+
assert.equal(persistedResult.toolCallId, toolCallId);
|
|
409
504
|
assert.equal(persistedResult.agent, "workflow");
|
|
505
|
+
assert.equal(persistedResult.cwd, workflowCwd);
|
|
506
|
+
assert.deepEqual(persistedResult.results?.map(({ agent, workflowKey }) => ({ agent, workflowKey })), [
|
|
507
|
+
{ agent: "echo", workflowKey: "work" },
|
|
508
|
+
]);
|
|
509
|
+
const steeringEnv = JSON.parse(persistedResult.results?.[0]?.output ?? "null") as Record<string, string | null>;
|
|
510
|
+
assert.match(steeringEnv[SUBAGENT_STEER_INBOX_ENV] ?? "", /control[/\\]workflow-foreground[/\\].+[/\\]control[/\\]steer-targets[/\\]0$/);
|
|
511
|
+
assert.match(steeringEnv[SUBAGENT_STEER_CAPABILITY_ENV] ?? "", /control[/\\]workflow-foreground[/\\].+[/\\]control[/\\]steer-capabilities[/\\]0\.json$/);
|
|
512
|
+
assert.match(steeringEnv[SUBAGENT_STEER_ACK_DIR_ENV] ?? "", /control[/\\]workflow-foreground[/\\].+[/\\]control[/\\]steer-acks[/\\]0$/);
|
|
513
|
+
assert.equal(fs.existsSync(path.join(result.details.asyncDir!, "control", "workflow-foreground", persistedResult.results?.[0]?.runId ?? "missing")), false);
|
|
410
514
|
assert.match(persistedResult.summary ?? "", /Return: \{\n "answer": 42\n\}/);
|
|
411
515
|
assert.deepEqual(persistedResult.workflow?.value, { answer: 42 });
|
|
516
|
+
assert.equal(fs.existsSync(path.join(DIRS.results, `${toolCallId}.json`)), false);
|
|
412
517
|
fs.rmSync(result.details.asyncDir!, { recursive: true, force: true });
|
|
413
|
-
fs.rmSync(
|
|
518
|
+
fs.rmSync(resultPath, { force: true });
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("runs an external CLI workflow child with subagents.defaultModel configured", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
522
|
+
const markerPath = path.join(tempDir, "external-started");
|
|
523
|
+
const executor = makeExecutor([
|
|
524
|
+
makeAgent("external", {
|
|
525
|
+
runner: { type: "external-cli", command: process.execPath, args: ["-e", `require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "started"); process.stdout.write("external result")`] },
|
|
526
|
+
model: "mock/default-model",
|
|
527
|
+
modelSource: { type: "subagents.defaultModel", scope: "user", path: "/settings.json", model: "mock/default-model" },
|
|
528
|
+
}),
|
|
529
|
+
]);
|
|
530
|
+
const ctx = { ...makeMinimalCtx(tempDir), model: { provider: "mock", id: "parent-model" } };
|
|
531
|
+
const started = await executor.execute(
|
|
532
|
+
`external-workflow-${Date.now()}`,
|
|
533
|
+
{ workflowScript: `return await runs.run("external", { agent: "external", task: "Run external", async: true });` },
|
|
534
|
+
new AbortController().signal,
|
|
535
|
+
undefined,
|
|
536
|
+
ctx,
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
assert.equal(started.isError, undefined);
|
|
540
|
+
assert.ok(started.details.asyncId);
|
|
541
|
+
const workflowResultPath = path.join(DIRS.results, `${started.details.asyncId}.json`);
|
|
542
|
+
let workflowResult: { state?: string; results?: Array<{ output?: string; runId?: string }> } = {};
|
|
543
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
544
|
+
if (fs.existsSync(workflowResultPath)) workflowResult = JSON.parse(fs.readFileSync(workflowResultPath, "utf-8"));
|
|
545
|
+
if (workflowResult.state === "complete" || workflowResult.state === "failed") break;
|
|
546
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
547
|
+
}
|
|
548
|
+
assert.equal(workflowResult.state, "complete");
|
|
549
|
+
assert.match(workflowResult.results?.[0]?.output ?? "", /Async: external/);
|
|
550
|
+
for (let attempt = 0; attempt < 100 && !fs.existsSync(markerPath); attempt++) {
|
|
551
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
552
|
+
}
|
|
553
|
+
assert.equal(fs.readFileSync(markerPath, "utf-8"), "started");
|
|
554
|
+
assert.equal(mockPi.callCount(), 0);
|
|
555
|
+
|
|
556
|
+
fs.rmSync(started.details.asyncDir!, { recursive: true, force: true });
|
|
557
|
+
fs.rmSync(workflowResultPath, { force: true });
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("rejects explicit model overrides for external CLI agents", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
561
|
+
const executor = makeExecutor([
|
|
562
|
+
makeAgent("external", {
|
|
563
|
+
runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
|
|
564
|
+
}),
|
|
565
|
+
]);
|
|
566
|
+
const result = await executor.execute(
|
|
567
|
+
"external-explicit-model",
|
|
568
|
+
{ agent: "external", task: "Run external", async: true, model: "mock/override" },
|
|
569
|
+
new AbortController().signal,
|
|
570
|
+
undefined,
|
|
571
|
+
makeMinimalCtx(tempDir),
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
assert.equal(result.isError, true);
|
|
575
|
+
assert.match(result.content[0]?.text ?? "", /does not support: model override/);
|
|
576
|
+
assert.equal(mockPi.callCount(), 0);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("rejects external CLI agent models that differ from inherited subagents.defaultModel", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
580
|
+
const executor = makeExecutor([
|
|
581
|
+
makeAgent("external", {
|
|
582
|
+
runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
|
|
583
|
+
model: "mock/override-model",
|
|
584
|
+
modelSource: { type: "subagents.defaultModel", scope: "user", path: "/settings.json", model: "mock/default-model" },
|
|
585
|
+
}),
|
|
586
|
+
]);
|
|
587
|
+
const result = await executor.execute(
|
|
588
|
+
"external-agent-override-model",
|
|
589
|
+
{ agent: "external", task: "Run external", async: true },
|
|
590
|
+
new AbortController().signal,
|
|
591
|
+
undefined,
|
|
592
|
+
makeMinimalCtx(tempDir),
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
assert.equal(result.isError, true);
|
|
596
|
+
assert.match(result.content[0]?.text ?? "", /does not support: model override/);
|
|
597
|
+
assert.equal(mockPi.callCount(), 0);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("rejects external CLI agent models that equal inherited subagents.defaultModel without provenance", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
601
|
+
const executor = makeExecutor([
|
|
602
|
+
makeAgent("external", {
|
|
603
|
+
runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
|
|
604
|
+
model: "mock/default-model",
|
|
605
|
+
}),
|
|
606
|
+
]);
|
|
607
|
+
const result = await executor.execute(
|
|
608
|
+
"external-agent-same-value-override-model",
|
|
609
|
+
{ agent: "external", task: "Run external", async: true },
|
|
610
|
+
new AbortController().signal,
|
|
611
|
+
undefined,
|
|
612
|
+
makeMinimalCtx(tempDir),
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
assert.equal(result.isError, true);
|
|
616
|
+
assert.match(result.content[0]?.text ?? "", /does not support: model override/);
|
|
617
|
+
assert.equal(mockPi.callCount(), 0);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it("projects live child activity into async workflow status", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
621
|
+
mockPi.onCall({
|
|
622
|
+
steps: [
|
|
623
|
+
{ jsonl: [events.toolStart("read", { path: "src/example.ts" })] },
|
|
624
|
+
{ delay: 2_500, jsonl: [events.toolEnd("read"), events.toolResult("read", "contents")] },
|
|
625
|
+
{ jsonl: [events.assistantMessage("Done")] },
|
|
626
|
+
],
|
|
627
|
+
});
|
|
628
|
+
const asyncJobs: SubagentState["asyncJobs"] = new Map();
|
|
629
|
+
const executor = makeExecutor([makeAgent("echo")], {
|
|
630
|
+
control: {
|
|
631
|
+
enabled: true,
|
|
632
|
+
needsAttentionAfterMs: 100,
|
|
633
|
+
activeNoticeAfterMs: 100,
|
|
634
|
+
activeNoticeAfterTurns: 999_999,
|
|
635
|
+
activeNoticeAfterTokens: 999_999,
|
|
636
|
+
notifyOn: ["active_long_running", "needs_attention"],
|
|
637
|
+
notifyChannels: ["event"],
|
|
638
|
+
},
|
|
639
|
+
}, false, undefined, true, asyncJobs);
|
|
640
|
+
|
|
641
|
+
const result = await executor.execute(
|
|
642
|
+
"workflow-live-activity",
|
|
643
|
+
{ workflowScript: `return runs.run("main", { agent: "echo", task: "Inspect the file" });` },
|
|
644
|
+
new AbortController().signal,
|
|
645
|
+
undefined,
|
|
646
|
+
makeMinimalCtx(tempDir),
|
|
647
|
+
);
|
|
648
|
+
const { asyncId: workflowRunId, asyncDir } = result.details;
|
|
649
|
+
assert.ok(workflowRunId);
|
|
650
|
+
assert.ok(asyncDir);
|
|
651
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
652
|
+
const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
|
|
653
|
+
const activeMarkerPath = path.join(DIRS.async, ACTIVE_RUN_INDEX_DIR, workflowRunId);
|
|
654
|
+
assert.equal(fs.existsSync(activeMarkerPath), true);
|
|
655
|
+
let liveStatus: AsyncStatus | undefined;
|
|
656
|
+
const activityDeadline = Date.now() + 5_000;
|
|
657
|
+
while (Date.now() < activityDeadline && !fs.existsSync(resultPath)) {
|
|
658
|
+
const candidate = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
659
|
+
if (candidate.activityState === "active_long_running" && candidate.steps?.[0]?.currentTool === "read") {
|
|
660
|
+
liveStatus = candidate;
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
assert.ok(liveStatus, "expected workflow status to expose live child activity");
|
|
667
|
+
assert.equal(liveStatus.activityState, "active_long_running");
|
|
668
|
+
assert.equal(typeof liveStatus.lastActivityAt, "number");
|
|
669
|
+
assert.equal(liveStatus.currentTool, "read");
|
|
670
|
+
assert.match(liveStatus.currentPath ?? "", /src[/\\]example\.ts$/);
|
|
671
|
+
assert.equal(liveStatus.toolCount, 1);
|
|
672
|
+
assert.equal(liveStatus.steps?.[0]?.status, "running");
|
|
673
|
+
assert.equal(liveStatus.steps?.[0]?.agent, "echo");
|
|
674
|
+
assert.match(liveStatus.steps?.[0]?.sessionFile ?? "", /session\.jsonl$/);
|
|
675
|
+
assert.equal(fs.existsSync(liveStatus.steps?.[0]?.sessionFile ?? ""), true);
|
|
676
|
+
assert.equal(liveStatus.steps?.[0]?.activityState, "active_long_running");
|
|
677
|
+
assert.equal(typeof liveStatus.steps?.[0]?.lastActivityAt, "number");
|
|
678
|
+
assert.equal(liveStatus.steps?.[0]?.toolCount, 1);
|
|
679
|
+
assert.equal(asyncJobs.get(workflowRunId)?.activityState, "active_long_running");
|
|
680
|
+
assert.equal(asyncJobs.get(workflowRunId)?.steps?.[0]?.currentTool, "read");
|
|
681
|
+
|
|
682
|
+
const completionDeadline = Date.now() + 5_000;
|
|
683
|
+
while (!fs.existsSync(resultPath)) {
|
|
684
|
+
if (Date.now() > completionDeadline) assert.fail("Timed out waiting for async workflow completion");
|
|
685
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
686
|
+
}
|
|
687
|
+
assert.equal(fs.existsSync(activeMarkerPath), false);
|
|
688
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
689
|
+
fs.rmSync(resultPath, { force: true });
|
|
414
690
|
});
|
|
415
691
|
|
|
416
692
|
it("rejects an invalid async workflow usage budget before creating run state", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
@@ -449,8 +725,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
449
725
|
);
|
|
450
726
|
|
|
451
727
|
assert.equal(started.isError, undefined);
|
|
452
|
-
assert.
|
|
453
|
-
|
|
728
|
+
assert.ok(started.details.asyncId);
|
|
729
|
+
assert.notEqual(started.details.asyncId, runId);
|
|
730
|
+
const resultPath = path.join(DIRS.results, `${started.details.asyncId}.json`);
|
|
454
731
|
let persisted: { state?: string; summary?: string; results?: Array<{ success?: boolean; output?: string }> } = {};
|
|
455
732
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
456
733
|
if (fs.existsSync(resultPath)) persisted = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
@@ -467,6 +744,60 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
467
744
|
fs.rmSync(resultPath, { force: true });
|
|
468
745
|
});
|
|
469
746
|
|
|
747
|
+
it("keeps ordinary async workflow child results in the watcher-owned path", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
748
|
+
mockPi.onCall({ output: "async child done" });
|
|
749
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
750
|
+
const result = await executor.execute(
|
|
751
|
+
`scripted-workflow-async-child-${Date.now()}`,
|
|
752
|
+
{ workflowScript: `return await runs.run("background", { agent: "echo", task: "Async child", async: true });`, async: false },
|
|
753
|
+
new AbortController().signal,
|
|
754
|
+
undefined,
|
|
755
|
+
makeMinimalCtx(tempDir),
|
|
756
|
+
);
|
|
757
|
+
const childRunId = (result.details.workflow?.value as { runId?: string } | undefined)?.runId;
|
|
758
|
+
assert.ok(childRunId);
|
|
759
|
+
const childDir = path.join(DIRS.async, childRunId);
|
|
760
|
+
const childResultPath = path.join(DIRS.results, `${childRunId}.json`);
|
|
761
|
+
for (let attempt = 0; attempt < 200 && !fs.existsSync(childResultPath); attempt++) {
|
|
762
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
763
|
+
}
|
|
764
|
+
assert.equal(fs.existsSync(childResultPath), true);
|
|
765
|
+
assert.equal(fs.existsSync(path.join(childDir, "workflow-result.json")), false);
|
|
766
|
+
fs.rmSync(childDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
767
|
+
fs.rmSync(childResultPath, { force: true });
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it("applies an agent deadline to a workflow-launched async child", { skip: !createSubagentExecutor ? "executor not importable" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
|
|
771
|
+
mockPi.onCall({ delay: 5_000, output: "too late" });
|
|
772
|
+
const executor = makeExecutor([makeAgent("slow", { defaultTimeoutMs: 150 })]);
|
|
773
|
+
const result = await executor.execute(
|
|
774
|
+
`scripted-workflow-async-child-timeout-${Date.now()}`,
|
|
775
|
+
{
|
|
776
|
+
workflowScript: `return await runs.run("background", { agent: "slow", task: "Wait", async: true });`,
|
|
777
|
+
async: false,
|
|
778
|
+
},
|
|
779
|
+
new AbortController().signal,
|
|
780
|
+
undefined,
|
|
781
|
+
makeMinimalCtx(tempDir),
|
|
782
|
+
);
|
|
783
|
+
const childRunId = (result.details.workflow?.value as { runId?: string } | undefined)?.runId;
|
|
784
|
+
assert.ok(childRunId, JSON.stringify(result.details.workflow?.value ?? result.content));
|
|
785
|
+
const childDir = path.join(DIRS.async, childRunId);
|
|
786
|
+
const childResultPath = path.join(DIRS.results, `${childRunId}.json`);
|
|
787
|
+
let persisted: { timeoutMs?: number; state?: string; results?: Array<{ timedOut?: boolean; error?: string }> } = {};
|
|
788
|
+
for (let attempt = 0; attempt < 200; attempt++) {
|
|
789
|
+
if (fs.existsSync(childResultPath)) persisted = JSON.parse(fs.readFileSync(childResultPath, "utf-8"));
|
|
790
|
+
if (persisted.state === "failed") break;
|
|
791
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
792
|
+
}
|
|
793
|
+
assert.equal(persisted.timeoutMs, 150);
|
|
794
|
+
assert.equal(persisted.state, "failed");
|
|
795
|
+
assert.deepEqual(persisted.results?.map((entry) => entry.timedOut), [true]);
|
|
796
|
+
assert.deepEqual(persisted.results?.map((entry) => entry.error), ["Subagent timed out after 150ms."]);
|
|
797
|
+
fs.rmSync(childDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
798
|
+
fs.rmSync(childResultPath, { force: true });
|
|
799
|
+
});
|
|
800
|
+
|
|
470
801
|
it("persists workflow parent metadata in an async worktree child status and result", { skip: !createSubagentExecutor || process.platform === "win32" ? "executor unavailable or worktree paths differ on Windows" : undefined }, async () => {
|
|
471
802
|
execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
|
|
472
803
|
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
|
|
@@ -476,14 +807,16 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
476
807
|
execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
|
|
477
808
|
mockPi.onCall({ output: "async child done" });
|
|
478
809
|
const executor = makeExecutor([makeAgent("echo")]);
|
|
479
|
-
const
|
|
810
|
+
const toolCallId = `scripted-workflow-parent-${Date.now()}`;
|
|
480
811
|
const started = await executor.execute(
|
|
481
|
-
|
|
812
|
+
toolCallId,
|
|
482
813
|
{ workflowScript: `const child = await runs.run("background", { agent: "echo", task: "Async child", async: true, worktree: true }); return child.runId;` },
|
|
483
814
|
new AbortController().signal,
|
|
484
815
|
undefined,
|
|
485
816
|
makeMinimalCtx(tempDir),
|
|
486
817
|
);
|
|
818
|
+
assert.ok(started.details.asyncId);
|
|
819
|
+
const workflowRunId = started.details.asyncId;
|
|
487
820
|
const workflowResultPath = path.join(DIRS.results, `${workflowRunId}.json`);
|
|
488
821
|
let childRunId: string | undefined;
|
|
489
822
|
for (let attempt = 0; attempt < 150; attempt++) {
|
|
@@ -494,6 +827,10 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
494
827
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
495
828
|
}
|
|
496
829
|
assert.ok(childRunId);
|
|
830
|
+
const workflowStatus = JSON.parse(fs.readFileSync(path.join(started.details.asyncDir!, "status.json"), "utf-8")) as AsyncStatus;
|
|
831
|
+
const workflowStepSessionFile = workflowStatus.steps?.[0]?.sessionFile ?? "";
|
|
832
|
+
assert.equal(workflowStatus.steps?.[0]?.agent, "echo");
|
|
833
|
+
assert.match(workflowStepSessionFile, /session\.jsonl$/);
|
|
497
834
|
const childDir = path.join(DIRS.async, childRunId);
|
|
498
835
|
const childStatusPath = path.join(childDir, "status.json");
|
|
499
836
|
let childStatus: { state?: string; mode?: string; parentWorkflowRunId?: string; workflowKey?: string; parallelHandoff?: { path?: string } } = {};
|
|
@@ -513,9 +850,10 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
513
850
|
const childResult = JSON.parse(fs.readFileSync(childResultPath, "utf-8")) as { parentWorkflowRunId?: string; workflowKey?: string };
|
|
514
851
|
assert.equal(childResult.parentWorkflowRunId, workflowRunId);
|
|
515
852
|
assert.equal(childResult.workflowKey, "background");
|
|
516
|
-
fs.
|
|
853
|
+
assert.equal(fs.existsSync(workflowStepSessionFile), true);
|
|
854
|
+
fs.rmSync(started.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
517
855
|
fs.rmSync(workflowResultPath, { force: true });
|
|
518
|
-
fs.rmSync(childDir, { recursive: true, force: true });
|
|
856
|
+
fs.rmSync(childDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
519
857
|
fs.rmSync(path.join(DIRS.results, `${childRunId}.json`), { force: true });
|
|
520
858
|
});
|
|
521
859
|
|
|
@@ -575,19 +913,20 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
575
913
|
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
576
914
|
});
|
|
577
915
|
|
|
578
|
-
it("routes workflow
|
|
916
|
+
it("routes workflow children through one automatic mission", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
579
917
|
mockPi.onCall({ output: "scanned auth" });
|
|
580
918
|
mockPi.onCall({ output: "reviewed auth" });
|
|
581
|
-
const executor = makeExecutor([makeAgent("echo")]);
|
|
919
|
+
const executor = makeExecutor([makeAgent("echo")], { missions: { globalIndex: false } });
|
|
582
920
|
|
|
583
921
|
const result = await executor.execute(
|
|
584
922
|
"scripted-workflow",
|
|
585
923
|
{
|
|
586
924
|
async: false,
|
|
587
925
|
workflowScript: `
|
|
926
|
+
const stateType = typeof state;
|
|
588
927
|
const scan = await runs.run("scan", { agent: "echo", task: "Scan auth" });
|
|
589
928
|
const review = await runs.run("review", { agent: "echo", task: "Review: " + scan.output });
|
|
590
|
-
return review.output;
|
|
929
|
+
return { output: review.output, stateType };
|
|
591
930
|
`,
|
|
592
931
|
},
|
|
593
932
|
new AbortController().signal,
|
|
@@ -599,9 +938,88 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
599
938
|
assert.match(result.content[0]?.text ?? "", /reviewed auth/);
|
|
600
939
|
assert.equal(result.details.mode, "workflow");
|
|
601
940
|
assert.equal(result.details.results.length, 2);
|
|
941
|
+
assert.equal(result.details.workflow?.value && (result.details.workflow.value as { stateType?: unknown }).stateType, "object");
|
|
942
|
+
assert.ok(result.details.missionId);
|
|
943
|
+
const missionDir = path.join(tempDir, ".pi-subagents", "missions");
|
|
944
|
+
const missionFiles = fs.readdirSync(missionDir).filter((entry) => entry.endsWith(".json"));
|
|
945
|
+
assert.equal(missionFiles.length, 1);
|
|
946
|
+
const mission = JSON.parse(fs.readFileSync(path.join(missionDir, missionFiles[0]!), "utf-8")) as { objective?: string };
|
|
947
|
+
assert.equal(mission.objective, utils.PROMPT_REDACTED);
|
|
602
948
|
assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state === "completed").map((entry) => entry.key), ["scan", "review"]);
|
|
603
949
|
});
|
|
604
950
|
|
|
951
|
+
it("keeps workflow children mission-detached when automatic mission persistence fails", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
952
|
+
mockPi.onCall({ output: "scanned auth" });
|
|
953
|
+
mockPi.onCall({ output: "reviewed auth" });
|
|
954
|
+
const blockedIndex = path.join(tempDir, "blocked-mission-index");
|
|
955
|
+
fs.writeFileSync(blockedIndex, "not a directory", "utf-8");
|
|
956
|
+
const executor = makeExecutor([makeAgent("echo")], { missions: { globalIndexDir: blockedIndex } });
|
|
957
|
+
|
|
958
|
+
const result = await executor.execute(
|
|
959
|
+
"scripted-workflow-mission-warning",
|
|
960
|
+
{
|
|
961
|
+
async: false,
|
|
962
|
+
workflowScript: `
|
|
963
|
+
const scan = await runs.run("scan", { agent: "echo", task: "Scan auth" });
|
|
964
|
+
const review = await runs.run("review", { agent: "echo", task: "Review: " + scan.output });
|
|
965
|
+
return review.output;
|
|
966
|
+
`,
|
|
967
|
+
},
|
|
968
|
+
new AbortController().signal,
|
|
969
|
+
undefined,
|
|
970
|
+
makeMinimalCtx(tempDir),
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
assert.equal(result.isError, undefined);
|
|
974
|
+
assert.equal(result.details.missionId, undefined);
|
|
975
|
+
assert.match(result.details.missionWarning ?? "", /Mission tracking unavailable/);
|
|
976
|
+
assert.equal(result.details.results.length, 2);
|
|
977
|
+
const missionDir = path.join(tempDir, ".pi-subagents", "missions");
|
|
978
|
+
const missionFiles = fs.existsSync(missionDir) ? fs.readdirSync(missionDir).filter((entry) => entry.endsWith(".json")) : [];
|
|
979
|
+
assert.equal(missionFiles.length, 1);
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
it("shares durable workflow state across a mission and omits it for mission:false", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
983
|
+
const executor = makeExecutor([makeAgent("echo")], { missions: { globalIndex: false } });
|
|
984
|
+
const first = await executor.execute(
|
|
985
|
+
"mission-state-first",
|
|
986
|
+
{
|
|
987
|
+
async: false,
|
|
988
|
+
mission: { title: "Stateful workflow" },
|
|
989
|
+
workflowScript: `await state.set("review.stage", { count: 1 }); return await state.get("review.stage");`,
|
|
990
|
+
},
|
|
991
|
+
new AbortController().signal,
|
|
992
|
+
undefined,
|
|
993
|
+
makeMinimalCtx(tempDir),
|
|
994
|
+
);
|
|
995
|
+
assert.equal(first.isError, undefined, first.content[0]?.text ?? "first workflow failed");
|
|
996
|
+
assert.ok(first.details.missionId);
|
|
997
|
+
assert.deepEqual(first.details.workflow?.value, { count: 1 });
|
|
998
|
+
const statePath = path.join(tempDir, ".pi-subagents", "missions", first.details.missionId, "state.json");
|
|
999
|
+
assert.equal(fs.existsSync(statePath), true);
|
|
1000
|
+
|
|
1001
|
+
const second = await executor.execute(
|
|
1002
|
+
"mission-state-second",
|
|
1003
|
+
{ async: false, missionId: first.details.missionId, workflowScript: `return await state.get("review.stage");` },
|
|
1004
|
+
new AbortController().signal,
|
|
1005
|
+
undefined,
|
|
1006
|
+
makeMinimalCtx(tempDir),
|
|
1007
|
+
);
|
|
1008
|
+
assert.equal(second.isError, undefined, second.content[0]?.text ?? "second workflow failed");
|
|
1009
|
+
assert.deepEqual(second.details.workflow?.value, { count: 1 });
|
|
1010
|
+
|
|
1011
|
+
const ephemeral = await executor.execute(
|
|
1012
|
+
"mission-state-off",
|
|
1013
|
+
{ async: false, mission: false, workflowScript: `return typeof state;` },
|
|
1014
|
+
new AbortController().signal,
|
|
1015
|
+
undefined,
|
|
1016
|
+
makeMinimalCtx(tempDir),
|
|
1017
|
+
);
|
|
1018
|
+
assert.equal(ephemeral.isError, undefined, ephemeral.content[0]?.text ?? "ephemeral workflow failed");
|
|
1019
|
+
assert.equal(ephemeral.details.workflow?.value, "undefined");
|
|
1020
|
+
assert.equal(ephemeral.details.missionId, undefined);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
605
1023
|
it("runs a direct single child in a managed worktree", { skip: !createSubagentExecutor || process.platform === "win32" ? "executor unavailable or worktree paths differ on Windows" : undefined }, async () => {
|
|
606
1024
|
execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
|
|
607
1025
|
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
|
|
@@ -610,11 +1028,11 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
610
1028
|
execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
|
|
611
1029
|
execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
|
|
612
1030
|
mockPi.onCall({ output: "isolated feature", writeFiles: [{ path: "feature.txt", content: "feature\n" }] });
|
|
613
|
-
const executor = makeExecutor([makeAgent("
|
|
1031
|
+
const executor = makeExecutor([makeAgent("worker")]);
|
|
614
1032
|
|
|
615
1033
|
const result = await executor.execute(
|
|
616
1034
|
"direct-worktree",
|
|
617
|
-
{ async: false, agent: "
|
|
1035
|
+
{ async: false, agent: "worker", task: "Implement feature", worktree: true },
|
|
618
1036
|
new AbortController().signal,
|
|
619
1037
|
undefined,
|
|
620
1038
|
makeMinimalCtx(tempDir),
|
|
@@ -635,6 +1053,57 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
635
1053
|
|
|
636
1054
|
});
|
|
637
1055
|
|
|
1056
|
+
it("aligns a forked workflow child session with its managed worktree cwd", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1057
|
+
execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
|
|
1058
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
|
|
1059
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd: tempDir });
|
|
1060
|
+
fs.writeFileSync(path.join(tempDir, "base.txt"), "base\n", "utf-8");
|
|
1061
|
+
execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
|
|
1062
|
+
execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
|
|
1063
|
+
|
|
1064
|
+
const parentSessionFile = path.join(mockPi.dir, "parent-session.jsonl");
|
|
1065
|
+
const childSessionFile = path.join(mockPi.dir, "forked-child-session.jsonl");
|
|
1066
|
+
fs.writeFileSync(parentSessionFile, `${JSON.stringify({ type: "session", version: 3, id: "parent", cwd: tempDir })}\n`, "utf-8");
|
|
1067
|
+
const ctx = makeMinimalCtx(tempDir);
|
|
1068
|
+
Object.assign(ctx.sessionManager, {
|
|
1069
|
+
getSessionFile: () => parentSessionFile,
|
|
1070
|
+
getLeafId: () => "parent-leaf",
|
|
1071
|
+
openSession: () => ({
|
|
1072
|
+
createBranchedSession: () => {
|
|
1073
|
+
fs.writeFileSync(childSessionFile, `${JSON.stringify({ type: "session", version: 3, id: "child", cwd: tempDir })}\n`, "utf-8");
|
|
1074
|
+
return childSessionFile;
|
|
1075
|
+
},
|
|
1076
|
+
}),
|
|
1077
|
+
});
|
|
1078
|
+
mockPi.onCall({ output: "isolated fork child" });
|
|
1079
|
+
const executor = makeExecutor([makeAgent("worker", { defaultContext: "fork" })]);
|
|
1080
|
+
|
|
1081
|
+
const result = await executor.execute(
|
|
1082
|
+
"forked-worktree-workflow",
|
|
1083
|
+
{ async: false, workflowScript: `return runs.run("isolated", { agent: "worker", task: "Work in isolation", worktree: true });` },
|
|
1084
|
+
new AbortController().signal,
|
|
1085
|
+
undefined,
|
|
1086
|
+
ctx,
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
1090
|
+
const workflowValue = result.details.workflow?.value as { artifactPaths?: string[] } | undefined;
|
|
1091
|
+
const handoffPath = workflowValue?.artifactPaths?.find((candidate) => candidate.endsWith(".json") && candidate.includes("handoffs"));
|
|
1092
|
+
assert.ok(handoffPath, JSON.stringify(workflowValue));
|
|
1093
|
+
const handoff = JSON.parse(fs.readFileSync(handoffPath, "utf-8")) as {
|
|
1094
|
+
groups: Array<{ cleanup: { tasks: Array<{ path: string }> } }>;
|
|
1095
|
+
};
|
|
1096
|
+
const managedWorktreeCwd = handoff.groups[0]?.cleanup.tasks[0]?.path;
|
|
1097
|
+
assert.ok(managedWorktreeCwd);
|
|
1098
|
+
const callCwd = readCall().cwd;
|
|
1099
|
+
assert.ok(callCwd);
|
|
1100
|
+
assert.notEqual(path.resolve(callCwd), path.resolve(tempDir));
|
|
1101
|
+
assert.equal(path.basename(callCwd), path.basename(managedWorktreeCwd));
|
|
1102
|
+
const sessionHeader = JSON.parse(fs.readFileSync(childSessionFile, "utf-8").split("\n", 1)[0]!) as { cwd?: string };
|
|
1103
|
+
assert.ok(sessionHeader.cwd);
|
|
1104
|
+
assert.equal(path.basename(sessionHeader.cwd), path.basename(callCwd));
|
|
1105
|
+
});
|
|
1106
|
+
|
|
638
1107
|
it("lets runs.all siblings settle when one child fails", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
639
1108
|
mockPi.onCall({ exitCode: 1, stderr: "first child failed" });
|
|
640
1109
|
mockPi.onCall({ output: "second child completed" });
|
|
@@ -669,6 +1138,112 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
669
1138
|
assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state !== "started").map(({ state }) => state).sort(), ["completed", "failed"]);
|
|
670
1139
|
});
|
|
671
1140
|
|
|
1141
|
+
it("rejects an over-limit runs.all batch before launching any workflow child", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1142
|
+
const executor = makeExecutor([makeAgent("echo")], { maxSubagentSpawnsPerRun: 1 });
|
|
1143
|
+
|
|
1144
|
+
const result = await executor.execute(
|
|
1145
|
+
"scripted-workflow-fanout-limit",
|
|
1146
|
+
{
|
|
1147
|
+
async: false,
|
|
1148
|
+
workflowScript: `return await runs.all([
|
|
1149
|
+
{ key: "first", agent: "echo", task: "First task" },
|
|
1150
|
+
{ key: "second", agent: "echo", task: "Second task" }
|
|
1151
|
+
]);`,
|
|
1152
|
+
},
|
|
1153
|
+
new AbortController().signal,
|
|
1154
|
+
undefined,
|
|
1155
|
+
makeMinimalCtx(tempDir),
|
|
1156
|
+
);
|
|
1157
|
+
|
|
1158
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
1159
|
+
assert.equal(mockPi.callCount(), 0);
|
|
1160
|
+
const children = result.details.workflow?.value as Array<{ ok: boolean; error?: string }>;
|
|
1161
|
+
assert.deepEqual(children.map(({ ok }) => ok), [false, false]);
|
|
1162
|
+
for (const child of children) assert.match(child.error ?? "", /workflow\[second\].*0\/1 used; 2 requested, 1 remaining/);
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
it("runs a direct child gate as host-verified acceptance", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1166
|
+
const markerFile = "direct-gate.txt";
|
|
1167
|
+
const markerPath = path.join(tempDir, markerFile);
|
|
1168
|
+
mockPi.onCall({ output: [
|
|
1169
|
+
"done",
|
|
1170
|
+
"```acceptance-report",
|
|
1171
|
+
JSON.stringify({
|
|
1172
|
+
criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "implemented" }],
|
|
1173
|
+
changedFiles: ["src/file.ts"],
|
|
1174
|
+
testsAddedOrUpdated: ["test/file.test.ts"],
|
|
1175
|
+
commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }],
|
|
1176
|
+
validationOutput: ["tests passed"],
|
|
1177
|
+
residualRisks: [],
|
|
1178
|
+
noStagedFiles: true,
|
|
1179
|
+
}),
|
|
1180
|
+
"```",
|
|
1181
|
+
].join("\n") });
|
|
1182
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
1183
|
+
|
|
1184
|
+
const result = await executor.execute(
|
|
1185
|
+
"direct-gate",
|
|
1186
|
+
{ async: false, agent: "echo", task: "Validate the result without edits", gate: `${process.execPath} -e "require('node:fs').writeFileSync('${markerFile}','verified')"` },
|
|
1187
|
+
new AbortController().signal,
|
|
1188
|
+
undefined,
|
|
1189
|
+
makeMinimalCtx(tempDir),
|
|
1190
|
+
);
|
|
1191
|
+
|
|
1192
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "direct gate failed");
|
|
1193
|
+
assert.equal(fs.readFileSync(markerPath, "utf-8"), "verified");
|
|
1194
|
+
assert.equal(result.details.results[0]?.acceptance?.status, "verified");
|
|
1195
|
+
assert.equal(result.details.results[0]?.acceptance?.verifyRuns[0]?.id, "gate");
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
it("lets runs.all siblings settle when one verified gate fails", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1199
|
+
const acceptedReport = [
|
|
1200
|
+
"done",
|
|
1201
|
+
"```acceptance-report",
|
|
1202
|
+
JSON.stringify({
|
|
1203
|
+
criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "implemented" }],
|
|
1204
|
+
changedFiles: ["src/file.ts"],
|
|
1205
|
+
testsAddedOrUpdated: ["test/file.test.ts"],
|
|
1206
|
+
commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }],
|
|
1207
|
+
validationOutput: ["tests passed"],
|
|
1208
|
+
residualRisks: [],
|
|
1209
|
+
noStagedFiles: true,
|
|
1210
|
+
}),
|
|
1211
|
+
"```",
|
|
1212
|
+
].join("\n");
|
|
1213
|
+
mockPi.onCall({ output: acceptedReport });
|
|
1214
|
+
mockPi.onCall({ output: acceptedReport });
|
|
1215
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
1216
|
+
|
|
1217
|
+
const result = await executor.execute(
|
|
1218
|
+
"scripted-workflow-gates",
|
|
1219
|
+
{
|
|
1220
|
+
async: false,
|
|
1221
|
+
workflowScript: `
|
|
1222
|
+
const children = await runs.all([
|
|
1223
|
+
{ key: "fails-gate", agent: "echo", task: "First task", gate: ${JSON.stringify(`${process.execPath} -e "process.exit(7)"`)} },
|
|
1224
|
+
{ key: "passes-gate", agent: "echo", task: "Second task", gate: ${JSON.stringify(`${process.execPath} -e "process.exit(0)"`)} }
|
|
1225
|
+
]);
|
|
1226
|
+
return children.map(({ key, ok }) => ({ key, ok }));
|
|
1227
|
+
`,
|
|
1228
|
+
},
|
|
1229
|
+
new AbortController().signal,
|
|
1230
|
+
undefined,
|
|
1231
|
+
makeMinimalCtx(tempDir),
|
|
1232
|
+
);
|
|
1233
|
+
|
|
1234
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
1235
|
+
assert.equal(mockPi.callCount(), 2);
|
|
1236
|
+
assert.deepEqual(result.details.workflow?.value, [
|
|
1237
|
+
{ key: "fails-gate", ok: false },
|
|
1238
|
+
{ key: "passes-gate", ok: true },
|
|
1239
|
+
]);
|
|
1240
|
+
const [failed, passed] = result.details.results;
|
|
1241
|
+
assert.equal(failed?.acceptance?.status, "rejected");
|
|
1242
|
+
assert.equal(failed?.acceptance?.verifyRuns[0]?.status, "failed");
|
|
1243
|
+
assert.equal(passed?.acceptance?.status, "verified");
|
|
1244
|
+
assert.equal(passed?.acceptance?.verifyRuns[0]?.status, "passed");
|
|
1245
|
+
});
|
|
1246
|
+
|
|
672
1247
|
it("gives parallel workflow children separate managed worktrees and durable handoffs", { skip: !createSubagentExecutor || process.platform === "win32" ? "executor unavailable or worktree paths differ on Windows" : undefined }, async () => {
|
|
673
1248
|
execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
|
|
674
1249
|
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
|
|
@@ -678,7 +1253,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
678
1253
|
execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
|
|
679
1254
|
mockPi.onCall({ output: "feature a", writeFiles: [{ path: "feature-a.txt", content: "a\n" }] });
|
|
680
1255
|
mockPi.onCall({ output: "feature b", writeFiles: [{ path: "feature-b.txt", content: "b\n" }] });
|
|
681
|
-
const executor = makeExecutor([makeAgent("
|
|
1256
|
+
const executor = makeExecutor([makeAgent("worker")]);
|
|
682
1257
|
|
|
683
1258
|
const result = await executor.execute(
|
|
684
1259
|
"scripted-workflow-worktrees",
|
|
@@ -686,8 +1261,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
686
1261
|
async: false,
|
|
687
1262
|
workflowScript: `
|
|
688
1263
|
const children = await runs.all([
|
|
689
|
-
{ key: "feature-a", agent: "
|
|
690
|
-
{ key: "feature-b", agent: "
|
|
1264
|
+
{ key: "feature-a", agent: "worker", task: "Implement A", worktree: true },
|
|
1265
|
+
{ key: "feature-b", agent: "worker", task: "Implement B", worktree: true }
|
|
691
1266
|
]);
|
|
692
1267
|
return children.map(({ key, artifactPaths }) => ({ key, artifactPaths }));
|
|
693
1268
|
`,
|
|
@@ -726,6 +1301,87 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
726
1301
|
assert.match(result.content[0]?.text ?? "", /handoffs/);
|
|
727
1302
|
});
|
|
728
1303
|
|
|
1304
|
+
it("preserves a workflow worktree when its child detaches for supervisor coordination", { skip: !createSubagentExecutor || process.platform === "win32" ? "executor unavailable or worktree paths differ on Windows" : undefined }, async () => {
|
|
1305
|
+
execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
|
|
1306
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
|
|
1307
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd: tempDir });
|
|
1308
|
+
fs.writeFileSync(path.join(tempDir, "base.txt"), "base\n", "utf-8");
|
|
1309
|
+
execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
|
|
1310
|
+
execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
|
|
1311
|
+
mockPi.onCall({
|
|
1312
|
+
steps: [
|
|
1313
|
+
{ jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
|
|
1314
|
+
{ delay: 500, jsonl: [events.assistantMessage("done after coordination")] },
|
|
1315
|
+
],
|
|
1316
|
+
});
|
|
1317
|
+
const piEvents = createEventBus();
|
|
1318
|
+
const executor = makeExecutor(
|
|
1319
|
+
[makeAgent("worker", { systemPrompt: "Intercom orchestration channel:" })],
|
|
1320
|
+
{},
|
|
1321
|
+
false,
|
|
1322
|
+
undefined,
|
|
1323
|
+
true,
|
|
1324
|
+
new Map(),
|
|
1325
|
+
undefined,
|
|
1326
|
+
undefined,
|
|
1327
|
+
piEvents,
|
|
1328
|
+
);
|
|
1329
|
+
let detachAccepted = false;
|
|
1330
|
+
piEvents.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => {
|
|
1331
|
+
if ((payload as { requestId?: unknown }).requestId === "workflow-worktree-detach") {
|
|
1332
|
+
detachAccepted ||= (payload as { accepted?: unknown }).accepted === true;
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
const detachTimer = setInterval(() => {
|
|
1336
|
+
if (!detachAccepted) piEvents.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "workflow-worktree-detach" });
|
|
1337
|
+
}, 10);
|
|
1338
|
+
detachTimer.unref();
|
|
1339
|
+
|
|
1340
|
+
const result = await executor.execute(
|
|
1341
|
+
"scripted-workflow-detached-worktree",
|
|
1342
|
+
{
|
|
1343
|
+
async: false,
|
|
1344
|
+
workflowScript: `
|
|
1345
|
+
const children = await runs.all([
|
|
1346
|
+
{ key: "detaches", agent: "worker", task: "Ask then continue", worktree: true }
|
|
1347
|
+
]);
|
|
1348
|
+
return children.map(({ key, ok, artifactPaths }) => ({ key, ok, artifactPaths }));
|
|
1349
|
+
`,
|
|
1350
|
+
},
|
|
1351
|
+
new AbortController().signal,
|
|
1352
|
+
undefined,
|
|
1353
|
+
makeMinimalCtx(tempDir),
|
|
1354
|
+
);
|
|
1355
|
+
clearInterval(detachTimer);
|
|
1356
|
+
|
|
1357
|
+
assert.equal(detachAccepted, true);
|
|
1358
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
1359
|
+
assert.match(result.content[0]?.text ?? "", /run detaches: failed/);
|
|
1360
|
+
const workflowValue = result.details.workflow?.value as Array<{ ok: boolean; artifactPaths: string[] }>;
|
|
1361
|
+
assert.equal(workflowValue[0]?.ok, false);
|
|
1362
|
+
const handoffPath = workflowValue[0]?.artifactPaths.find((candidate) => candidate.endsWith(".json"));
|
|
1363
|
+
assert.ok(handoffPath, result.content[0]?.text ?? "missing pending handoff");
|
|
1364
|
+
const handoff = JSON.parse(fs.readFileSync(handoffPath, "utf-8")) as {
|
|
1365
|
+
groups: Array<{
|
|
1366
|
+
cleanup: { state: string; tasks: Array<{ path: string; branch: string; preserved: boolean; worktreeRemoved: boolean; branchRemoved: boolean }> };
|
|
1367
|
+
}>;
|
|
1368
|
+
};
|
|
1369
|
+
const cleanup = handoff.groups[0]?.cleanup;
|
|
1370
|
+
assert.equal(cleanup?.state, "partial");
|
|
1371
|
+
assert.equal(cleanup?.tasks[0]?.preserved, true);
|
|
1372
|
+
assert.equal(cleanup?.tasks[0]?.worktreeRemoved, false);
|
|
1373
|
+
assert.equal(cleanup?.tasks[0]?.branchRemoved, false);
|
|
1374
|
+
const worktreePath = cleanup?.tasks[0]?.path;
|
|
1375
|
+
const branch = cleanup?.tasks[0]?.branch;
|
|
1376
|
+
assert.ok(worktreePath);
|
|
1377
|
+
assert.ok(branch);
|
|
1378
|
+
assert.equal(fs.existsSync(worktreePath), true, "live detached worktree must remain present");
|
|
1379
|
+
|
|
1380
|
+
await new Promise((resolve) => setTimeout(resolve, 750));
|
|
1381
|
+
execFileSync("git", ["worktree", "remove", "--force", worktreePath], { cwd: tempDir });
|
|
1382
|
+
execFileSync("git", ["branch", "-D", branch], { cwd: tempDir, stdio: "ignore" });
|
|
1383
|
+
});
|
|
1384
|
+
|
|
729
1385
|
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 () => {
|
|
730
1386
|
execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
|
|
731
1387
|
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
|
|
@@ -735,7 +1391,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
735
1391
|
execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
|
|
736
1392
|
mockPi.onCall({ output: "isolated", writeFiles: [{ path: "isolated.txt", content: "isolated\n" }] });
|
|
737
1393
|
mockPi.onCall({ output: "shared", writeFiles: [{ path: "shared.txt", content: "shared\n" }] });
|
|
738
|
-
const executor = makeExecutor([makeAgent("
|
|
1394
|
+
const executor = makeExecutor([makeAgent("worker")]);
|
|
739
1395
|
|
|
740
1396
|
const result = await executor.execute(
|
|
741
1397
|
"scripted-workflow-worktree-default",
|
|
@@ -743,8 +1399,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
743
1399
|
async: false,
|
|
744
1400
|
worktree: true,
|
|
745
1401
|
workflowScript: `
|
|
746
|
-
const isolated = await runs.run("isolated", { agent: "
|
|
747
|
-
const shared = await runs.run("shared", { agent: "
|
|
1402
|
+
const isolated = await runs.run("isolated", { agent: "worker", task: "Isolated" });
|
|
1403
|
+
const shared = await runs.run("shared", { agent: "worker", task: "Shared", worktree: false });
|
|
748
1404
|
return { isolated: isolated.artifactPaths, shared: shared.artifactPaths };
|
|
749
1405
|
`,
|
|
750
1406
|
},
|
|
@@ -762,6 +1418,79 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
762
1418
|
assert.equal(fs.existsSync(handoffPaths[0]!), true);
|
|
763
1419
|
});
|
|
764
1420
|
|
|
1421
|
+
it("supports dynamic parallel phases followed by sequential worktree children", { skip: !createSubagentExecutor || process.platform === "win32" ? "executor unavailable or worktree paths differ on Windows" : undefined }, async () => {
|
|
1422
|
+
execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
|
|
1423
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
|
|
1424
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd: tempDir });
|
|
1425
|
+
fs.writeFileSync(path.join(tempDir, "base.txt"), "base\n", "utf-8");
|
|
1426
|
+
execFileSync("git", ["add", "base.txt"], { cwd: tempDir });
|
|
1427
|
+
execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
|
|
1428
|
+
mockPi.onCall({ output: "api built", writeFiles: [{ path: "api.txt", content: "api\n" }] });
|
|
1429
|
+
mockPi.onCall({ output: "ui built", writeFiles: [{ path: "ui.txt", content: "ui\n" }] });
|
|
1430
|
+
mockPi.onCall({ output: "joined", writeFiles: [{ path: "joined.txt", content: "joined\n" }] });
|
|
1431
|
+
mockPi.onCall({ output: "shared", writeFiles: [{ path: "shared.txt", content: "shared\n" }] });
|
|
1432
|
+
const executor = makeExecutor([makeAgent("worker")]);
|
|
1433
|
+
|
|
1434
|
+
const result = await executor.execute(
|
|
1435
|
+
"scripted-workflow-dynamic-worktree-phases",
|
|
1436
|
+
{
|
|
1437
|
+
async: false,
|
|
1438
|
+
worktree: true,
|
|
1439
|
+
workflowScript: `
|
|
1440
|
+
const targets = ["api", "ui"];
|
|
1441
|
+
const built = await runs.all(targets.map((target) => ({
|
|
1442
|
+
key: "build-" + target,
|
|
1443
|
+
agent: "worker",
|
|
1444
|
+
task: "Build " + target
|
|
1445
|
+
})));
|
|
1446
|
+
const joined = await runs.run("join", { agent: "worker", task: built.map((child) => child.key).join(",") });
|
|
1447
|
+
const shared = await runs.run("shared", { agent: "worker", task: joined.key, worktree: false });
|
|
1448
|
+
return {
|
|
1449
|
+
built: built.map((child) => ({ key: child.key, artifactPaths: child.artifactPaths })),
|
|
1450
|
+
joined: { key: joined.key, artifactPaths: joined.artifactPaths },
|
|
1451
|
+
shared: shared.key
|
|
1452
|
+
};
|
|
1453
|
+
`,
|
|
1454
|
+
},
|
|
1455
|
+
new AbortController().signal,
|
|
1456
|
+
undefined,
|
|
1457
|
+
makeMinimalCtx(tempDir),
|
|
1458
|
+
);
|
|
1459
|
+
|
|
1460
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
1461
|
+
assert.equal(mockPi.callCount(), 4, result.content[0]?.text ?? "workflow produced no output");
|
|
1462
|
+
assert.equal(fs.existsSync(path.join(tempDir, "api.txt")), false);
|
|
1463
|
+
assert.equal(fs.existsSync(path.join(tempDir, "ui.txt")), false);
|
|
1464
|
+
assert.equal(fs.existsSync(path.join(tempDir, "joined.txt")), false);
|
|
1465
|
+
assert.equal(fs.readFileSync(path.join(tempDir, "shared.txt"), "utf-8"), "shared\n");
|
|
1466
|
+
|
|
1467
|
+
const output = result.content[0]?.text ?? "";
|
|
1468
|
+
assert.match(output, /build-api/);
|
|
1469
|
+
assert.match(output, /build-ui/);
|
|
1470
|
+
assert.match(output, /join/);
|
|
1471
|
+
assert.match(output, /shared/);
|
|
1472
|
+
const handoffPaths = [...output.matchAll(/"([^"\n]*\/handoffs\/[^"\n]+\.json)"/g)].map((match) => match[1]!);
|
|
1473
|
+
assert.equal(handoffPaths.length, 3, output);
|
|
1474
|
+
const worktreePaths = new Set<string>();
|
|
1475
|
+
for (const handoffPath of handoffPaths) {
|
|
1476
|
+
const handoff = JSON.parse(fs.readFileSync(handoffPath, "utf-8")) as {
|
|
1477
|
+
groups: Array<{
|
|
1478
|
+
children: Array<{ patch: { changed: boolean; path: string } }>;
|
|
1479
|
+
cleanup: { state: string; tasks: Array<{ path: string; worktreeRemoved: boolean; branchRemoved: boolean }> };
|
|
1480
|
+
}>;
|
|
1481
|
+
};
|
|
1482
|
+
assert.equal(handoff.groups.length, 1);
|
|
1483
|
+
assert.equal(handoff.groups[0]?.children[0]?.patch.changed, true);
|
|
1484
|
+
assert.equal(fs.existsSync(handoff.groups[0]!.children[0]!.patch.path), true);
|
|
1485
|
+
assert.equal(handoff.groups[0]?.cleanup.state, "complete");
|
|
1486
|
+
assert.equal(handoff.groups[0]?.cleanup.tasks[0]?.worktreeRemoved, true);
|
|
1487
|
+
assert.equal(handoff.groups[0]?.cleanup.tasks[0]?.branchRemoved, true);
|
|
1488
|
+
worktreePaths.add(handoff.groups[0]!.cleanup.tasks[0]!.path);
|
|
1489
|
+
}
|
|
1490
|
+
assert.equal(worktreePaths.size, 3);
|
|
1491
|
+
for (const worktreePath of worktreePaths) assert.equal(fs.existsSync(worktreePath), false);
|
|
1492
|
+
});
|
|
1493
|
+
|
|
765
1494
|
it("applies a workflow usage budget across scripted child launches", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
766
1495
|
mockPi.onCall({ output: "first result" });
|
|
767
1496
|
const executor = makeExecutor([makeAgent("echo")]);
|
|
@@ -884,11 +1613,27 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
884
1613
|
);
|
|
885
1614
|
|
|
886
1615
|
assert.equal(result.isError, true);
|
|
887
|
-
assert.match(result.content[0]?.text ?? "", /(?:verified.*object form|verify.*at least one command)/i);
|
|
1616
|
+
assert.match(result.content[0]?.text ?? "", /(?:verified.*object form|verify.*at least one runtime command)/i);
|
|
888
1617
|
}
|
|
889
1618
|
assert.equal(mockPi.callCount(), 0);
|
|
890
1619
|
});
|
|
891
1620
|
|
|
1621
|
+
it("rejects invalid verified async chain acceptance before spawning", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1622
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
1623
|
+
|
|
1624
|
+
const result = await executor.execute(
|
|
1625
|
+
"invalid-verified-async-chain-acceptance",
|
|
1626
|
+
{ chain: [{ agent: "echo", task: "Do work", acceptance: { level: "verified", verify: [] } }], async: true },
|
|
1627
|
+
new AbortController().signal,
|
|
1628
|
+
undefined,
|
|
1629
|
+
makeMinimalCtx(tempDir),
|
|
1630
|
+
);
|
|
1631
|
+
|
|
1632
|
+
assert.equal(result.isError, true);
|
|
1633
|
+
assert.match(result.content[0]?.text ?? "", /verify.*at least one runtime command/i);
|
|
1634
|
+
assert.equal(mockPi.callCount(), 0);
|
|
1635
|
+
});
|
|
1636
|
+
|
|
892
1637
|
it("rejects unknown action strings at runtime", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
893
1638
|
const executor = makeExecutor([makeAgent("echo")]);
|
|
894
1639
|
|
|
@@ -1053,7 +1798,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1053
1798
|
requestId: "registered-a",
|
|
1054
1799
|
ownerRunId: "owner-delegation",
|
|
1055
1800
|
nodeId: "node-a",
|
|
1056
|
-
agent: "
|
|
1801
|
+
agent: "worker",
|
|
1057
1802
|
task: "Return literal JSON-looking text",
|
|
1058
1803
|
context: "fresh",
|
|
1059
1804
|
cwd: tempDir,
|
|
@@ -1065,7 +1810,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1065
1810
|
requestId: "registered-b",
|
|
1066
1811
|
ownerRunId: "owner-delegation",
|
|
1067
1812
|
nodeId: "node-b",
|
|
1068
|
-
agent: "
|
|
1813
|
+
agent: "reviewer",
|
|
1069
1814
|
task: "Run the second logical node",
|
|
1070
1815
|
context: "fresh",
|
|
1071
1816
|
cwd: tempDir,
|
|
@@ -1181,13 +1926,53 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1181
1926
|
assert.equal(mockPi.callCount(), 1);
|
|
1182
1927
|
});
|
|
1183
1928
|
|
|
1929
|
+
it("qualifies inherited nested claims with the generated nested run id", async () => {
|
|
1930
|
+
mockPi.onCall({ output: "nested completed" });
|
|
1931
|
+
const descriptor = createRunFanoutBudget("root-run", 2);
|
|
1932
|
+
const previous = process.env[RUN_FANOUT_BUDGET_ENV];
|
|
1933
|
+
try {
|
|
1934
|
+
process.env[RUN_FANOUT_BUDGET_ENV] = encodeRunFanoutBudgetDescriptor({ ...descriptor, parentPath: "tasks[0]" });
|
|
1935
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
1936
|
+
const result = await executor.execute("nested", { agent: "echo", task: "Nested work" }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
|
|
1937
|
+
|
|
1938
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "nested run failed");
|
|
1939
|
+
const claims = fs.readdirSync(path.join(descriptor.directory, "claims"));
|
|
1940
|
+
assert.equal(claims.length, 1);
|
|
1941
|
+
const claim = JSON.parse(fs.readFileSync(path.join(descriptor.directory, "claims", claims[0]!), "utf-8")) as { path: string };
|
|
1942
|
+
assert.match(claim.path, /^tasks\[0\]\/[a-f0-9]{8}\/single$/);
|
|
1943
|
+
} finally {
|
|
1944
|
+
if (previous === undefined) delete process.env[RUN_FANOUT_BUDGET_ENV];
|
|
1945
|
+
else process.env[RUN_FANOUT_BUDGET_ENV] = previous;
|
|
1946
|
+
fs.rmSync(descriptor.directory, { recursive: true, force: true });
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1950
|
+
it("rejects an over-limit static run fan-out before creating session artifacts", async () => {
|
|
1951
|
+
const sessionDir = path.join(tempDir, "run-fanout-preflight");
|
|
1952
|
+
const executor = makeExecutor([makeAgent("echo"), makeAgent("second")], { maxSubagentSpawnsPerRun: 1 });
|
|
1953
|
+
const result = await executor.execute(
|
|
1954
|
+
"run-fanout-preflight",
|
|
1955
|
+
{ tasks: [{ agent: "echo", task: "First" }, { agent: "second", task: "Second" }], sessionDir },
|
|
1956
|
+
new AbortController().signal,
|
|
1957
|
+
undefined,
|
|
1958
|
+
makeMinimalCtx(tempDir),
|
|
1959
|
+
);
|
|
1960
|
+
|
|
1961
|
+
assert.equal(result.isError, true);
|
|
1962
|
+
assert.match(result.content[0]?.text ?? "", /Run fan-out limit reached at tasks\[1\] \(0\/1 used; 2 requested, 1 remaining\)/);
|
|
1963
|
+
assert.deepEqual(result.details.runFanoutBudget, { used: 0, limit: 1, remaining: 1 });
|
|
1964
|
+
assert.equal(result.details.runFanoutRejection?.path, "tasks[1]");
|
|
1965
|
+
assert.equal(fs.existsSync(sessionDir), false);
|
|
1966
|
+
assert.equal(mockPi.callCount(), 0);
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1184
1969
|
it("reports structured spawn-budget usage through status", async () => {
|
|
1185
1970
|
const spawnState = { sessionId: "session-123", count: 3, configuredLimit: 4, granted: 1, grantHistory: [] };
|
|
1186
1971
|
const executor = makeExecutor([makeAgent("echo")], { maxSubagentSpawnsPerSession: 4 }, false, spawnState);
|
|
1187
1972
|
|
|
1188
1973
|
const status = await executor.execute("status", { action: "status" }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
|
|
1189
1974
|
|
|
1190
|
-
assert.match(status.content[0]?.text ?? "", /^
|
|
1975
|
+
assert.match(status.content[0]?.text ?? "", /^Status target: active runs\nSpawn budget: 3\/5 used, 2 remaining/);
|
|
1191
1976
|
assert.deepEqual(status.details?.spawnBudget, {
|
|
1192
1977
|
used: 3,
|
|
1193
1978
|
configuredLimit: 4,
|
|
@@ -1409,6 +2194,52 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1409
2194
|
assert.deepEqual(result.details?.totalCost, { inputTokens: 100, outputTokens: 50, costUsd: 0.001 });
|
|
1410
2195
|
});
|
|
1411
2196
|
|
|
2197
|
+
it("ignores stale foreground control notification contexts after reload", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2198
|
+
mockPi.onCall({
|
|
2199
|
+
jsonl: [
|
|
2200
|
+
events.assistantMessage("first update"),
|
|
2201
|
+
events.assistantMessage("second update"),
|
|
2202
|
+
],
|
|
2203
|
+
});
|
|
2204
|
+
const state: SubagentState = {
|
|
2205
|
+
baseCwd: tempDir,
|
|
2206
|
+
currentSessionId: null,
|
|
2207
|
+
asyncJobs: new Map(),
|
|
2208
|
+
foregroundRuns: new Map(),
|
|
2209
|
+
foregroundControls: new Map(),
|
|
2210
|
+
lastForegroundControlId: null,
|
|
2211
|
+
};
|
|
2212
|
+
const staleEvents = {
|
|
2213
|
+
on: createEventBus().on,
|
|
2214
|
+
emit() { throw new Error("This extension ctx is stale after session replacement or reload."); },
|
|
2215
|
+
};
|
|
2216
|
+
const updates: ExecutorToolResult[] = [];
|
|
2217
|
+
const executor = createSubagentExecutor!({
|
|
2218
|
+
pi: { events: staleEvents, getSessionName: () => undefined },
|
|
2219
|
+
state,
|
|
2220
|
+
config: { control: { enabled: true, activeNoticeAfterTurns: 2, activeNoticeAfterMs: 999_999, activeNoticeAfterTokens: 999_999, notifyOn: ["active_long_running"], notifyChannels: ["event"] } },
|
|
2221
|
+
asyncByDefault: false,
|
|
2222
|
+
tempArtifactsDir: tempDir,
|
|
2223
|
+
getSubagentSessionRoot: () => path.join(tempDir, ".pi-subagents", "sessions"),
|
|
2224
|
+
expandTilde: (value: string) => value,
|
|
2225
|
+
discoverAgents: () => ({ agents: [makeAgent("echo")] }),
|
|
2226
|
+
allowMutatingManagementActions: true,
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
const result = await executor.execute(
|
|
2230
|
+
"stale-control-context",
|
|
2231
|
+
{ agent: "echo", task: "Investigate behavior", async: false },
|
|
2232
|
+
new AbortController().signal,
|
|
2233
|
+
(update: ExecutorToolResult) => updates.push(update),
|
|
2234
|
+
makeMinimalCtx(tempDir),
|
|
2235
|
+
);
|
|
2236
|
+
|
|
2237
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "foreground run failed");
|
|
2238
|
+
assert.equal(result.details.results[0]?.exitCode, 0);
|
|
2239
|
+
const controlEvents = updates.flatMap((update) => update.details?.controlEvents ?? []);
|
|
2240
|
+
assert.equal(controlEvents[0]?.type, "active_long_running");
|
|
2241
|
+
});
|
|
2242
|
+
|
|
1412
2243
|
it("emits resolved model and thinking for nested foreground starts", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1413
2244
|
mockPi.onCall({ output: "nested result" });
|
|
1414
2245
|
const route = createNestedRoute("root-nested-model");
|
|
@@ -1502,10 +2333,10 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1502
2333
|
|
|
1503
2334
|
it("fails implementation runs that complete without mutation attempts", async () => {
|
|
1504
2335
|
mockPi.onCall({ output: "Validation:\nlet rawFilename = params.filename.trim();" });
|
|
1505
|
-
const agents = [makeAgent("
|
|
2336
|
+
const agents = [makeAgent("worker")];
|
|
1506
2337
|
const controlEvents: Array<{ message: string }> = [];
|
|
1507
2338
|
|
|
1508
|
-
const result = await runSync(tempDir, agents, "
|
|
2339
|
+
const result = await runSync(tempDir, agents, "worker", "Implement the approved file changes", {
|
|
1509
2340
|
runId: "guard-run",
|
|
1510
2341
|
onControlEvent: (event: { message: string }) => controlEvents.push(event),
|
|
1511
2342
|
});
|
|
@@ -1515,18 +2346,18 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1515
2346
|
assert.equal(result.finalOutput, "Validation:\nlet rawFilename = params.filename.trim();");
|
|
1516
2347
|
assert.equal(result.progress.status, "failed");
|
|
1517
2348
|
assert.deepEqual(controlEvents.map((event) => event.message), [
|
|
1518
|
-
"
|
|
2349
|
+
"worker completed without making edits for an implementation task",
|
|
1519
2350
|
]);
|
|
1520
2351
|
assert.deepEqual(result.controlEvents?.map((event) => event.message), [
|
|
1521
|
-
"
|
|
2352
|
+
"worker completed without making edits for an implementation task",
|
|
1522
2353
|
]);
|
|
1523
2354
|
});
|
|
1524
2355
|
|
|
1525
2356
|
it("agent contract v1 reports omitted acceptance separately without injecting a prompt", async () => {
|
|
1526
2357
|
mockPi.onCall({ output: "Plan only" });
|
|
1527
|
-
const agents = [makeAgent("
|
|
2358
|
+
const agents = [makeAgent("worker", { tools: ["read", "write"] })];
|
|
1528
2359
|
|
|
1529
|
-
const result = await runSync(tempDir, agents, "
|
|
2360
|
+
const result = await runSync(tempDir, agents, "worker", "Implement the approved file changes", {
|
|
1530
2361
|
runId: "v1-no-acceptance",
|
|
1531
2362
|
agentContract: { version: 1 },
|
|
1532
2363
|
});
|
|
@@ -1543,9 +2374,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1543
2374
|
|
|
1544
2375
|
it("agent contract v1 keeps acceptance rejection out of execution status", async () => {
|
|
1545
2376
|
mockPi.onCall({ output: "Done\n```acceptance-report\n{\"criteriaSatisfied\":[{\"id\":\"criterion-1\",\"status\":\"not-satisfied\",\"evidence\":\"no proof\"}]}\n```" });
|
|
1546
|
-
const agents = [makeAgent("
|
|
2377
|
+
const agents = [makeAgent("worker", { tools: ["read"], completionGuard: false })];
|
|
1547
2378
|
|
|
1548
|
-
const result = await runSync(tempDir, agents, "
|
|
2379
|
+
const result = await runSync(tempDir, agents, "worker", "Summarize the fix", {
|
|
1549
2380
|
runId: "v1-acceptance-reject",
|
|
1550
2381
|
agentContract: { version: 1 },
|
|
1551
2382
|
acceptance: { level: "checked", criteria: ["Return required proof"] },
|
|
@@ -1561,9 +2392,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1561
2392
|
|
|
1562
2393
|
it("agent contract v1 records explicit completion guard as an effect", async () => {
|
|
1563
2394
|
mockPi.onCall({ output: "Plan only" });
|
|
1564
|
-
const agents = [makeAgent("
|
|
2395
|
+
const agents = [makeAgent("worker", { tools: ["read", "write"], completionGuard: true })];
|
|
1565
2396
|
|
|
1566
|
-
const result = await runSync(tempDir, agents, "
|
|
2397
|
+
const result = await runSync(tempDir, agents, "worker", "Implement the approved file changes", {
|
|
1567
2398
|
runId: "v1-completion-effect",
|
|
1568
2399
|
agentContract: { version: 1 },
|
|
1569
2400
|
});
|
|
@@ -1601,6 +2432,36 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1601
2432
|
if (child?.artifactPaths?.outputPath) assert.match(fs.readFileSync(child.artifactPaths.outputPath, "utf-8"), /"note": "captured"/);
|
|
1602
2433
|
});
|
|
1603
2434
|
|
|
2435
|
+
it("accepts recovered tool errors before valid structured output but rejects later errors", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2436
|
+
const recoveredError = { type: "tool_result_end", message: { role: "toolResult", toolName: "read", isError: true, content: [{ type: "text", text: "EISDIR" }] } };
|
|
2437
|
+
const structuredEvents = [
|
|
2438
|
+
{ type: "tool_execution_start", toolName: "structured_output", args: { value: { ok: true } } },
|
|
2439
|
+
{ type: "tool_result_end", message: { role: "toolResult", toolName: "structured_output", content: [{ type: "text", text: "Structured output captured." }] } },
|
|
2440
|
+
{ type: "tool_execution_end", toolName: "structured_output" },
|
|
2441
|
+
];
|
|
2442
|
+
mockPi.onCall({
|
|
2443
|
+
stdoutRaw: [recoveredError, ...structuredEvents].map((entry) => JSON.stringify(entry)).join("\n") + "\n",
|
|
2444
|
+
structuredOutputCapture: { ok: true },
|
|
2445
|
+
});
|
|
2446
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
2447
|
+
const params = { agent: "echo", task: "Return structured data", outputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } }, acceptance: false } as const;
|
|
2448
|
+
|
|
2449
|
+
const recovered = await executor.execute("single-schema-recovered-error", params, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
|
|
2450
|
+
|
|
2451
|
+
assert.equal(recovered.isError, undefined);
|
|
2452
|
+
assert.deepEqual(recovered.details?.results?.[0]?.structuredOutput, { ok: true });
|
|
2453
|
+
|
|
2454
|
+
mockPi.reset();
|
|
2455
|
+
mockPi.onCall({
|
|
2456
|
+
stdoutRaw: [...structuredEvents, recoveredError].map((entry) => JSON.stringify(entry)).join("\n") + "\n",
|
|
2457
|
+
structuredOutputCapture: { ok: true },
|
|
2458
|
+
});
|
|
2459
|
+
const terminal = await executor.execute("single-schema-terminal-error", params, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
|
|
2460
|
+
|
|
2461
|
+
assert.equal(terminal.isError, true);
|
|
2462
|
+
assert.match(terminal.details?.results?.[0]?.error ?? "", /read failed/);
|
|
2463
|
+
});
|
|
2464
|
+
|
|
1604
2465
|
it("rejects structured output capture files that were not produced by the structured_output tool", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1605
2466
|
mockPi.onCall({ output: "spoofed", structuredOutputCapture: { ok: true } });
|
|
1606
2467
|
const executor = makeExecutor([makeAgent("echo")]);
|
|
@@ -1682,11 +2543,11 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1682
2543
|
|
|
1683
2544
|
it("returns captured output when the foreground executor fails an implementation run", async () => {
|
|
1684
2545
|
mockPi.onCall({ output: "Oracle review:\n- finding one\n- finding two" });
|
|
1685
|
-
const executor = makeExecutor([makeAgent("
|
|
2546
|
+
const executor = makeExecutor([makeAgent("oracle")]);
|
|
1686
2547
|
|
|
1687
2548
|
const result = await executor.execute(
|
|
1688
2549
|
"failed-single-output",
|
|
1689
|
-
{ agent: "
|
|
2550
|
+
{ agent: "oracle", task: "Implement the approved file changes" },
|
|
1690
2551
|
new AbortController().signal,
|
|
1691
2552
|
undefined,
|
|
1692
2553
|
makeMinimalCtx(tempDir),
|
|
@@ -1701,9 +2562,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1701
2562
|
|
|
1702
2563
|
it("fails future-tense implementation summaries when no mutation attempt occurred", async () => {
|
|
1703
2564
|
mockPi.onCall({ output: "I’ll do that now and report back after implementing." });
|
|
1704
|
-
const agents = [makeAgent("
|
|
2565
|
+
const agents = [makeAgent("worker")];
|
|
1705
2566
|
|
|
1706
|
-
const result = await runSync(tempDir, agents, "
|
|
2567
|
+
const result = await runSync(tempDir, agents, "worker", "Implement the approved fixes", {
|
|
1707
2568
|
runId: "guard-future-tense",
|
|
1708
2569
|
});
|
|
1709
2570
|
|
|
@@ -1761,9 +2622,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1761
2622
|
events.assistantMessage("Applied edit"),
|
|
1762
2623
|
],
|
|
1763
2624
|
});
|
|
1764
|
-
const agents = [makeAgent("
|
|
2625
|
+
const agents = [makeAgent("worker")];
|
|
1765
2626
|
|
|
1766
|
-
const result = await runSync(tempDir, agents, "
|
|
2627
|
+
const result = await runSync(tempDir, agents, "worker", "Implement the approved file changes", {
|
|
1767
2628
|
runId: "guard-success",
|
|
1768
2629
|
});
|
|
1769
2630
|
|
|
@@ -1774,21 +2635,24 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1774
2635
|
|
|
1775
2636
|
it("resolves explicit agent aliases to canonical execution names", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1776
2637
|
mockPi.onCall({ output: "Implemented" });
|
|
1777
|
-
const executor = makeExecutor([makeAgent("
|
|
2638
|
+
const executor = makeExecutor([makeAgent("worker", { aliases: ["developer"], completionGuard: false })]);
|
|
1778
2639
|
|
|
1779
2640
|
const result = await executor.execute("single", { agent: "developer", task: "Implement" }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
|
|
1780
2641
|
|
|
1781
2642
|
assert.equal(result.isError, undefined);
|
|
1782
|
-
assert.equal(result.details?.results[0]?.agent, "
|
|
2643
|
+
assert.equal(result.details?.results[0]?.agent, "worker");
|
|
1783
2644
|
assert.match(result.content[0]?.text ?? "", /Implemented/);
|
|
1784
2645
|
});
|
|
1785
2646
|
|
|
1786
|
-
it("returns error for unknown agent", async () => {
|
|
2647
|
+
it("returns error for unknown agent without retaining the prompt", async () => {
|
|
1787
2648
|
const agents = makeAgentConfigs(["echo"]);
|
|
1788
|
-
const
|
|
2649
|
+
const sentinel = "PROMPT_AUDIT_SENTINEL_UNKNOWN";
|
|
2650
|
+
const result = await runSync(tempDir, agents, "nonexistent", sentinel, {});
|
|
1789
2651
|
|
|
1790
2652
|
assert.equal(result.exitCode, 1);
|
|
1791
2653
|
assert.ok(result.error?.includes("Unknown agent"));
|
|
2654
|
+
assert.equal(result.task, "[prompt redacted]");
|
|
2655
|
+
assert.doesNotMatch(JSON.stringify(result), new RegExp(sentinel));
|
|
1792
2656
|
});
|
|
1793
2657
|
|
|
1794
2658
|
|
|
@@ -1832,10 +2696,10 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1832
2696
|
events.assistantMessage("I need to retry the same edit."),
|
|
1833
2697
|
],
|
|
1834
2698
|
});
|
|
1835
|
-
const agents = [makeAgent("
|
|
2699
|
+
const agents = [makeAgent("worker")];
|
|
1836
2700
|
const controlEvents: NonNullable<RunSyncResult["controlEvents"]> = [];
|
|
1837
2701
|
|
|
1838
|
-
const result = await runSync(tempDir, agents, "
|
|
2702
|
+
const result = await runSync(tempDir, agents, "worker", "Implement the approved fixes", {
|
|
1839
2703
|
runId: "run-failures",
|
|
1840
2704
|
controlConfig: { enabled: true, failedToolAttemptsBeforeAttention: 3, notifyOn: ["active_long_running", "needs_attention"] },
|
|
1841
2705
|
onControlEvent: (event: NonNullable<RunSyncResult["controlEvents"]>[number]) => controlEvents.push(event),
|
|
@@ -1884,12 +2748,12 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1884
2748
|
it("retries a zero-activity startup exit on the same model", async () => {
|
|
1885
2749
|
mockPi.onCall({ exitCode: 1 });
|
|
1886
2750
|
mockPi.onCall({ output: "Recovered after startup race" });
|
|
1887
|
-
const agents = [makeAgent("
|
|
2751
|
+
const agents = [makeAgent("worker", {
|
|
1888
2752
|
model: "openai/gpt-5-mini",
|
|
1889
2753
|
fallbackModels: ["anthropic/claude-sonnet-4"],
|
|
1890
2754
|
})];
|
|
1891
2755
|
|
|
1892
|
-
const result = await runSync(tempDir, agents, "
|
|
2756
|
+
const result = await runSync(tempDir, agents, "worker", "Do work", {
|
|
1893
2757
|
runId: "startup-retry-sync",
|
|
1894
2758
|
acceptance: false,
|
|
1895
2759
|
});
|
|
@@ -1904,15 +2768,38 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1904
2768
|
assert.equal(mockPi.callCount(), 2);
|
|
1905
2769
|
});
|
|
1906
2770
|
|
|
2771
|
+
it("escalates to file task delivery after a zero-activity SIGKILL startup exit", { skip: process.platform === "win32" ? "POSIX child signal reporting is unavailable on Windows" : false }, async () => {
|
|
2772
|
+
mockPi.onCall({ signal: "SIGKILL" });
|
|
2773
|
+
mockPi.onCall({ output: "Recovered via file delivery" });
|
|
2774
|
+
const agents = [makeAgent("worker", { model: "openai/gpt-5-mini" })];
|
|
2775
|
+
|
|
2776
|
+
const result = await runSync(tempDir, agents, "worker", "Do work", {
|
|
2777
|
+
runId: "startup-sigkill-file-delivery-sync",
|
|
2778
|
+
acceptance: false,
|
|
2779
|
+
});
|
|
2780
|
+
|
|
2781
|
+
assert.equal(result.exitCode, 0);
|
|
2782
|
+
assert.equal(result.finalOutput, "Recovered via file delivery");
|
|
2783
|
+
assert.equal(mockPi.callCount(), 2);
|
|
2784
|
+
const [firstArgs, retryArgs] = readAllCallArgs();
|
|
2785
|
+
assert.ok(firstArgs?.includes("Task: Do work"), "first attempt should deliver the task inline");
|
|
2786
|
+
const retryTaskArg = retryArgs?.at(-1) ?? "";
|
|
2787
|
+
assert.ok(
|
|
2788
|
+
retryTaskArg.startsWith("@") && retryTaskArg.endsWith("task.md"),
|
|
2789
|
+
`retry should reference a task file instead of inline argv, got: ${retryTaskArg}`,
|
|
2790
|
+
);
|
|
2791
|
+
assert.match(result.progress.recentOutput.join("\n"), /\[startup-retry\].*file task delivery/i);
|
|
2792
|
+
});
|
|
2793
|
+
|
|
1907
2794
|
it("does not retry a non-zero exit after tool activity", async () => {
|
|
1908
2795
|
mockPi.onCall({ jsonl: [events.toolStart("read", { path: "package.json" })], exitCode: 1 });
|
|
1909
2796
|
mockPi.onCall({ output: "must not run" });
|
|
1910
|
-
const agents = [makeAgent("
|
|
2797
|
+
const agents = [makeAgent("worker", {
|
|
1911
2798
|
model: "openai/gpt-5-mini",
|
|
1912
2799
|
fallbackModels: ["anthropic/claude-sonnet-4"],
|
|
1913
2800
|
})];
|
|
1914
2801
|
|
|
1915
|
-
const result = await runSync(tempDir, agents, "
|
|
2802
|
+
const result = await runSync(tempDir, agents, "worker", "Read a file", {
|
|
1916
2803
|
runId: "startup-no-retry-after-tool",
|
|
1917
2804
|
acceptance: false,
|
|
1918
2805
|
});
|
|
@@ -1925,9 +2812,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1925
2812
|
it("does not retry non-SIGKILL signaled child exits", { skip: process.platform === "win32" ? "POSIX child signal reporting is unavailable on Windows" : false }, async () => {
|
|
1926
2813
|
mockPi.onCall({ signal: "SIGTERM" });
|
|
1927
2814
|
mockPi.onCall({ output: "must not run" });
|
|
1928
|
-
const agents = [makeAgent("
|
|
2815
|
+
const agents = [makeAgent("worker", { model: "openai/gpt-5-mini" })];
|
|
1929
2816
|
|
|
1930
|
-
const result = await runSync(tempDir, agents, "
|
|
2817
|
+
const result = await runSync(tempDir, agents, "worker", "Do work", {
|
|
1931
2818
|
runId: "startup-no-retry-after-signal",
|
|
1932
2819
|
acceptance: false,
|
|
1933
2820
|
});
|
|
@@ -1941,9 +2828,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1941
2828
|
|
|
1942
2829
|
it("prefers signal termination errors over stderr tails", { skip: process.platform === "win32" ? "POSIX child signal reporting is unavailable on Windows" : false }, async () => {
|
|
1943
2830
|
mockPi.onCall({ stderr: "INFO benign startup line\n", signal: "SIGTERM" });
|
|
1944
|
-
const agents = [makeAgent("
|
|
2831
|
+
const agents = [makeAgent("worker", { model: "openai/gpt-5-mini" })];
|
|
1945
2832
|
|
|
1946
|
-
const result = await runSync(tempDir, agents, "
|
|
2833
|
+
const result = await runSync(tempDir, agents, "worker", "Do work", {
|
|
1947
2834
|
runId: "signal-error-over-stderr",
|
|
1948
2835
|
acceptance: false,
|
|
1949
2836
|
});
|
|
@@ -1957,9 +2844,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1957
2844
|
it("does not retry a child exit with raw stdout diagnostics", async () => {
|
|
1958
2845
|
mockPi.onCall({ stdoutRaw: "configuration failed before protocol startup\n", exitCode: 1 });
|
|
1959
2846
|
mockPi.onCall({ output: "must not run" });
|
|
1960
|
-
const agents = [makeAgent("
|
|
2847
|
+
const agents = [makeAgent("worker", { model: "openai/gpt-5-mini" })];
|
|
1961
2848
|
|
|
1962
|
-
const result = await runSync(tempDir, agents, "
|
|
2849
|
+
const result = await runSync(tempDir, agents, "worker", "Do work", {
|
|
1963
2850
|
runId: "startup-no-retry-after-stdout-diagnostic",
|
|
1964
2851
|
acceptance: false,
|
|
1965
2852
|
});
|
|
@@ -1972,12 +2859,12 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1972
2859
|
|
|
1973
2860
|
it("reports an actionable error after startup retries are exhausted", async () => {
|
|
1974
2861
|
mockPi.onCall({ exitCode: 1 });
|
|
1975
|
-
const agents = [makeAgent("
|
|
2862
|
+
const agents = [makeAgent("worker", {
|
|
1976
2863
|
model: "openai/gpt-5-mini",
|
|
1977
2864
|
fallbackModels: ["anthropic/claude-sonnet-4"],
|
|
1978
2865
|
})];
|
|
1979
2866
|
|
|
1980
|
-
const result = await runSync(tempDir, agents, "
|
|
2867
|
+
const result = await runSync(tempDir, agents, "worker", "Do work", {
|
|
1981
2868
|
runId: "startup-retry-exhausted-sync",
|
|
1982
2869
|
acceptance: false,
|
|
1983
2870
|
});
|
|
@@ -2307,7 +3194,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2307
3194
|
|
|
2308
3195
|
assert.equal(result.exitCode, 1);
|
|
2309
3196
|
assert.match(result.error ?? "", /429 quota exceeded/);
|
|
2310
|
-
|
|
3197
|
+
// No fallback model configured: the last candidate is retried on the same
|
|
3198
|
+
// model up to MODEL_RETRY_DELAYS_MS.length times before giving up.
|
|
3199
|
+
assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, false, false, false]);
|
|
2311
3200
|
});
|
|
2312
3201
|
|
|
2313
3202
|
it("treats recovered child tool errors as successful foreground runs", async () => {
|
|
@@ -2415,7 +3304,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2415
3304
|
});
|
|
2416
3305
|
|
|
2417
3306
|
assert.equal(result.exitCode, 1);
|
|
2418
|
-
|
|
3307
|
+
// Primary fails, fallback fails, then the last (fallback) candidate is
|
|
3308
|
+
// retried on the same model up to MODEL_RETRY_DELAYS_MS.length times.
|
|
3309
|
+
assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, false, false, false, false]);
|
|
2419
3310
|
assert.match(result.error ?? "", /429 quota exceeded/);
|
|
2420
3311
|
});
|
|
2421
3312
|
|
|
@@ -2487,6 +3378,32 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2487
3378
|
assert.ok(result.progress.durationMs > 0, "should track duration");
|
|
2488
3379
|
});
|
|
2489
3380
|
|
|
3381
|
+
it("streams progress while a foreground child has not emitted output", async () => {
|
|
3382
|
+
const updates: Array<{ text: string; durationMs: number | undefined }> = [];
|
|
3383
|
+
const releasePath = path.join(tempDir, "release-foreground-progress");
|
|
3384
|
+
mockPi.onCall({ output: "Done", waitForPath: releasePath });
|
|
3385
|
+
|
|
3386
|
+
const runPromise = runSync(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
|
|
3387
|
+
onUpdate: (update: { content: Array<{ type: string; text?: string }>; details?: { progress?: ProgressSummary[] } }) => {
|
|
3388
|
+
updates.push({
|
|
3389
|
+
text: update.content[0]?.text ?? "",
|
|
3390
|
+
durationMs: update.details?.progress?.[0]?.durationMs,
|
|
3391
|
+
});
|
|
3392
|
+
},
|
|
3393
|
+
});
|
|
3394
|
+
const deadline = Date.now() + 5_000;
|
|
3395
|
+
while (updates.filter((update) => update.text === "(running...)").length < 2 && Date.now() < deadline) {
|
|
3396
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
3397
|
+
}
|
|
3398
|
+
fs.writeFileSync(releasePath, "release", "utf-8");
|
|
3399
|
+
const result = await runPromise;
|
|
3400
|
+
|
|
3401
|
+
const runningUpdates = updates.filter((update) => update.text === "(running...)");
|
|
3402
|
+
assert.equal(result.exitCode, 0);
|
|
3403
|
+
assert.ok(runningUpdates.length >= 2, "expected an initial update and a heartbeat before child output");
|
|
3404
|
+
assert.ok((runningUpdates.at(-1)?.durationMs ?? 0) > (runningUpdates[0]?.durationMs ?? 0), "expected heartbeat duration to advance");
|
|
3405
|
+
});
|
|
3406
|
+
|
|
2490
3407
|
it("tracks live activity updates and exposes artifact paths while running", async () => {
|
|
2491
3408
|
const updates: Array<{ details?: { results?: Array<{ artifactPaths?: ArtifactPaths }>; progress?: ProgressSummary[] } }> = [];
|
|
2492
3409
|
mockPi.onCall({
|
|
@@ -2563,9 +3480,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2563
3480
|
events.assistantMessage("Found 2 files: file1.txt and file2.txt"),
|
|
2564
3481
|
],
|
|
2565
3482
|
});
|
|
2566
|
-
const agents = makeAgentConfigs(["
|
|
3483
|
+
const agents = makeAgentConfigs(["scout"]);
|
|
2567
3484
|
|
|
2568
|
-
const result = await runSync(tempDir, agents, "
|
|
3485
|
+
const result = await runSync(tempDir, agents, "scout", "List files", {});
|
|
2569
3486
|
|
|
2570
3487
|
assert.equal(result.exitCode, 0);
|
|
2571
3488
|
const output = getFinalOutput(result.messages);
|
|
@@ -2592,13 +3509,13 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2592
3509
|
|
|
2593
3510
|
it("injects an agent-file-relative local skill into the foreground child prompt", async () => {
|
|
2594
3511
|
mockPi.onCall({ output: "Done" });
|
|
2595
|
-
const agentFile = path.join(tempDir, "agents", "nested", "
|
|
3512
|
+
const agentFile = path.join(tempDir, "agents", "nested", "worker.md");
|
|
2596
3513
|
const skillFile = path.join(path.dirname(agentFile), "skills", "local", "SKILL.md");
|
|
2597
3514
|
fs.mkdirSync(path.dirname(skillFile), { recursive: true });
|
|
2598
3515
|
fs.writeFileSync(skillFile, "---\ndescription: local skill description\n---\nLocal skill body\n", "utf-8");
|
|
2599
|
-
const agents = [makeAgent("
|
|
3516
|
+
const agents = [makeAgent("worker", { filePath: agentFile, skills: ["local"], skillPath: ["./skills"] })];
|
|
2600
3517
|
|
|
2601
|
-
const result = await runSync(tempDir, agents, "
|
|
3518
|
+
const result = await runSync(tempDir, agents, "worker", "Task", {});
|
|
2602
3519
|
|
|
2603
3520
|
assert.equal(result.exitCode, 0);
|
|
2604
3521
|
assert.deepEqual(result.skills, ["local"]);
|
|
@@ -2622,9 +3539,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2622
3539
|
});
|
|
2623
3540
|
|
|
2624
3541
|
it("fails foreground runs on explicit unavailable pi-subagents skill requests without spawning", async () => {
|
|
2625
|
-
const agents = [makeAgent("
|
|
3542
|
+
const agents = [makeAgent("worker")];
|
|
2626
3543
|
|
|
2627
|
-
const result = await runSync(tempDir, agents, "
|
|
3544
|
+
const result = await runSync(tempDir, agents, "worker", "Task", { skills: ["pi-subagents"] });
|
|
2628
3545
|
|
|
2629
3546
|
assert.equal(result.exitCode, 1);
|
|
2630
3547
|
assert.equal(result.error, "Skills not found: pi-subagents");
|
|
@@ -2632,16 +3549,16 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2632
3549
|
});
|
|
2633
3550
|
|
|
2634
3551
|
it("fails foreground runs when an agent default requests pi-subagents skill", async () => {
|
|
2635
|
-
const agents = [makeAgent("
|
|
3552
|
+
const agents = [makeAgent("worker", { skills: ["pi-subagents"] })];
|
|
2636
3553
|
|
|
2637
|
-
const result = await runSync(tempDir, agents, "
|
|
3554
|
+
const result = await runSync(tempDir, agents, "worker", "Task", {});
|
|
2638
3555
|
|
|
2639
3556
|
assert.equal(result.exitCode, 1);
|
|
2640
3557
|
assert.equal(result.error, "Skills not found: pi-subagents");
|
|
2641
3558
|
assert.equal(mockPi.callCount(), 0);
|
|
2642
3559
|
});
|
|
2643
3560
|
|
|
2644
|
-
it("writes artifacts
|
|
3561
|
+
it("writes artifacts without retaining the effective prompt", async () => {
|
|
2645
3562
|
mockPi.onCall({
|
|
2646
3563
|
output: "Result text",
|
|
2647
3564
|
runtimeAcknowledgedExtensions: { version: 1, source: "child-runtime", ids: ["ext.ok"], omitted: 0 },
|
|
@@ -2649,8 +3566,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2649
3566
|
const privateExtension = path.join(tempDir, "extensions", "private-extension.ts");
|
|
2650
3567
|
const agents = [makeAgent("echo", { extensions: [privateExtension] })];
|
|
2651
3568
|
const artifactsDir = path.join(tempDir, "artifacts");
|
|
3569
|
+
const sentinel = "PROMPT_AUDIT_SENTINEL_1021";
|
|
2652
3570
|
|
|
2653
|
-
const result = await runSync(tempDir, agents, "echo",
|
|
3571
|
+
const result = await runSync(tempDir, agents, "echo", sentinel, {
|
|
2654
3572
|
runId: "test-run",
|
|
2655
3573
|
artifactsDir,
|
|
2656
3574
|
artifactConfig: { enabled: true, includeInput: true, includeOutput: true, includeMetadata: true },
|
|
@@ -2658,16 +3576,27 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2658
3576
|
|
|
2659
3577
|
assert.equal(result.exitCode, 0);
|
|
2660
3578
|
assert.ok(result.artifactPaths, "should have artifact paths");
|
|
3579
|
+
assert.ok(result.artifactPaths.inputPath, "should have a redacted input artifact");
|
|
3580
|
+
assert.doesNotMatch(fs.readFileSync(result.artifactPaths.inputPath, "utf-8"), new RegExp(sentinel));
|
|
3581
|
+
assert.match(fs.readFileSync(result.artifactPaths.inputPath, "utf-8"), /live Prompt Audit only/);
|
|
2661
3582
|
assert.ok(result.transcriptPath, "should expose transcript path on the result");
|
|
2662
3583
|
assert.equal(result.transcriptPath, result.artifactPaths.transcriptPath);
|
|
2663
3584
|
assert.ok(fs.existsSync(result.transcriptPath), "transcript should be written");
|
|
2664
3585
|
const transcript = fs.readFileSync(result.transcriptPath, "utf-8").trim().split("\n").map((line) => JSON.parse(line) as { recordType?: string; source?: string; text?: string });
|
|
2665
3586
|
assert.equal(transcript[0]?.recordType, "message");
|
|
2666
3587
|
assert.equal(transcript[0]?.source, "foreground");
|
|
3588
|
+
assert.match(transcript[0]?.text ?? "", /live Prompt Audit only/);
|
|
3589
|
+
assert.doesNotMatch(fs.readFileSync(result.transcriptPath, "utf-8"), new RegExp(sentinel));
|
|
2667
3590
|
assert.match(transcript.at(-1)?.text ?? "", /^Result text/);
|
|
2668
3591
|
assert.equal(result.transcriptError, undefined);
|
|
2669
3592
|
assert.ok(fs.existsSync(artifactsDir), "artifacts dir should exist");
|
|
2670
|
-
const
|
|
3593
|
+
const metadataText = fs.readFileSync(result.artifactPaths.metadataPath, "utf-8");
|
|
3594
|
+
const metadata = JSON.parse(metadataText) as { task?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions };
|
|
3595
|
+
assert.doesNotMatch(metadataText, new RegExp(sentinel));
|
|
3596
|
+
assert.equal(metadata.task, "[prompt redacted]");
|
|
3597
|
+
assert.equal(result.task, "[prompt redacted]");
|
|
3598
|
+
assert.equal(result.progress.task, "[prompt redacted]");
|
|
3599
|
+
assert.match(readCallArgs().join("\n"), new RegExp(sentinel));
|
|
2671
3600
|
assert.equal(metadata.launchContractDigest, result.launchContractDigest);
|
|
2672
3601
|
assert.equal(result.launchResolvedExtensions?.source, "launch-resolved");
|
|
2673
3602
|
assert.equal(result.launchResolvedExtensions?.disableAmbientExtensions, true);
|
|
@@ -2806,6 +3735,28 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2806
3735
|
assert.equal(fs.readFileSync(outputPath, "utf-8"), "fresh assistant output");
|
|
2807
3736
|
});
|
|
2808
3737
|
|
|
3738
|
+
it("top-level reviewer runs do not inherit bundled chain artifact reads", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
3739
|
+
fs.writeFileSync(path.join(tempDir, "plan.md"), "chain plan");
|
|
3740
|
+
fs.writeFileSync(path.join(tempDir, "progress.md"), "chain progress");
|
|
3741
|
+
mockPi.onCall({ output: "Review done" });
|
|
3742
|
+
const reviewer = discoverAgents(tempDir, "project").agents.find((agent) => agent.name === "reviewer");
|
|
3743
|
+
assert.ok(reviewer, "expected bundled reviewer");
|
|
3744
|
+
assert.equal(reviewer.defaultReads, undefined);
|
|
3745
|
+
const executor = makeExecutor([reviewer]);
|
|
3746
|
+
|
|
3747
|
+
await executor.execute(
|
|
3748
|
+
"single-reviewer-without-chain-artifacts",
|
|
3749
|
+
{ agent: "reviewer", task: "Review the supplied files." },
|
|
3750
|
+
new AbortController().signal,
|
|
3751
|
+
undefined,
|
|
3752
|
+
makeMinimalCtx(tempDir),
|
|
3753
|
+
);
|
|
3754
|
+
|
|
3755
|
+
const taskArg = readCallArgs().at(-1) ?? "";
|
|
3756
|
+
assert.doesNotMatch(taskArg, /\[Read from:/);
|
|
3757
|
+
assert.doesNotMatch(taskArg, /plan\.md|progress\.md/);
|
|
3758
|
+
});
|
|
3759
|
+
|
|
2809
3760
|
it("routes foreground single relative outputs to the run output artifact directory by default", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
2810
3761
|
mockPi.onCall({ output: "default report" });
|
|
2811
3762
|
const executor = makeExecutor([makeAgent("researcher", { output: "context.md" })]);
|
|
@@ -3034,6 +3985,81 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
3034
3985
|
assert.equal(agentResult.details?.timeoutMs, 4_000);
|
|
3035
3986
|
});
|
|
3036
3987
|
|
|
3988
|
+
it("threads the global config timeout default from deps.config, without overriding explicit or agent values", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
3989
|
+
const NINETY_MIN = 90 * 60 * 1000;
|
|
3990
|
+
mockPi.onCall({ output: "config default" });
|
|
3991
|
+
mockPi.onCall({ output: "explicit over config" });
|
|
3992
|
+
mockPi.onCall({ output: "agent over config" });
|
|
3993
|
+
mockPi.onCall({ output: "invalid config ignored" });
|
|
3994
|
+
|
|
3995
|
+
// A global config.timeoutMs replaces the built-in 30-minute foreground backstop.
|
|
3996
|
+
const configExecutor = makeExecutor([makeAgent("echo")], { timeoutMs: NINETY_MIN });
|
|
3997
|
+
const configResult = await configExecutor.execute(
|
|
3998
|
+
"config-timeout-default",
|
|
3999
|
+
{ agent: "echo", task: "Task", async: false },
|
|
4000
|
+
new AbortController().signal,
|
|
4001
|
+
undefined,
|
|
4002
|
+
makeMinimalCtx(tempDir),
|
|
4003
|
+
);
|
|
4004
|
+
assert.equal(configResult.details?.timeoutMs, NINETY_MIN);
|
|
4005
|
+
|
|
4006
|
+
// An explicit call value still wins over the global config default.
|
|
4007
|
+
const explicitResult = await configExecutor.execute(
|
|
4008
|
+
"config-timeout-explicit",
|
|
4009
|
+
{ agent: "echo", task: "Task", async: false, timeoutMs: 2_000 },
|
|
4010
|
+
new AbortController().signal,
|
|
4011
|
+
undefined,
|
|
4012
|
+
makeMinimalCtx(tempDir),
|
|
4013
|
+
);
|
|
4014
|
+
assert.equal(explicitResult.details?.timeoutMs, 2_000);
|
|
4015
|
+
|
|
4016
|
+
// An agent frontmatter default still wins over the global config default (single launches).
|
|
4017
|
+
const agentResult = await makeExecutor([makeAgent("echo", { defaultTimeoutMs: 4_000 })], { timeoutMs: NINETY_MIN }).execute(
|
|
4018
|
+
"config-timeout-agent-default",
|
|
4019
|
+
{ agent: "echo", task: "Task", async: false },
|
|
4020
|
+
new AbortController().signal,
|
|
4021
|
+
undefined,
|
|
4022
|
+
makeMinimalCtx(tempDir),
|
|
4023
|
+
);
|
|
4024
|
+
assert.equal(agentResult.details?.timeoutMs, 4_000);
|
|
4025
|
+
|
|
4026
|
+
// An invalid config value is ignored -> falls back to the built-in 30-minute default.
|
|
4027
|
+
const invalidResult = await makeExecutor([makeAgent("echo")], { timeoutMs: -1 }).execute(
|
|
4028
|
+
"config-timeout-invalid",
|
|
4029
|
+
{ agent: "echo", task: "Task", async: false },
|
|
4030
|
+
new AbortController().signal,
|
|
4031
|
+
undefined,
|
|
4032
|
+
makeMinimalCtx(tempDir),
|
|
4033
|
+
);
|
|
4034
|
+
assert.equal(invalidResult.details?.timeoutMs, executorMod?.DEFAULT_FOREGROUND_TIMEOUT_MS);
|
|
4035
|
+
});
|
|
4036
|
+
|
|
4037
|
+
it("applies the global config timeout default to foreground workflow scripts", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
4038
|
+
mockPi.onCall({ delay: 5_000, output: "too late" });
|
|
4039
|
+
mockPi.onCall({ delay: 5_000, output: "too late" });
|
|
4040
|
+
const executor = makeExecutor([makeAgent("echo")], { timeoutMs: 250 });
|
|
4041
|
+
|
|
4042
|
+
const configResult = await executor.execute(
|
|
4043
|
+
"workflow-config-timeout-default",
|
|
4044
|
+
{ async: false, workflowScript: `return await runs.run("slow", { agent: "echo", task: "Wait" });` },
|
|
4045
|
+
new AbortController().signal,
|
|
4046
|
+
undefined,
|
|
4047
|
+
makeMinimalCtx(tempDir),
|
|
4048
|
+
);
|
|
4049
|
+
assert.equal(configResult.isError, true);
|
|
4050
|
+
assert.match(configResult.content[0]?.text ?? "", /Workflow script timed out after 250ms/);
|
|
4051
|
+
|
|
4052
|
+
const explicitResult = await executor.execute(
|
|
4053
|
+
"workflow-config-timeout-explicit",
|
|
4054
|
+
{ async: false, timeoutMs: 150, workflowScript: `return await runs.run("slow", { agent: "echo", task: "Wait" });` },
|
|
4055
|
+
new AbortController().signal,
|
|
4056
|
+
undefined,
|
|
4057
|
+
makeMinimalCtx(tempDir),
|
|
4058
|
+
);
|
|
4059
|
+
assert.equal(explicitResult.isError, true);
|
|
4060
|
+
assert.match(explicitResult.content[0]?.text ?? "", /Workflow script timed out after 150ms/);
|
|
4061
|
+
});
|
|
4062
|
+
|
|
3037
4063
|
it("runs omitted async launches in the background when the global default is enabled", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
3038
4064
|
const executor = makeExecutor([makeAgent("echo")], {}, true);
|
|
3039
4065
|
|
|
@@ -3310,8 +4336,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
3310
4336
|
});
|
|
3311
4337
|
|
|
3312
4338
|
mockPi.onCall({ echoEnv: envKeys });
|
|
3313
|
-
const nonFanoutAgents = [makeAgent("
|
|
3314
|
-
const nonFanout = await runSync(tempDir, nonFanoutAgents, "
|
|
4339
|
+
const nonFanoutAgents = [makeAgent("worker", { tools: ["read"] })];
|
|
4340
|
+
const nonFanout = await runSync(tempDir, nonFanoutAgents, "worker", "Task", { runId: "non-fanout-run" });
|
|
3315
4341
|
assert.equal(nonFanout.exitCode, 0);
|
|
3316
4342
|
assert.deepEqual(JSON.parse(nonFanout.finalOutput ?? "{}"), {
|
|
3317
4343
|
SELESAI_SUBAGENT_FANOUT_CHILD: "0",
|
|
@@ -3357,9 +4383,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
3357
4383
|
|
|
3358
4384
|
it("fails with actionable diagnostics when a requested extension tool is not loaded", async () => {
|
|
3359
4385
|
mockPi.onCall({ output: "Model incorrectly claimed success", missingTools: ["fixture_search"] });
|
|
3360
|
-
const agents = [makeAgent("extension-
|
|
4386
|
+
const agents = [makeAgent("extension-worker", { tools: ["read", "fixture_search"], fallbackModels: ["mock/fallback-model"] })];
|
|
3361
4387
|
|
|
3362
|
-
const result = await runSync(tempDir, agents, "extension-
|
|
4388
|
+
const result = await runSync(tempDir, agents, "extension-worker", "Use fixture search", { runId: "missing-extension-tool" });
|
|
3363
4389
|
|
|
3364
4390
|
assert.equal(result.exitCode, 1);
|
|
3365
4391
|
assert.match(result.error ?? "", /requested unavailable child tools: fixture_search/);
|
|
@@ -3585,9 +4611,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
3585
4611
|
mockAssistantMessage("final wrapped output", "stop"),
|
|
3586
4612
|
],
|
|
3587
4613
|
});
|
|
3588
|
-
const agents = makeAgentConfigs(["
|
|
4614
|
+
const agents = makeAgentConfigs(["worker"]);
|
|
3589
4615
|
|
|
3590
|
-
const result = await runSync(tempDir, agents, "
|
|
4616
|
+
const result = await runSync(tempDir, agents, "worker", "Use the final grace turn to wrap up.", {
|
|
3591
4617
|
turnBudget: { maxTurns: 1, graceTurns: 1 },
|
|
3592
4618
|
runId: "foreground-turn-budget-soft",
|
|
3593
4619
|
});
|
|
@@ -3620,7 +4646,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
3620
4646
|
},
|
|
3621
4647
|
],
|
|
3622
4648
|
});
|
|
3623
|
-
const agents = makeAgentConfigs(["
|
|
4649
|
+
const agents = makeAgentConfigs(["worker"]);
|
|
3624
4650
|
const snapshots: Array<{
|
|
3625
4651
|
turnBudget?: { outcome?: string; terminationDeferredAtTurn?: number };
|
|
3626
4652
|
turnBudgetExceeded?: boolean;
|
|
@@ -3629,7 +4655,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
3629
4655
|
status?: string;
|
|
3630
4656
|
}> = [];
|
|
3631
4657
|
|
|
3632
|
-
const result = await runSync(tempDir, agents, "
|
|
4658
|
+
const result = await runSync(tempDir, agents, "worker", "Finish active tool work before enforcing the hard limit.", {
|
|
3633
4659
|
turnBudget: { maxTurns: 1, graceTurns: 0 },
|
|
3634
4660
|
runId: "foreground-turn-budget-deferred",
|
|
3635
4661
|
onUpdate(update: { details?: { results?: Array<{ turnBudget?: { outcome?: string; terminationDeferredAtTurn?: number }; turnBudgetExceeded?: boolean; error?: string }>; progress?: Array<{ currentTool?: string; status?: string }> } }) {
|