@selesai/code 0.5.20 → 0.5.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extensions/handoff-new.test.ts +8 -9
- package/dist/extensions/handoff-new.ts +6 -8
- package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
- package/dist/extensions/pi-subagents/README.md +602 -220
- package/dist/extensions/pi-subagents/banner.png +0 -0
- package/dist/extensions/pi-subagents/index.ts +1 -0
- package/dist/extensions/pi-subagents/install.mjs +3 -3
- package/dist/extensions/pi-subagents/package-lock.json +3405 -0
- package/dist/extensions/pi-subagents/package.json +26 -14
- 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-context-build.md +3 -3
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
- 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 +13 -11
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
- package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
- package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
- package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
- package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
- package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
- package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
- package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
- package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
- package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
- package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
- package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
- package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
- package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
- package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
- package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
- package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
- package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
- package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
- package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
- package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
- package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
- package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
- package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
- package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
- package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
- package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
- package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
- package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
- package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
- package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
- package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
- package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
- package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
- package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
- package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
- package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
- package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
- package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
- package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
- package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
- package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
- package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
- package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
- package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
- package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
- package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -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 +478 -19
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
- package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
- package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
- package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
- package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
- package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
- package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
- package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
- package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
- package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
- package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
- package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
- package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
- package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
- package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
- package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
- package/dist/extensions/question/batch.ts +39 -0
- package/dist/extensions/question/dialog-adapter.ts +23 -1
- package/dist/extensions/question/index.ts +237 -7
- package/dist/extensions/question/schemas.ts +28 -15
- package/dist/extensions/question/tests/batch.test.ts +26 -0
- package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
- package/dist/extensions/question/tui-adapter.ts +2 -3
- package/dist/extensions/question/types.ts +37 -0
- package/dist/extensions/question/ui-protocol.ts +3 -3
- package/dist/skills/batch-grill-me/SKILL.md +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
finalizeProcessTerminal,
|
|
8
|
+
processTerminalPath,
|
|
9
|
+
readProcessTerminal,
|
|
10
|
+
sanitizeProcessTerminal,
|
|
11
|
+
writeProcessTerminalCandidate,
|
|
12
|
+
} from "../../src/runs/background/process-terminal.ts";
|
|
13
|
+
|
|
14
|
+
test("process-terminal proof requires the matching runner instance and writer close records", () => {
|
|
15
|
+
const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
|
|
16
|
+
try {
|
|
17
|
+
writeProcessTerminalCandidate(asyncDir, {
|
|
18
|
+
version: 1,
|
|
19
|
+
runId: "run-1",
|
|
20
|
+
runnerProcessInstanceId: "runner-1",
|
|
21
|
+
expectedWriters: { "0": 1 },
|
|
22
|
+
writers: {
|
|
23
|
+
"0": [{
|
|
24
|
+
processInstanceId: "writer-1",
|
|
25
|
+
kind: "pi-writer",
|
|
26
|
+
attempt: 0,
|
|
27
|
+
closeObservedAt: 10,
|
|
28
|
+
exitCode: 0,
|
|
29
|
+
signal: null,
|
|
30
|
+
}],
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({ runId: "run-1", state: "complete", lifecycleArtifactVersion: 3, steps: [{ agent: "worker", status: "complete" }] }));
|
|
34
|
+
fs.writeFileSync(path.join(asyncDir, "events.jsonl"), "");
|
|
35
|
+
|
|
36
|
+
const mismatch = finalizeProcessTerminal(asyncDir, "run-1", {
|
|
37
|
+
processInstanceId: "runner-2",
|
|
38
|
+
closeObservedAt: 20,
|
|
39
|
+
exitCode: 0,
|
|
40
|
+
signal: null,
|
|
41
|
+
});
|
|
42
|
+
assert.equal(mismatch.state, "unknown");
|
|
43
|
+
assert.equal(mismatch.reason, "runner-instance-mismatch");
|
|
44
|
+
|
|
45
|
+
fs.rmSync(processTerminalPath(asyncDir), { force: true });
|
|
46
|
+
const observed = finalizeProcessTerminal(asyncDir, "run-1", {
|
|
47
|
+
processInstanceId: "runner-1",
|
|
48
|
+
closeObservedAt: 30,
|
|
49
|
+
exitCode: 0,
|
|
50
|
+
signal: null,
|
|
51
|
+
});
|
|
52
|
+
assert.equal(observed.state, "observed");
|
|
53
|
+
assert.equal(observed.instances?.length, 2);
|
|
54
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf8")).processTerminal.state, "observed");
|
|
55
|
+
} finally {
|
|
56
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("process-terminal rejects missing writer close evidence", () => {
|
|
61
|
+
const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
|
|
62
|
+
try {
|
|
63
|
+
writeProcessTerminalCandidate(asyncDir, {
|
|
64
|
+
version: 1,
|
|
65
|
+
runId: "run-missing-writer",
|
|
66
|
+
runnerProcessInstanceId: "runner-missing-writer",
|
|
67
|
+
expectedWriters: { "0": 1 },
|
|
68
|
+
writers: { "0": [] },
|
|
69
|
+
});
|
|
70
|
+
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({ runId: "run-missing-writer", state: "complete", lifecycleArtifactVersion: 3, steps: [{ agent: "worker", status: "complete" }] }));
|
|
71
|
+
fs.writeFileSync(path.join(asyncDir, "events.jsonl"), "");
|
|
72
|
+
const proof = finalizeProcessTerminal(asyncDir, "run-missing-writer", {
|
|
73
|
+
processInstanceId: "runner-missing-writer",
|
|
74
|
+
closeObservedAt: 40,
|
|
75
|
+
exitCode: 1,
|
|
76
|
+
signal: null,
|
|
77
|
+
});
|
|
78
|
+
assert.equal(proof.state, "unknown");
|
|
79
|
+
assert.equal(proof.reason, "writer-close-unverified");
|
|
80
|
+
} finally {
|
|
81
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("malformed process-terminal sidecars project unknown instead of throwing", () => {
|
|
86
|
+
const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
|
|
87
|
+
try {
|
|
88
|
+
fs.writeFileSync(processTerminalPath(asyncDir), JSON.stringify({ version: 1, state: "bogus" }));
|
|
89
|
+
const proof = readProcessTerminal(asyncDir, { runId: "malformed-run", runnerProcessInstanceId: "malformed-runner" });
|
|
90
|
+
assert.equal(proof?.state, "unknown");
|
|
91
|
+
assert.equal(proof?.reason, "proof-write-failed");
|
|
92
|
+
assert.match(proof?.diagnostic ?? "", /Invalid process-terminal proof/);
|
|
93
|
+
fs.writeFileSync(path.join(asyncDir, "events.jsonl"), "");
|
|
94
|
+
const finalized = finalizeProcessTerminal(asyncDir, "malformed-run", {
|
|
95
|
+
processInstanceId: "malformed-runner",
|
|
96
|
+
closeObservedAt: 10,
|
|
97
|
+
exitCode: 1,
|
|
98
|
+
signal: null,
|
|
99
|
+
});
|
|
100
|
+
assert.equal(finalized.state, "unknown");
|
|
101
|
+
} finally {
|
|
102
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("process-terminal preserves stopped non-resumability and requires lease release acknowledgement", () => {
|
|
107
|
+
const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
|
|
108
|
+
try {
|
|
109
|
+
writeProcessTerminalCandidate(asyncDir, {
|
|
110
|
+
version: 1,
|
|
111
|
+
runId: "stopped-run",
|
|
112
|
+
runnerProcessInstanceId: "stopped-runner",
|
|
113
|
+
expectedWriters: { "0": 1 },
|
|
114
|
+
writers: {
|
|
115
|
+
"0": [{ processInstanceId: "stopped-writer", kind: "pi-writer", attempt: 0, closeObservedAt: 10, exitCode: 0, signal: null }],
|
|
116
|
+
},
|
|
117
|
+
revivalLeaseToken: "lease-token",
|
|
118
|
+
revivalLeaseReleaseAcknowledged: false,
|
|
119
|
+
});
|
|
120
|
+
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({ runId: "stopped-run", state: "stopped", lifecycleArtifactVersion: 3, steps: [{ agent: "worker", status: "stopped" }] }));
|
|
121
|
+
fs.writeFileSync(path.join(asyncDir, "events.jsonl"), "");
|
|
122
|
+
const unverified = finalizeProcessTerminal(asyncDir, "stopped-run", { processInstanceId: "stopped-runner", closeObservedAt: 20, exitCode: 0, signal: null });
|
|
123
|
+
assert.equal(unverified.state, "unknown");
|
|
124
|
+
assert.equal(unverified.reason, "canonical-session-release-unverified");
|
|
125
|
+
|
|
126
|
+
fs.rmSync(processTerminalPath(asyncDir), { force: true });
|
|
127
|
+
const candidatePath = path.join(asyncDir, "process-terminal-candidate.json");
|
|
128
|
+
const candidate = JSON.parse(fs.readFileSync(candidatePath, "utf8"));
|
|
129
|
+
candidate.revivalLeaseReleaseAcknowledged = true;
|
|
130
|
+
delete candidate.revivalLeaseToken;
|
|
131
|
+
fs.writeFileSync(candidatePath, JSON.stringify(candidate));
|
|
132
|
+
const observed = finalizeProcessTerminal(asyncDir, "stopped-run", { processInstanceId: "stopped-runner", closeObservedAt: 30, exitCode: 0, signal: null });
|
|
133
|
+
assert.equal(observed.state, "observed");
|
|
134
|
+
assert.equal(observed.resumeDisposition, "non-resumable");
|
|
135
|
+
} finally {
|
|
136
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
test("process-terminal rejects cross-run observed sidecars and inconsistent expected writer maps", () => {
|
|
142
|
+
const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
|
|
143
|
+
try {
|
|
144
|
+
fs.writeFileSync(processTerminalPath(asyncDir), JSON.stringify({
|
|
145
|
+
version: 1,
|
|
146
|
+
state: "observed",
|
|
147
|
+
runId: "wrong-run",
|
|
148
|
+
runnerProcessInstanceId: "runner-1",
|
|
149
|
+
observedAt: 10,
|
|
150
|
+
instances: [{ processInstanceId: "runner-1", kind: "runner", closeObservedAt: 10, exitCode: 0, signal: null }],
|
|
151
|
+
}));
|
|
152
|
+
const crossRun = finalizeProcessTerminal(asyncDir, "actual-run", { processInstanceId: "runner-1", closeObservedAt: 20, exitCode: 0, signal: null });
|
|
153
|
+
assert.equal(crossRun.state, "unknown");
|
|
154
|
+
assert.equal(crossRun.reason, "proof-write-failed");
|
|
155
|
+
assert.match(crossRun.diagnostic ?? "", /wrong-run|expected/);
|
|
156
|
+
|
|
157
|
+
fs.rmSync(processTerminalPath(asyncDir), { force: true });
|
|
158
|
+
writeProcessTerminalCandidate(asyncDir, {
|
|
159
|
+
version: 1,
|
|
160
|
+
runId: "actual-run",
|
|
161
|
+
runnerProcessInstanceId: "runner-1",
|
|
162
|
+
expectedWriters: { "0": 0 },
|
|
163
|
+
writers: {
|
|
164
|
+
"0": [{ processInstanceId: "unexpected-writer", kind: "pi-writer", attempt: 0, closeObservedAt: 10, exitCode: 0, signal: null }],
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({ runId: "actual-run", state: "complete", lifecycleArtifactVersion: 3, steps: [{ agent: "worker", status: "complete" }] }));
|
|
168
|
+
fs.writeFileSync(path.join(asyncDir, "events.jsonl"), "");
|
|
169
|
+
const inconsistent = finalizeProcessTerminal(asyncDir, "actual-run", { processInstanceId: "runner-1", closeObservedAt: 30, exitCode: 0, signal: null });
|
|
170
|
+
assert.equal(inconsistent.state, "unknown");
|
|
171
|
+
assert.equal(inconsistent.reason, "writer-close-unverified");
|
|
172
|
+
} finally {
|
|
173
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("process-terminal sanitizes malformed status fallback proofs", () => {
|
|
178
|
+
const malformed = readProcessTerminal("/no/such/dir", { runId: "run-fallback", runnerProcessInstanceId: "runner-fallback" });
|
|
179
|
+
assert.equal(malformed, undefined);
|
|
180
|
+
const sanitized = sanitizeProcessTerminal({ version: 1, state: "bogus", runId: "run-fallback", runnerProcessInstanceId: "runner-fallback" }, { runId: "run-fallback", runnerProcessInstanceId: "runner-fallback" }, "status.json");
|
|
181
|
+
assert.equal(sanitized?.state, "unknown");
|
|
182
|
+
assert.equal(sanitized?.reason, "proof-write-failed");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("process-terminal reports unknown when the runner candidate is unavailable", () => {
|
|
186
|
+
const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
|
|
187
|
+
try {
|
|
188
|
+
const proof = finalizeProcessTerminal(asyncDir, "run-2", {
|
|
189
|
+
processInstanceId: "runner-2",
|
|
190
|
+
closeObservedAt: 40,
|
|
191
|
+
exitCode: 1,
|
|
192
|
+
signal: null,
|
|
193
|
+
});
|
|
194
|
+
assert.deepEqual(proof, {
|
|
195
|
+
version: 1,
|
|
196
|
+
state: "unknown",
|
|
197
|
+
runId: "run-2",
|
|
198
|
+
runnerProcessInstanceId: "runner-2",
|
|
199
|
+
reason: "runner-candidate-missing",
|
|
200
|
+
});
|
|
201
|
+
} finally {
|
|
202
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
checkSubagentDepth,
|
|
5
5
|
getSubagentDepthEnv,
|
|
6
6
|
DEFAULT_SUBAGENT_MAX_DEPTH,
|
|
7
|
-
DEFAULT_MAX_SUBAGENT_SPAWNS_PER_SESSION,
|
|
8
7
|
normalizeMaxSubagentDepth,
|
|
9
8
|
normalizeMaxSubagentSpawnsPerSession,
|
|
10
9
|
resolveMaxSubagentSpawnsPerSession,
|
|
@@ -39,12 +38,6 @@ describe("DEFAULT_SUBAGENT_MAX_DEPTH", () => {
|
|
|
39
38
|
});
|
|
40
39
|
});
|
|
41
40
|
|
|
42
|
-
describe("DEFAULT_MAX_SUBAGENT_SPAWNS_PER_SESSION", () => {
|
|
43
|
-
it("is 40", () => {
|
|
44
|
-
assert.equal(DEFAULT_MAX_SUBAGENT_SPAWNS_PER_SESSION, 40);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
41
|
describe("normalizeMaxSubagentDepth", () => {
|
|
49
42
|
it("accepts integers >= 0", () => {
|
|
50
43
|
assert.equal(normalizeMaxSubagentDepth(0), 0);
|
|
@@ -74,20 +67,35 @@ describe("normalizeMaxSubagentSpawnsPerSession", () => {
|
|
|
74
67
|
});
|
|
75
68
|
|
|
76
69
|
describe("resolveMaxSubagentSpawnsPerSession", () => {
|
|
77
|
-
it("uses env
|
|
70
|
+
it("uses positive env values as opt-in caps", () => {
|
|
78
71
|
process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_SESSION = "5";
|
|
79
72
|
assert.equal(resolveMaxSubagentSpawnsPerSession(1), 5);
|
|
80
73
|
});
|
|
81
74
|
|
|
82
|
-
it("falls back to config when env is absent", () => {
|
|
75
|
+
it("falls back to a positive config cap when env is absent", () => {
|
|
83
76
|
delete process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_SESSION;
|
|
84
77
|
assert.equal(resolveMaxSubagentSpawnsPerSession(7), 7);
|
|
85
78
|
});
|
|
86
79
|
|
|
87
|
-
it("
|
|
80
|
+
it("ignores invalid env values and falls back to config", () => {
|
|
81
|
+
for (const value of ["garbage", "-1", "1.5"]) {
|
|
82
|
+
process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_SESSION = value;
|
|
83
|
+
assert.equal(resolveMaxSubagentSpawnsPerSession(7), 7);
|
|
84
|
+
assert.equal(resolveMaxSubagentSpawnsPerSession(undefined), undefined);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("is unlimited by default", () => {
|
|
89
|
+
delete process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_SESSION;
|
|
90
|
+
assert.equal(resolveMaxSubagentSpawnsPerSession(undefined), undefined);
|
|
91
|
+
assert.equal(resolveMaxSubagentSpawnsPerSession(-1), undefined);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("treats zero as an explicit unlimited override", () => {
|
|
95
|
+
process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_SESSION = "0";
|
|
96
|
+
assert.equal(resolveMaxSubagentSpawnsPerSession(7), undefined);
|
|
88
97
|
delete process.env.SELESAI_SUBAGENT_MAX_SPAWNS_PER_SESSION;
|
|
89
|
-
assert.equal(resolveMaxSubagentSpawnsPerSession(
|
|
90
|
-
assert.equal(resolveMaxSubagentSpawnsPerSession(-1), 40);
|
|
98
|
+
assert.equal(resolveMaxSubagentSpawnsPerSession(0), undefined);
|
|
91
99
|
});
|
|
92
100
|
});
|
|
93
101
|
|
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
|
-
import {
|
|
6
|
+
import { stopRequestPath } from "../../src/runs/background/control-channel.ts";
|
|
7
7
|
import {
|
|
8
8
|
SUBAGENT_RPC_PROTOCOL_VERSION,
|
|
9
9
|
SUBAGENT_RPC_READY_EVENT,
|
|
@@ -83,6 +83,14 @@ describe("subagent extension RPC bridge", () => {
|
|
|
83
83
|
assert.equal(reply.success, true);
|
|
84
84
|
assert.equal(reply.method, "ping");
|
|
85
85
|
assert.equal((reply as { data: { version?: number } }).data.version, SUBAGENT_RPC_PROTOCOL_VERSION);
|
|
86
|
+
assert.equal(
|
|
87
|
+
(reply as { data: { events?: { asyncComplete?: string } } }).data.events?.asyncComplete,
|
|
88
|
+
"subagent:async-complete",
|
|
89
|
+
);
|
|
90
|
+
assert.equal(
|
|
91
|
+
(reply as { data: { capabilities?: { nonRecoveringSteer?: boolean } } }).data.capabilities?.nonRecoveringSteer,
|
|
92
|
+
true,
|
|
93
|
+
);
|
|
86
94
|
|
|
87
95
|
bridge.dispose();
|
|
88
96
|
});
|
|
@@ -160,6 +168,31 @@ describe("subagent extension RPC bridge", () => {
|
|
|
160
168
|
bridge.dispose();
|
|
161
169
|
});
|
|
162
170
|
|
|
171
|
+
it("preserves schema-valid static parallel extension fields", async () => {
|
|
172
|
+
const events = new FakeEvents();
|
|
173
|
+
let executedParams: any;
|
|
174
|
+
const bridge = registerSubagentRpcBridge({
|
|
175
|
+
events,
|
|
176
|
+
getContext: () => ctx(),
|
|
177
|
+
execute: async (_id, params) => {
|
|
178
|
+
executedParams = params;
|
|
179
|
+
return {
|
|
180
|
+
content: [{ type: "text", text: "Async: worker [run-1]" }],
|
|
181
|
+
details: { mode: "chain", results: [], asyncId: "run-1", asyncDir: "/tmp/run-1" },
|
|
182
|
+
} as any;
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const reply = await request(events, "spawn-chain-extra", "spawn", {
|
|
187
|
+
chain: [{ parallel: [{ agent: "worker", extensionField: true }] }],
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
assert.equal(reply.success, true);
|
|
191
|
+
assert.equal(executedParams.chain[0].parallel[0].extensionField, true);
|
|
192
|
+
|
|
193
|
+
bridge.dispose();
|
|
194
|
+
});
|
|
195
|
+
|
|
163
196
|
it("rejects foreground or management spawn requests before executor dispatch", async () => {
|
|
164
197
|
const events = new FakeEvents();
|
|
165
198
|
let executeCalls = 0;
|
|
@@ -185,6 +218,86 @@ describe("subagent extension RPC bridge", () => {
|
|
|
185
218
|
bridge.dispose();
|
|
186
219
|
});
|
|
187
220
|
|
|
221
|
+
it("delegates acknowledged steering through the existing async action", async () => {
|
|
222
|
+
const events = new FakeEvents();
|
|
223
|
+
let executedParams: unknown;
|
|
224
|
+
const bridge = registerSubagentRpcBridge({
|
|
225
|
+
events,
|
|
226
|
+
getContext: () => ctx(),
|
|
227
|
+
execute: async (_id, params) => {
|
|
228
|
+
executedParams = params;
|
|
229
|
+
return {
|
|
230
|
+
content: [{ type: "text", text: "Steering delivered." }],
|
|
231
|
+
details: { mode: "management", results: [] },
|
|
232
|
+
} as any;
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const reply = await request(events, "steer-1", "steer", {
|
|
237
|
+
id: "abc123",
|
|
238
|
+
index: 0,
|
|
239
|
+
message: " Focus on the failing test. ",
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
assert.equal(reply.success, true);
|
|
243
|
+
assert.deepEqual(executedParams, {
|
|
244
|
+
action: "steer",
|
|
245
|
+
id: "abc123",
|
|
246
|
+
index: 0,
|
|
247
|
+
message: "Focus on the failing test.",
|
|
248
|
+
steeringRecovery: false,
|
|
249
|
+
});
|
|
250
|
+
assert.equal((reply as { data: { text?: string } }).data.text, "Steering delivered.");
|
|
251
|
+
|
|
252
|
+
bridge.dispose();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("rejects targetless RPC steering before executor dispatch", async () => {
|
|
256
|
+
const events = new FakeEvents();
|
|
257
|
+
let executeCalls = 0;
|
|
258
|
+
const bridge = registerSubagentRpcBridge({
|
|
259
|
+
events,
|
|
260
|
+
getContext: () => ctx(),
|
|
261
|
+
execute: async () => {
|
|
262
|
+
executeCalls++;
|
|
263
|
+
return { content: [], details: { mode: "management", results: [] } } as any;
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const reply = await request(events, "steer-no-target", "steer", {
|
|
268
|
+
message: "keep going",
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
assert.equal(reply.success, false);
|
|
272
|
+
assert.equal((reply as { error: { code: string } }).error.code, "invalid_params");
|
|
273
|
+
assert.match((reply as { error: { message: string } }).error.message, /requires id, runId, or dir/);
|
|
274
|
+
assert.equal(executeCalls, 0);
|
|
275
|
+
bridge.dispose();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("rejects empty RPC steering before executor dispatch", async () => {
|
|
279
|
+
const events = new FakeEvents();
|
|
280
|
+
let executeCalls = 0;
|
|
281
|
+
const bridge = registerSubagentRpcBridge({
|
|
282
|
+
events,
|
|
283
|
+
getContext: () => ctx(),
|
|
284
|
+
execute: async () => {
|
|
285
|
+
executeCalls++;
|
|
286
|
+
return { content: [], details: { mode: "management", results: [] } } as any;
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const reply = await request(events, "steer-empty", "steer", {
|
|
291
|
+
id: "abc123",
|
|
292
|
+
message: " ",
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
assert.equal(reply.success, false);
|
|
296
|
+
assert.equal((reply as { error: { code: string } }).error.code, "invalid_params");
|
|
297
|
+
assert.equal(executeCalls, 0);
|
|
298
|
+
bridge.dispose();
|
|
299
|
+
});
|
|
300
|
+
|
|
188
301
|
it("delegates interrupt through the existing executor action", async () => {
|
|
189
302
|
const events = new FakeEvents();
|
|
190
303
|
let executedParams: unknown;
|
|
@@ -205,7 +318,7 @@ describe("subagent extension RPC bridge", () => {
|
|
|
205
318
|
bridge.dispose();
|
|
206
319
|
});
|
|
207
320
|
|
|
208
|
-
it("uses the
|
|
321
|
+
it("uses the async stop control path for stop", async () => {
|
|
209
322
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-rpc-stop-"));
|
|
210
323
|
try {
|
|
211
324
|
const events = new FakeEvents();
|
|
@@ -238,7 +351,7 @@ describe("subagent extension RPC bridge", () => {
|
|
|
238
351
|
assert.equal(reply.success, true);
|
|
239
352
|
assert.equal((reply as { data: { runId?: string; state?: string } }).data.runId, "run-stop");
|
|
240
353
|
assert.equal((reply as { data: { state?: string } }).data.state, "stopping");
|
|
241
|
-
assert.equal(fs.existsSync(
|
|
354
|
+
assert.equal(fs.existsSync(stopRequestPath(asyncDir)), true);
|
|
242
355
|
|
|
243
356
|
bridge.dispose();
|
|
244
357
|
} finally {
|
|
@@ -283,7 +396,7 @@ describe("subagent extension RPC bridge", () => {
|
|
|
283
396
|
assert.equal(reply.success, false);
|
|
284
397
|
assert.equal((reply as { error: { code: string; message: string } }).error.code, "not_found");
|
|
285
398
|
assert.match((reply as { error: { message: string } }).error.message, /active session/);
|
|
286
|
-
assert.equal(fs.existsSync(
|
|
399
|
+
assert.equal(fs.existsSync(stopRequestPath(asyncDir)), false);
|
|
287
400
|
assert.equal(killCalls, 0);
|
|
288
401
|
|
|
289
402
|
bridge.dispose();
|
|
@@ -308,7 +308,7 @@ describe("async run status inspection", () => {
|
|
|
308
308
|
|
|
309
309
|
const text = textContent(result);
|
|
310
310
|
assert.equal(result.isError, undefined);
|
|
311
|
-
assert.match(text, /Subagent fleet: 2
|
|
311
|
+
assert.match(text, /Subagent fleet: 2 tracked/);
|
|
312
312
|
assert.match(text, /Foreground runs:/);
|
|
313
313
|
assert.match(text, /fg-run \| running \| scout/);
|
|
314
314
|
assert.match(text, /Async runs:/);
|
|
@@ -456,9 +456,8 @@ describe("async run status inspection", () => {
|
|
|
456
456
|
startedAt: 100,
|
|
457
457
|
lastUpdate: 200,
|
|
458
458
|
currentStep: 0,
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
steps: [{ agent: "worker", status: "running", startedAt: 100, steerCount: 2, lastSteerAt: 150 }],
|
|
459
|
+
steering: { requested: 2, scheduled: 1, pending: 1, delivered: 2, failed: 0, recovered: 1, lastRequestedAt: 150, lastDeliveredAt: 150, recent: [{ id: "request", requestedAt: 150, message: "guidance", targets: [{ index: 0, state: "recovered", recoveredAt: 180, lateDeliveredAt: 190 }] }] },
|
|
460
|
+
steps: [{ agent: "worker", status: "running", startedAt: 100, steering: { requested: 2, scheduled: 1, pending: 1, delivered: 2, failed: 0, recovered: 1, lastRequestedAt: 150, lastDeliveredAt: 150, recent: [{ id: "request", requestedAt: 150, message: "guidance", targets: [{ index: 0, state: "recovered", recoveredAt: 180, lateDeliveredAt: 190 }] }] } }],
|
|
462
461
|
}, null, 2), "utf-8");
|
|
463
462
|
|
|
464
463
|
const exact = inspectSubagentStatus({ id: "run-steered" }, {
|
|
@@ -469,8 +468,8 @@ describe("async run status inspection", () => {
|
|
|
469
468
|
});
|
|
470
469
|
const exactText = textContent(exact);
|
|
471
470
|
assert.equal(exact.isError, undefined);
|
|
472
|
-
assert.match(exactText, /Steering: 2
|
|
473
|
-
assert.match(exactText, /Step 1: worker running
|
|
471
|
+
assert.match(exactText, /Steering: 2 requested, 1 scheduled, 1 pending, 2 delivered, 0 failed, 1 recovered, 1 late acknowledged/);
|
|
472
|
+
assert.match(exactText, /Step 1: worker running/);
|
|
474
473
|
|
|
475
474
|
const list = inspectSubagentStatus({}, {
|
|
476
475
|
asyncDirRoot: asyncRoot,
|
|
@@ -480,7 +479,7 @@ describe("async run status inspection", () => {
|
|
|
480
479
|
});
|
|
481
480
|
const listText = textContent(list);
|
|
482
481
|
assert.equal(list.isError, undefined);
|
|
483
|
-
assert.match(listText, /2
|
|
482
|
+
assert.match(listText, /steering 1 scheduled, 1 pending, 2 delivered, 0 failed, 1 recovered/);
|
|
484
483
|
} finally {
|
|
485
484
|
fs.rmSync(root, { recursive: true, force: true });
|
|
486
485
|
}
|
|
@@ -961,6 +960,40 @@ describe("async run status inspection", () => {
|
|
|
961
960
|
}
|
|
962
961
|
});
|
|
963
962
|
|
|
963
|
+
it("shows stopped result-only runs without revive guidance", () => {
|
|
964
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-stopped-result-"));
|
|
965
|
+
try {
|
|
966
|
+
const asyncRoot = path.join(root, "runs");
|
|
967
|
+
const resultsDir = path.join(root, "results");
|
|
968
|
+
fs.mkdirSync(path.join(asyncRoot, "run-stopped-result"), { recursive: true });
|
|
969
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
970
|
+
const sessionFile = path.join(root, "session.jsonl");
|
|
971
|
+
fs.writeFileSync(sessionFile, "", "utf-8");
|
|
972
|
+
fs.writeFileSync(path.join(resultsDir, "run-stopped-result.json"), JSON.stringify({
|
|
973
|
+
id: "run-stopped-result",
|
|
974
|
+
agent: "worker",
|
|
975
|
+
success: false,
|
|
976
|
+
state: "stopped",
|
|
977
|
+
stopped: true,
|
|
978
|
+
sessionFile,
|
|
979
|
+
summary: "Subagent stopped by user.",
|
|
980
|
+
}, null, 2), "utf-8");
|
|
981
|
+
|
|
982
|
+
const result = inspectSubagentStatus({ id: "run-stopped-result" }, {
|
|
983
|
+
asyncDirRoot: asyncRoot,
|
|
984
|
+
resultsDir,
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
const text = textContent(result);
|
|
988
|
+
assert.equal(result.isError, undefined);
|
|
989
|
+
assert.match(text, /State: stopped/);
|
|
990
|
+
assert.match(text, /Resume: unavailable; stopped runs are not resumable/);
|
|
991
|
+
assert.doesNotMatch(text, /Revive:/);
|
|
992
|
+
} finally {
|
|
993
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
|
|
964
997
|
it("falls back to an existing result when async dir has no status file", () => {
|
|
965
998
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-result-fallback-"));
|
|
966
999
|
try {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
scheduledRunsEnabled,
|
|
15
15
|
} from "../../src/runs/background/scheduled-runs.ts";
|
|
16
16
|
import type { ExtensionConfig } from "../../src/shared/types.ts";
|
|
17
|
+
import { encodeSubagentCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling } from "../../src/api/capability-ceiling.ts";
|
|
17
18
|
import type { ExtensionContext } from "@selesai/code";
|
|
18
19
|
|
|
19
20
|
type TimerHandle = number;
|
|
@@ -78,7 +79,7 @@ async function flushMicrotasks(times = 10): Promise<void> {
|
|
|
78
79
|
for (let i = 0; i < times; i++) await Promise.resolve();
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
function createHarness(options: { config?: ExtensionConfig; storeRoot?: string; now?: number } = {}): TestHarness {
|
|
82
|
+
function createHarness(options: { config?: ExtensionConfig; storeRoot?: string; now?: number; resolveCapabilityCeiling?: (sessionId: string) => { version: 1; allowedTools?: string[]; denyExtensions: boolean; sources: string[] } | undefined } = {}): TestHarness {
|
|
82
83
|
const timers = new FakeTimers();
|
|
83
84
|
const launches: LaunchRecord[] = [];
|
|
84
85
|
const clock = { now: options.now ?? 1_000_000 };
|
|
@@ -89,6 +90,7 @@ function createHarness(options: { config?: ExtensionConfig; storeRoot?: string;
|
|
|
89
90
|
now: () => clock.now,
|
|
90
91
|
randomId: () => "id-" + randomUUID().slice(0, 4),
|
|
91
92
|
timers,
|
|
93
|
+
resolveCapabilityCeiling: options.resolveCapabilityCeiling,
|
|
92
94
|
launch: (params, ctx, signal) => {
|
|
93
95
|
let resolve!: LaunchRecord["resolve"];
|
|
94
96
|
const promise = new Promise<{ content: Array<{ type: "text"; text: string }>; details: Record<string, unknown>; isError?: boolean }>((res) => {
|
|
@@ -190,6 +192,30 @@ describe("ScheduledRunManager create/list/status/cancel", () => {
|
|
|
190
192
|
assert.equal(harness.timers.pendingCount(), 0);
|
|
191
193
|
});
|
|
192
194
|
|
|
195
|
+
it("rejects capability-ceiling-restricted schedules instead of persisting policy-less jobs", async () => {
|
|
196
|
+
const harness = createHarness({ resolveCapabilityCeiling: () => ({ version: 1, allowedTools: ["read"], denyExtensions: true, sources: ["plan"] }) });
|
|
197
|
+
const result = await harness.manager.handleToolCall({ action: "schedule", agent: "worker", task: "review", schedule: "+10m" }, harness.ctx);
|
|
198
|
+
assert.equal(isError(result), true);
|
|
199
|
+
assert.match(result.content[0]!.text, /does not yet persist ceilings/);
|
|
200
|
+
assert.equal(harness.launches.length, 0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
it("rejects schedules restricted only by inherited process policy", async () => {
|
|
205
|
+
const previous = process.env.SELESAI_SUBAGENT_CAPABILITY_CEILING_V1;
|
|
206
|
+
try {
|
|
207
|
+
process.env.SELESAI_SUBAGENT_CAPABILITY_CEILING_V1 = encodeSubagentCapabilityCeiling({ version: 1, allowedTools: ["read"], denyExtensions: true, sources: ["ancestor"] });
|
|
208
|
+
const harness = createHarness({ resolveCapabilityCeiling: resolveCurrentSubagentCapabilityCeiling });
|
|
209
|
+
const result = await harness.manager.handleToolCall({ action: "schedule", agent: "worker", task: "review", schedule: "+10m" }, harness.ctx);
|
|
210
|
+
assert.equal(isError(result), true);
|
|
211
|
+
assert.match(result.content[0]!.text, /does not yet persist ceilings/);
|
|
212
|
+
assert.equal(harness.launches.length, 0);
|
|
213
|
+
} finally {
|
|
214
|
+
if (previous === undefined) delete process.env.SELESAI_SUBAGENT_CAPABILITY_CEILING_V1;
|
|
215
|
+
else process.env.SELESAI_SUBAGENT_CAPABILITY_CEILING_V1 = previous;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
193
219
|
it("creates a scheduled job and arms a timer", async () => {
|
|
194
220
|
const harness = freshHarness();
|
|
195
221
|
const result = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "review later", schedule: "+10m", scheduleName: "nightly review" }, harness.ctx);
|
|
@@ -211,6 +237,27 @@ describe("ScheduledRunManager create/list/status/cancel", () => {
|
|
|
211
237
|
assert.match(none.content[0]!.text, /exactly one execution mode/);
|
|
212
238
|
});
|
|
213
239
|
|
|
240
|
+
it("rejects explicit reviewed acceptance at every scheduled nesting level before persisting a job", async () => {
|
|
241
|
+
const harness = freshHarness();
|
|
242
|
+
const cases = [
|
|
243
|
+
{ agent: "scout", task: "review later", acceptance: "reviewed" },
|
|
244
|
+
{ agent: "scout", task: "review later", acceptance: { level: "reviewed" } },
|
|
245
|
+
{ tasks: [{ agent: "scout", task: "review later", acceptance: "reviewed" }] },
|
|
246
|
+
{ chain: [{ agent: "scout", task: "review later", acceptance: { level: "reviewed" } }] },
|
|
247
|
+
{ chain: [{ parallel: [{ agent: "scout", task: "review later", acceptance: "reviewed" }] }] },
|
|
248
|
+
];
|
|
249
|
+
for (const params of cases) {
|
|
250
|
+
const result = await harness.manager.handleToolCall({
|
|
251
|
+
action: "schedule",
|
|
252
|
+
schedule: "+10m",
|
|
253
|
+
...params,
|
|
254
|
+
}, harness.ctx);
|
|
255
|
+
assert.equal(isError(result), true);
|
|
256
|
+
assert.match(result.content[0]!.text, /achieved status.*omit acceptance.*acceptance\.review\.required/i);
|
|
257
|
+
}
|
|
258
|
+
assert.equal(harness.timers.pendingCount(), 0);
|
|
259
|
+
});
|
|
260
|
+
|
|
214
261
|
it("requires a schedule and rejects fork/async-false/clarify-true", async () => {
|
|
215
262
|
const harness = freshHarness();
|
|
216
263
|
const noSchedule = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x" }, harness.ctx);
|
|
@@ -180,9 +180,10 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
|
|
|
180
180
|
assert.equal(actionSchema.type, "string");
|
|
181
181
|
assert.equal(actionSchema.enum, undefined);
|
|
182
182
|
const description = String(actionSchema.description ?? "");
|
|
183
|
-
assert.match(description, /
|
|
184
|
-
assert.match(description, /
|
|
185
|
-
assert.match(description,
|
|
183
|
+
assert.match(description, /Optional management\/control action/);
|
|
184
|
+
assert.match(description, /Omit this field entirely for execution\/delegation/);
|
|
185
|
+
assert.match(description, /\{agent, task\}, \{tasks\}, or \{chain\}/);
|
|
186
|
+
assert.match(description, /use it only for management\/control actions/);
|
|
186
187
|
assert.doesNotMatch(description, /orchestration\./);
|
|
187
188
|
});
|
|
188
189
|
|
|
@@ -240,6 +241,12 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
|
|
|
240
241
|
assert.equal(linesSchema.maximum, 500);
|
|
241
242
|
assert.match(String(linesSchema.description ?? ""), /transcript/i);
|
|
242
243
|
|
|
244
|
+
const additionalSchema = SubagentParams?.properties?.additional;
|
|
245
|
+
assert.ok(additionalSchema, "additional schema should exist");
|
|
246
|
+
assert.equal(additionalSchema.minimum, 1);
|
|
247
|
+
assert.match(String(additionalSchema.description ?? ""), /grant-spawn-budget/);
|
|
248
|
+
assert.match(String(additionalSchema.description ?? ""), /root interactive parent/i);
|
|
249
|
+
|
|
243
250
|
const controlSchema = SubagentParams?.properties?.control;
|
|
244
251
|
assert.ok(controlSchema, "control schema should exist");
|
|
245
252
|
assert.equal(controlSchema.properties?.needsAttentionAfterMs?.minimum, 1);
|
|
@@ -416,6 +423,14 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
|
|
|
416
423
|
assert.equal(acceptanceSchema.type, undefined);
|
|
417
424
|
assert.equal(hasAnyOfType(acceptanceSchema, "string"), true);
|
|
418
425
|
assert.equal(hasAnyOfType(acceptanceSchema, "boolean"), true);
|
|
426
|
+
const acceptanceStringBranches = anyOfBranches(acceptanceSchema).filter((branch) => branch.type === "string");
|
|
427
|
+
const acceptanceLevelBranch = acceptanceStringBranches.find((branch) => Array.isArray(branch.enum) && branch.enum.includes("auto"));
|
|
428
|
+
assert.deepEqual(acceptanceLevelBranch?.enum, ["auto", "attested", "checked", "verified"], "evidence levels end at verified");
|
|
429
|
+
const reviewedRecoveryBranch = acceptanceStringBranches.find((branch) => Array.isArray(branch.enum) && branch.enum.includes("reviewed"));
|
|
430
|
+
assert.deepEqual(reviewedRecoveryBranch?.enum, ["reviewed"]);
|
|
431
|
+
assert.equal(reviewedRecoveryBranch?.deprecated, true);
|
|
432
|
+
assert.match(String(acceptanceSchema.description ?? ""), /reviewer\/read-only calls, omit acceptance/i);
|
|
433
|
+
assert.match(String(acceptanceSchema.description ?? ""), /acceptance\.review\.required/);
|
|
419
434
|
const acceptanceObjectBranch = anyOfBranches(acceptanceSchema).find((branch) => branch.type === "object");
|
|
420
435
|
assert.ok(acceptanceObjectBranch, "acceptance should support object config");
|
|
421
436
|
assert.equal(acceptanceObjectBranch.additionalProperties, true);
|
|
@@ -499,6 +514,8 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
|
|
|
499
514
|
{ tasks: [{ agent: "worker", task: "Fix" }], maxRuntimeMs: 1000 },
|
|
500
515
|
{ chain: [{ agent: "worker", task: "Fix" }], timeoutMs: 1000, maxRuntimeMs: 1000 },
|
|
501
516
|
{ agent: "worker", task: "Fix", acceptance: "checked" },
|
|
517
|
+
{ agent: "worker", task: "Fix", acceptance: "reviewed" },
|
|
518
|
+
{ agent: "worker", task: "Fix", acceptance: { level: "none", reason: "parent will verify manually" } },
|
|
502
519
|
{ agent: "worker", task: "Fix", acceptance: { level: "checked", review: false } },
|
|
503
520
|
{ tasks: [{ agent: "worker", task: "Fix", acceptance: false }] },
|
|
504
521
|
{ chain: [{ agent: "worker", acceptance: { level: "checked" } }] },
|
|
@@ -517,6 +534,7 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
|
|
|
517
534
|
];
|
|
518
535
|
const invalidValues = [
|
|
519
536
|
{ skill: 123 },
|
|
537
|
+
{ agent: "worker", task: "Fix", acceptance: "none" },
|
|
520
538
|
{ skill: [123] },
|
|
521
539
|
{ output: 123 },
|
|
522
540
|
{ timeoutMs: 0 },
|