@selesai/code 0.5.19 → 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-intercom/broker/spawn.test.ts +31 -0
- package/dist/extensions/pi-intercom/broker/spawn.ts +5 -3
- 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 +2 -1
|
@@ -4,7 +4,10 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { beforeEach, describe, it } from "node:test";
|
|
6
6
|
|
|
7
|
+
import { scheduledRunStorePath } from "../../src/runs/background/scheduled-runs.ts";
|
|
8
|
+
import { SUBAGENT_FANOUT_CHILD_ENV } from "../../src/runs/shared/pi-args.ts";
|
|
7
9
|
import { ASYNC_DIR } from "../../src/shared/types.ts";
|
|
10
|
+
import type { WatchdogReviewFunction } from "../../src/watchdog/runtime.ts";
|
|
8
11
|
|
|
9
12
|
const SLASH_RESULT_TYPE = "subagent-slash-result";
|
|
10
13
|
const SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
|
|
@@ -51,13 +54,19 @@ interface SlashLiveStateModule {
|
|
|
51
54
|
resolveSlashMessageDetails?: typeof import("../../src/slash/slash-live-state.ts").resolveSlashMessageDetails;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
interface WatchdogRegisterModule {
|
|
58
|
+
registerMainWatchdog?: typeof import("../../src/watchdog/register-main.ts").registerMainWatchdog;
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
let registerSlashCommands: RegisterSlashCommandsModule["registerSlashCommands"];
|
|
62
|
+
let registerMainWatchdog: WatchdogRegisterModule["registerMainWatchdog"];
|
|
55
63
|
let clearSlashSnapshots: SlashLiveStateModule["clearSlashSnapshots"];
|
|
56
64
|
let getSlashRenderableSnapshot: SlashLiveStateModule["getSlashRenderableSnapshot"];
|
|
57
65
|
let resolveSlashMessageDetails: SlashLiveStateModule["resolveSlashMessageDetails"];
|
|
58
66
|
let available = true;
|
|
59
67
|
try {
|
|
60
68
|
({ registerSlashCommands } = await import("../../src/slash/slash-commands.ts") as RegisterSlashCommandsModule);
|
|
69
|
+
({ registerMainWatchdog } = await import("../../src/watchdog/register-main.ts") as WatchdogRegisterModule);
|
|
61
70
|
({ clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails } = await import("../../src/slash/slash-live-state.ts") as SlashLiveStateModule);
|
|
62
71
|
} catch {
|
|
63
72
|
available = false;
|
|
@@ -88,6 +97,9 @@ function createState(cwd: string) {
|
|
|
88
97
|
baseCwd: cwd,
|
|
89
98
|
currentSessionId: null,
|
|
90
99
|
asyncJobs: new Map(),
|
|
100
|
+
foregroundRuns: new Map(),
|
|
101
|
+
foregroundControls: new Map(),
|
|
102
|
+
lastForegroundControlId: null,
|
|
91
103
|
cleanupTimers: new Map(),
|
|
92
104
|
lastUiContext: null,
|
|
93
105
|
poller: null,
|
|
@@ -103,13 +115,17 @@ function createState(cwd: string) {
|
|
|
103
115
|
|
|
104
116
|
async function withIsolatedHome<T>(fn: () => Promise<T>): Promise<T> {
|
|
105
117
|
const home = fs.mkdtempSync(path.join(os.tmpdir(), "pi-slash-home-"));
|
|
118
|
+
const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
106
119
|
const previousHome = process.env.HOME;
|
|
107
120
|
const previousUserProfile = process.env.USERPROFILE;
|
|
121
|
+
process.env.SELESAI_CODING_AGENT_DIR = path.join(home, ".selesai", "agent");
|
|
108
122
|
process.env.HOME = home;
|
|
109
123
|
process.env.USERPROFILE = home;
|
|
110
124
|
try {
|
|
111
125
|
return await fn();
|
|
112
126
|
} finally {
|
|
127
|
+
if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
128
|
+
else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
|
|
113
129
|
if (previousHome === undefined) delete process.env.HOME;
|
|
114
130
|
else process.env.HOME = previousHome;
|
|
115
131
|
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
@@ -126,10 +142,19 @@ function createCommandContext(
|
|
|
126
142
|
custom: (...args: unknown[]) => Promise<unknown>;
|
|
127
143
|
notify: (message: string, type?: string) => void;
|
|
128
144
|
confirm: (title: string, message: string) => Promise<boolean>;
|
|
145
|
+
select: (title: string, choices: string[]) => Promise<string | undefined>;
|
|
146
|
+
editor: (title: string, prefill: string) => Promise<string | undefined>;
|
|
129
147
|
setStatus: (key: string, text: string | undefined) => void;
|
|
130
148
|
setToolsExpanded: (expanded: boolean) => void;
|
|
131
149
|
sessionManager: unknown;
|
|
132
|
-
modelRegistry: {
|
|
150
|
+
modelRegistry: {
|
|
151
|
+
refresh?: () => void;
|
|
152
|
+
getAvailable: () => Array<{ provider: string; id: string; reasoning?: boolean; thinkingLevelMap?: Record<string, string | null> }>;
|
|
153
|
+
find?: (provider: string, id: string) => unknown;
|
|
154
|
+
hasConfiguredAuth?: (model: unknown) => boolean;
|
|
155
|
+
};
|
|
156
|
+
model: { provider: string; id: string };
|
|
157
|
+
thinkingLevel: string;
|
|
133
158
|
}> = {},
|
|
134
159
|
) {
|
|
135
160
|
return {
|
|
@@ -138,12 +163,16 @@ function createCommandContext(
|
|
|
138
163
|
ui: {
|
|
139
164
|
notify: overrides.notify ?? ((_message: string) => {}),
|
|
140
165
|
confirm: overrides.confirm ?? (async () => false),
|
|
166
|
+
select: overrides.select ?? (async () => undefined),
|
|
167
|
+
editor: overrides.editor ?? (async () => undefined),
|
|
141
168
|
setStatus: overrides.setStatus ?? ((_key: string, _text: string | undefined) => {}),
|
|
142
169
|
setToolsExpanded: overrides.setToolsExpanded ?? ((_expanded: boolean) => {}),
|
|
143
170
|
onTerminalInput: () => () => {},
|
|
144
|
-
custom: overrides.custom
|
|
171
|
+
...(overrides.custom ? { custom: overrides.custom } : {}),
|
|
145
172
|
},
|
|
146
|
-
|
|
173
|
+
model: overrides.model,
|
|
174
|
+
thinkingLevel: overrides.thinkingLevel,
|
|
175
|
+
modelRegistry: overrides.modelRegistry ?? { getAvailable: () => [], find: () => undefined, hasConfiguredAuth: () => true },
|
|
147
176
|
sessionManager: overrides.sessionManager ?? {
|
|
148
177
|
getSessionFile: () => null,
|
|
149
178
|
getSessionId: () => "session-test",
|
|
@@ -166,6 +195,25 @@ function writeProjectChain(root: string, fileName: string, content: string): voi
|
|
|
166
195
|
fs.writeFileSync(path.join(root, ".selesai", "chains", fileName), content, "utf-8");
|
|
167
196
|
}
|
|
168
197
|
|
|
198
|
+
function createWatchdogHarness(review?: WatchdogReviewFunction) {
|
|
199
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
200
|
+
const renderers = new Map<string, (message: { content: string; details?: unknown }, options: { expanded: boolean }, theme: { fg(name: string, value: string): string; bold(value: string): string }) => { render(width: number): string[] } | undefined>();
|
|
201
|
+
const sent: unknown[] = [];
|
|
202
|
+
const pi = {
|
|
203
|
+
events: createEventBus(),
|
|
204
|
+
on() {},
|
|
205
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
206
|
+
registerShortcut() {},
|
|
207
|
+
registerMessageRenderer(type: string, renderer: (message: { content: string; details?: unknown }, options: { expanded: boolean }, theme: { fg(name: string, value: string): string; bold(value: string): string }) => { render(width: number): string[] } | undefined) {
|
|
208
|
+
renderers.set(type, renderer);
|
|
209
|
+
},
|
|
210
|
+
getThinkingLevel() { return "medium" as const; },
|
|
211
|
+
sendMessage(message: unknown) { sent.push(message); },
|
|
212
|
+
};
|
|
213
|
+
const runtime = registerMainWatchdog!(pi as never, review ? { review } : undefined);
|
|
214
|
+
return { commands, renderers, runtime, sent };
|
|
215
|
+
}
|
|
216
|
+
|
|
169
217
|
async function captureSlashCommandParams(
|
|
170
218
|
commandName: string,
|
|
171
219
|
args: string,
|
|
@@ -212,6 +260,219 @@ async function captureSlashCommandParams(
|
|
|
212
260
|
});
|
|
213
261
|
}
|
|
214
262
|
|
|
263
|
+
describe("subagents watchdog slash command", { skip: !available ? "watchdog command not importable" : undefined }, () => {
|
|
264
|
+
it("shows default-off status with runtime state, sources, and review seam", async () => {
|
|
265
|
+
await withIsolatedHome(async () => {
|
|
266
|
+
await withTempProject("pi-watchdog-status-", async (root) => {
|
|
267
|
+
const { commands, sent } = createWatchdogHarness();
|
|
268
|
+
await commands.get("subagents-watchdog")!.handler("", createCommandContext({ cwd: root }));
|
|
269
|
+
|
|
270
|
+
const content = String((sent[0] as { content?: unknown }).content ?? "");
|
|
271
|
+
assert.match(content, /Subagent watchdog/);
|
|
272
|
+
assert.match(content, /Main: off \(default off\)/);
|
|
273
|
+
assert.match(content, /Runtime: idle/);
|
|
274
|
+
assert.match(content, /Review model call: real model review/);
|
|
275
|
+
assert.match(content, /Sources:/);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("recommends and saves a strong complementary watchdog model", async () => {
|
|
281
|
+
await withIsolatedHome(async () => {
|
|
282
|
+
await withTempProject("pi-watchdog-model-", async (root) => {
|
|
283
|
+
const gpt = { provider: "openai-codex", id: "gpt-5.5", reasoning: true };
|
|
284
|
+
const opus = { provider: "anthropic", id: "claude-opus-4-8", reasoning: true };
|
|
285
|
+
const models = [gpt, opus];
|
|
286
|
+
const modelRegistry = {
|
|
287
|
+
getAvailable: () => models,
|
|
288
|
+
find: (provider: string, id: string) => models.find((entry) => entry.provider === provider && entry.id === id),
|
|
289
|
+
hasConfiguredAuth: (model: unknown) => Boolean(model),
|
|
290
|
+
};
|
|
291
|
+
const ctx = createCommandContext({ cwd: root, model: gpt, modelRegistry });
|
|
292
|
+
const { commands, sent } = createWatchdogHarness();
|
|
293
|
+
|
|
294
|
+
await commands.get("subagents-watchdog")!.handler("recommend-model", ctx);
|
|
295
|
+
await commands.get("subagents-watchdog")!.handler("model recommended", ctx);
|
|
296
|
+
|
|
297
|
+
const recommendation = String((sent[0] as { content?: unknown }).content ?? "");
|
|
298
|
+
assert.match(recommendation, /Recommended: anthropic\/claude-opus-4-8:high/);
|
|
299
|
+
const settingsPath = path.join(process.env.HOME!, ".selesai", "agent", "settings.json");
|
|
300
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
301
|
+
assert.equal(settings.subagents.watchdog.main.model, "anthropic/claude-opus-4-8");
|
|
302
|
+
assert.equal(settings.subagents.watchdog.main.thinking, "high");
|
|
303
|
+
assert.equal(settings.subagents.watchdog.enabled, undefined);
|
|
304
|
+
assert.match(String((sent[1] as { content?: unknown }).content ?? ""), /Run \/subagents-watchdog on if the watchdog is still off/);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("supports session-scoped recommended watchdog models without writing settings", async () => {
|
|
310
|
+
await withIsolatedHome(async () => {
|
|
311
|
+
await withTempProject("pi-watchdog-session-model-", async (root) => {
|
|
312
|
+
const opus = { provider: "anthropic", id: "claude-opus-4-8", reasoning: true };
|
|
313
|
+
const gpt = { provider: "openai-codex", id: "gpt-5.5", reasoning: true };
|
|
314
|
+
const models = [opus, gpt];
|
|
315
|
+
const modelRegistry = {
|
|
316
|
+
getAvailable: () => models,
|
|
317
|
+
find: (provider: string, id: string) => models.find((entry) => entry.provider === provider && entry.id === id),
|
|
318
|
+
hasConfiguredAuth: (model: unknown) => Boolean(model),
|
|
319
|
+
};
|
|
320
|
+
const { commands, sent } = createWatchdogHarness();
|
|
321
|
+
|
|
322
|
+
await commands.get("subagents-watchdog")!.handler("session model recommended", createCommandContext({ cwd: root, model: opus, modelRegistry }));
|
|
323
|
+
|
|
324
|
+
assert.equal(fs.existsSync(path.join(process.env.HOME!, ".selesai", "agent", "settings.json")), false);
|
|
325
|
+
const content = String((sent[0] as { content?: unknown }).content ?? "");
|
|
326
|
+
assert.match(content, /session model: openai-codex\/gpt-5\.5:high/);
|
|
327
|
+
assert.match(content, /Main model: openai-codex\/gpt-5\.5 \(session override\)/);
|
|
328
|
+
assert.match(content, /Main thinking: high/);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("shows explicit watchdog model thinking accurately when no thinking is configured", async () => {
|
|
334
|
+
await withIsolatedHome(async () => {
|
|
335
|
+
await withTempProject("pi-watchdog-status-model-", async (root) => {
|
|
336
|
+
const settingsPath = path.join(process.env.HOME!, ".selesai", "agent", "settings.json");
|
|
337
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
338
|
+
fs.writeFileSync(settingsPath, JSON.stringify({ subagents: { watchdog: { enabled: true, main: { model: "openai-codex/gpt-5.5" } } } }, null, 2), "utf-8");
|
|
339
|
+
const gpt = { provider: "openai-codex", id: "gpt-5.5", reasoning: true };
|
|
340
|
+
const opus = { provider: "anthropic", id: "claude-opus-4-8", reasoning: true };
|
|
341
|
+
const models = [gpt, opus];
|
|
342
|
+
const modelRegistry = {
|
|
343
|
+
getAvailable: () => models,
|
|
344
|
+
find: (provider: string, id: string) => models.find((entry) => entry.provider === provider && entry.id === id),
|
|
345
|
+
hasConfiguredAuth: (model: unknown) => Boolean(model),
|
|
346
|
+
};
|
|
347
|
+
const { commands, sent } = createWatchdogHarness();
|
|
348
|
+
|
|
349
|
+
await commands.get("subagents-watchdog")!.handler("status", createCommandContext({ cwd: root, model: gpt, modelRegistry }));
|
|
350
|
+
|
|
351
|
+
const content = String((sent[0] as { content?: unknown }).content ?? "");
|
|
352
|
+
assert.match(content, /Main model: openai-codex\/gpt-5\.5 \(configured\)/);
|
|
353
|
+
assert.match(content, /Main thinking: off \(default for explicit watchdog model\)/);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("writes only user watchdog enabled settings and preserves existing settings", async () => {
|
|
359
|
+
await withIsolatedHome(async () => {
|
|
360
|
+
await withTempProject("pi-watchdog-toggle-", async (root) => {
|
|
361
|
+
const settingsPath = path.join(process.env.HOME!, ".selesai", "agent", "settings.json");
|
|
362
|
+
const projectSettingsPath = path.join(root, ".selesai", "settings.json");
|
|
363
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
364
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
365
|
+
other: true,
|
|
366
|
+
subagents: {
|
|
367
|
+
agentOverrides: { scout: { model: "openai/test" } },
|
|
368
|
+
watchdog: { agentEndTimeoutMs: 1234, main: { enabled: false, model: "openai/watchdog" } },
|
|
369
|
+
},
|
|
370
|
+
}, null, 2), "utf-8");
|
|
371
|
+
fs.writeFileSync(projectSettingsPath, JSON.stringify({ subagents: { defaultModel: "anthropic/project" } }, null, 2), "utf-8");
|
|
372
|
+
const projectBefore = fs.readFileSync(projectSettingsPath, "utf-8");
|
|
373
|
+
const { commands, sent } = createWatchdogHarness();
|
|
374
|
+
|
|
375
|
+
await commands.get("subagents-watchdog")!.handler("on", createCommandContext({ cwd: root }));
|
|
376
|
+
let settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
377
|
+
assert.equal(settings.other, true);
|
|
378
|
+
assert.equal(settings.subagents.agentOverrides.scout.model, "openai/test");
|
|
379
|
+
assert.equal(settings.subagents.watchdog.agentEndTimeoutMs, 1234);
|
|
380
|
+
assert.equal(settings.subagents.watchdog.enabled, true);
|
|
381
|
+
assert.equal(settings.subagents.watchdog.main.enabled, true);
|
|
382
|
+
assert.equal(settings.subagents.watchdog.main.model, "openai/watchdog");
|
|
383
|
+
assert.equal(fs.readFileSync(projectSettingsPath, "utf-8"), projectBefore);
|
|
384
|
+
|
|
385
|
+
await commands.get("subagents-watchdog")!.handler("off", createCommandContext({ cwd: root }));
|
|
386
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
387
|
+
assert.equal(settings.subagents.watchdog.enabled, false);
|
|
388
|
+
assert.equal(settings.subagents.watchdog.main.enabled, false);
|
|
389
|
+
assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /saved to user settings/);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("uses session on/off overrides without writing settings files", async () => {
|
|
395
|
+
await withIsolatedHome(async () => {
|
|
396
|
+
await withTempProject("pi-watchdog-session-", async (root) => {
|
|
397
|
+
const settingsPath = path.join(process.env.HOME!, ".selesai", "agent", "settings.json");
|
|
398
|
+
const projectSettingsPath = path.join(root, ".selesai", "settings.json");
|
|
399
|
+
const { commands, sent } = createWatchdogHarness();
|
|
400
|
+
|
|
401
|
+
await commands.get("subagents-watchdog")!.handler("session on", createCommandContext({ cwd: root }));
|
|
402
|
+
await commands.get("subagents-watchdog")!.handler("session off", createCommandContext({ cwd: root }));
|
|
403
|
+
|
|
404
|
+
assert.equal(fs.existsSync(settingsPath), false);
|
|
405
|
+
assert.equal(fs.existsSync(projectSettingsPath), false);
|
|
406
|
+
assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /session override: on/i);
|
|
407
|
+
assert.match(String((sent[1] as { content?: unknown }).content ?? ""), /session override: off/i);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("sends deterministic concern and blocker warning messages through the renderer path", async () => {
|
|
413
|
+
await withIsolatedHome(async () => {
|
|
414
|
+
const { commands, renderers, sent } = createWatchdogHarness();
|
|
415
|
+
await commands.get("subagents-watchdog")!.handler("test concern check the concern", createCommandContext());
|
|
416
|
+
await commands.get("subagents-watchdog")!.handler("test blocker check the blocker", createCommandContext());
|
|
417
|
+
|
|
418
|
+
const concern = sent[0] as { customType?: string; content?: string; display?: boolean; details?: Record<string, unknown> };
|
|
419
|
+
const blocker = sent[1] as { customType?: string; content?: string; display?: boolean; details?: Record<string, unknown> };
|
|
420
|
+
assert.equal(concern.customType, "subagent_watchdog_warning");
|
|
421
|
+
assert.equal(concern.display, true);
|
|
422
|
+
assert.equal(concern.details?.severity, "concern");
|
|
423
|
+
assert.equal(concern.details?.source, "main");
|
|
424
|
+
assert.equal(concern.details?.state, "displayed");
|
|
425
|
+
assert.match(concern.content ?? "", /source="main"/);
|
|
426
|
+
assert.match(concern.content ?? "", /<state>displayed<\/state>/);
|
|
427
|
+
assert.match(concern.content ?? "", /<recommended_action>/);
|
|
428
|
+
assert.equal(blocker.details?.severity, "blocker");
|
|
429
|
+
assert.match(blocker.content ?? "", /<blocker_guidance>/);
|
|
430
|
+
|
|
431
|
+
const renderer = renderers.get("subagent_watchdog_warning")!;
|
|
432
|
+
const rendered = renderer(blocker as never, { expanded: true }, { fg: (_name, value) => value, bold: (value) => value })!.render(100).join("\n");
|
|
433
|
+
assert.match(rendered, /Subagent watchdog Blocker \(displayed\): check the blocker/);
|
|
434
|
+
assert.match(rendered, /Manual \/subagents-watchdog test blocker message/);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("sends accepted review warnings as visible custom watchdog messages", async () => {
|
|
439
|
+
await withIsolatedHome(async () => {
|
|
440
|
+
await withTempProject("pi-watchdog-review-warning-", async (root) => {
|
|
441
|
+
const review: WatchdogReviewFunction = (request) => {
|
|
442
|
+
assert.equal(request.emitWarning({
|
|
443
|
+
severity: "concern",
|
|
444
|
+
category: "test-gap",
|
|
445
|
+
confidence: "high",
|
|
446
|
+
source: "main",
|
|
447
|
+
summary: "Focused validation is missing",
|
|
448
|
+
evidence: "The reviewed turn delta says changes were made but contains no test command.",
|
|
449
|
+
recommendedAction: "Run the focused watchdog tests before accepting the turn.",
|
|
450
|
+
}), true);
|
|
451
|
+
return { stopReason: "stop" };
|
|
452
|
+
};
|
|
453
|
+
const { runtime, sent } = createWatchdogHarness(review);
|
|
454
|
+
|
|
455
|
+
runtime.setSessionEnabled(true, root);
|
|
456
|
+
runtime.handleBeforeAgentStart({ prompt: "Patch watchdog runtime." }, { cwd: root });
|
|
457
|
+
runtime.handleTurnEnd({
|
|
458
|
+
type: "turn_end",
|
|
459
|
+
message: { role: "assistant", content: "Changed watchdog runtime without running tests." },
|
|
460
|
+
toolResults: [{ role: "toolResult", toolName: "edit", content: "Edited src/watchdog/runtime.ts", isError: false }],
|
|
461
|
+
}, { cwd: root });
|
|
462
|
+
await runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: root });
|
|
463
|
+
|
|
464
|
+
const message = sent[0] as { customType?: string; content?: string; display?: boolean; details?: Record<string, unknown> };
|
|
465
|
+
assert.equal(message.customType, "subagent_watchdog_warning");
|
|
466
|
+
assert.equal(message.display, true);
|
|
467
|
+
assert.equal(message.details?.state, "displayed");
|
|
468
|
+
assert.equal(message.details?.summary, "Focused validation is missing");
|
|
469
|
+
assert.match(message.content ?? "", /<subagent_watchdog/);
|
|
470
|
+
assert.match(message.content ?? "", /<recommended_action>/);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
215
476
|
describe("slash command custom message delivery", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
|
|
216
477
|
beforeEach(() => {
|
|
217
478
|
clearSlashSnapshots?.();
|
|
@@ -260,6 +521,7 @@ describe("slash command custom message delivery", { skip: !available ? "slash-co
|
|
|
260
521
|
const ctx = createCommandContext({ sessionManager });
|
|
261
522
|
registerSlashCommands!(pi, createState(process.cwd()));
|
|
262
523
|
await commands.get("run")!.handler("scout", ctx);
|
|
524
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
263
525
|
|
|
264
526
|
assert.deepEqual(requestedParams, { agent: "scout", task: "", clarify: false, agentScope: "both" });
|
|
265
527
|
assert.equal(requestedCtx, ctx);
|
|
@@ -309,6 +571,7 @@ describe("slash command custom message delivery", { skip: !available ? "slash-co
|
|
|
309
571
|
log.push(`status:${text ?? "clear"}`);
|
|
310
572
|
},
|
|
311
573
|
}));
|
|
574
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
312
575
|
|
|
313
576
|
assert.equal(sent.length, 2);
|
|
314
577
|
assert.equal((sent[0] as { customType?: string; display?: boolean }).customType, SLASH_RESULT_TYPE);
|
|
@@ -398,6 +661,7 @@ describe("slash command custom message delivery", { skip: !available ? "slash-co
|
|
|
398
661
|
log.push(`status:${text ?? "clear"}`);
|
|
399
662
|
},
|
|
400
663
|
}));
|
|
664
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
401
665
|
|
|
402
666
|
assert.equal(sent.length, 2);
|
|
403
667
|
assert.equal((sent[0] as { customType?: string; display?: boolean }).customType, SLASH_RESULT_TYPE);
|
|
@@ -484,6 +748,7 @@ describe("slash command custom message delivery", { skip: !available ? "slash-co
|
|
|
484
748
|
registerSlashCommands!(pi, createState(process.cwd()));
|
|
485
749
|
const args = Array.from({ length: 9 }, (_, index) => `scout \"task ${index + 1}\"`).join(" -> ");
|
|
486
750
|
await commands.get("parallel")!.handler(args, createCommandContext());
|
|
751
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
487
752
|
|
|
488
753
|
assert.equal(requestedTasks, 9);
|
|
489
754
|
assert.equal(sent.length, 2);
|
|
@@ -592,6 +857,47 @@ Review {previous}
|
|
|
592
857
|
});
|
|
593
858
|
});
|
|
594
859
|
|
|
860
|
+
it("/run-chain returns while its foreground subagent is still running", async () => {
|
|
861
|
+
await withTempProject("pi-run-chain-nonblocking-", async (root) => {
|
|
862
|
+
writeProjectChain(root, "slow.chain.md", `---
|
|
863
|
+
name: slow
|
|
864
|
+
description: Slow chain
|
|
865
|
+
---
|
|
866
|
+
|
|
867
|
+
## scout
|
|
868
|
+
|
|
869
|
+
Think for a long time about {task}
|
|
870
|
+
`);
|
|
871
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
872
|
+
const events = createEventBus();
|
|
873
|
+
let requestId: string | undefined;
|
|
874
|
+
events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
|
|
875
|
+
const payload = data as { requestId: string };
|
|
876
|
+
requestId = payload.requestId;
|
|
877
|
+
events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId });
|
|
878
|
+
});
|
|
879
|
+
const pi = {
|
|
880
|
+
events,
|
|
881
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
882
|
+
registerShortcut() {},
|
|
883
|
+
sendMessage(_message: unknown) {},
|
|
884
|
+
};
|
|
885
|
+
registerSlashCommands!(pi, createState(root));
|
|
886
|
+
|
|
887
|
+
const returned = await Promise.race([
|
|
888
|
+
commands.get("run-chain")!.handler("slow -- test UI responsiveness", createCommandContext({ cwd: root })).then(() => true),
|
|
889
|
+
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 25)),
|
|
890
|
+
]);
|
|
891
|
+
assert.equal(returned, true, "the slash command should not hold the UI while the child runs");
|
|
892
|
+
assert.ok(requestId);
|
|
893
|
+
events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
|
|
894
|
+
requestId,
|
|
895
|
+
result: { content: [{ type: "text", text: "done" }], details: { mode: "chain", results: [] } },
|
|
896
|
+
isError: false,
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
|
|
595
901
|
it("/run-chain launches a saved JSON chain with dynamic fanout", async () => {
|
|
596
902
|
await withTempProject("pi-run-chain-json-dynamic-", async (root) => {
|
|
597
903
|
writeProjectChain(root, "dynamic-review.chain.json", JSON.stringify({
|
|
@@ -1249,6 +1555,463 @@ describe("subagent profiles slash commands", { skip: !available ? "slash-command
|
|
|
1249
1555
|
});
|
|
1250
1556
|
});
|
|
1251
1557
|
|
|
1558
|
+
describe("subagents admin slash command", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
|
|
1559
|
+
beforeEach(() => {
|
|
1560
|
+
clearSlashSnapshots?.();
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
function registerAdmin(sent: unknown[]) {
|
|
1564
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
1565
|
+
const pi = {
|
|
1566
|
+
events: createEventBus(),
|
|
1567
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
1568
|
+
registerShortcut() {},
|
|
1569
|
+
sendMessage(message: unknown) { sent.push(message); },
|
|
1570
|
+
};
|
|
1571
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
1572
|
+
return commands;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
it("registers /subagents", async () => {
|
|
1576
|
+
await withIsolatedHome(async () => {
|
|
1577
|
+
const commands = registerAdmin([]);
|
|
1578
|
+
assert.equal(commands.has("subagents"), true);
|
|
1579
|
+
});
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
it("treats agent and scope selector cancellation as a silent no-op", async () => {
|
|
1583
|
+
await withIsolatedHome(async () => {
|
|
1584
|
+
await withTempProject("pi-subagents-admin-cancel-", async (root) => {
|
|
1585
|
+
const sent: unknown[] = [];
|
|
1586
|
+
const notifications: string[] = [];
|
|
1587
|
+
const commands = registerAdmin(sent);
|
|
1588
|
+
|
|
1589
|
+
await commands.get("subagents")!.handler("", createCommandContext({
|
|
1590
|
+
cwd: root,
|
|
1591
|
+
hasUI: true,
|
|
1592
|
+
select: async () => undefined,
|
|
1593
|
+
notify: (message) => notifications.push(message),
|
|
1594
|
+
}));
|
|
1595
|
+
|
|
1596
|
+
await commands.get("subagents")!.handler("worker model", createCommandContext({
|
|
1597
|
+
cwd: root,
|
|
1598
|
+
hasUI: true,
|
|
1599
|
+
modelRegistry: { getAvailable: () => [{ provider: "test", id: "new-model", reasoning: true }] },
|
|
1600
|
+
select: async (title) => title.startsWith("Select model") ? "test/new-model" : undefined,
|
|
1601
|
+
notify: (message) => notifications.push(message),
|
|
1602
|
+
}));
|
|
1603
|
+
|
|
1604
|
+
assert.deepEqual(sent, []);
|
|
1605
|
+
assert.deepEqual(notifications, []);
|
|
1606
|
+
assert.equal(fs.existsSync(path.join(root, ".selesai", "settings.json")), false);
|
|
1607
|
+
});
|
|
1608
|
+
});
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
it("reports duplicate names as ambiguous without UI", async () => {
|
|
1612
|
+
await withIsolatedHome(async () => {
|
|
1613
|
+
await withTempProject("pi-subagents-admin-ambiguous-", async (root) => {
|
|
1614
|
+
fs.writeFileSync(path.join(root, ".selesai", "agents", "worker.md"), "---\nname: worker\ndescription: Project worker\n---\n\nWork.\n", "utf-8");
|
|
1615
|
+
const sent: unknown[] = [];
|
|
1616
|
+
const commands = registerAdmin(sent);
|
|
1617
|
+
await commands.get("subagents")!.handler("worker", createCommandContext({ cwd: root }));
|
|
1618
|
+
assert.equal(sent.length, 1);
|
|
1619
|
+
const content = (sent[0] as { content?: string }).content ?? "";
|
|
1620
|
+
assert.match(content, /Subagent 'worker' is ambiguous/);
|
|
1621
|
+
assert.match(content, /project:/);
|
|
1622
|
+
assert.match(content, /builtin:/);
|
|
1623
|
+
});
|
|
1624
|
+
});
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
it("keeps same-source duplicate agents distinct in interactive pickers", async () => {
|
|
1628
|
+
await withIsolatedHome(async () => {
|
|
1629
|
+
await withTempProject("pi-subagents-admin-same-source-", async (root) => {
|
|
1630
|
+
const localPath = path.join(process.env.SELESAI_CODING_AGENT_DIR!, "agents", "duplicate-admin-agent.md");
|
|
1631
|
+
const extraDir = path.join(root, "extra-agents");
|
|
1632
|
+
const extraPath = path.join(extraDir, "duplicate-admin-agent.md");
|
|
1633
|
+
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
1634
|
+
fs.mkdirSync(extraDir, { recursive: true });
|
|
1635
|
+
const frontmatter = "---\nname: duplicate-admin-agent\ndescription: Duplicate agent\nmodel: test/model\n---\n\n";
|
|
1636
|
+
fs.writeFileSync(localPath, `${frontmatter}Local prompt.\n`, "utf-8");
|
|
1637
|
+
fs.writeFileSync(extraPath, `${frontmatter}Extra prompt.\n`, "utf-8");
|
|
1638
|
+
const previousExtraDirs = process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
|
|
1639
|
+
process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS = extraDir;
|
|
1640
|
+
try {
|
|
1641
|
+
const sent: unknown[] = [];
|
|
1642
|
+
const commands = registerAdmin(sent);
|
|
1643
|
+
await commands.get("subagents")!.handler("duplicate-admin-agent details", createCommandContext({
|
|
1644
|
+
cwd: root,
|
|
1645
|
+
hasUI: true,
|
|
1646
|
+
select: async (_title, choices) => {
|
|
1647
|
+
assert.equal(choices.length, 2);
|
|
1648
|
+
assert.ok(choices.some((choice) => choice.includes(localPath)));
|
|
1649
|
+
assert.ok(choices.some((choice) => choice.includes(extraPath)));
|
|
1650
|
+
return choices.find((choice) => choice.includes(localPath));
|
|
1651
|
+
},
|
|
1652
|
+
}));
|
|
1653
|
+
const content = (sent[0] as { content?: string }).content ?? "";
|
|
1654
|
+
assert.ok(content.split("\n").includes(`Path: ${localPath}`));
|
|
1655
|
+
} finally {
|
|
1656
|
+
if (previousExtraDirs === undefined) delete process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
|
|
1657
|
+
else process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS = previousExtraDirs;
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
});
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
it("updates a project agent model without dumping metadata first", async () => {
|
|
1664
|
+
await withIsolatedHome(async () => {
|
|
1665
|
+
await withTempProject("pi-subagents-admin-model-", async (root) => {
|
|
1666
|
+
const agentPath = path.join(root, ".selesai", "agents", "worker.md");
|
|
1667
|
+
fs.writeFileSync(agentPath, `---\nname: worker\ndescription: Test worker\nmodel: anthropic/claude-sonnet-4-6\n---\n\nDo work.\n`, "utf-8");
|
|
1668
|
+
const sent: unknown[] = [];
|
|
1669
|
+
const commands = registerAdmin(sent);
|
|
1670
|
+
let refreshes = 0;
|
|
1671
|
+
await commands.get("subagents")!.handler("worker model", createCommandContext({
|
|
1672
|
+
cwd: root,
|
|
1673
|
+
hasUI: true,
|
|
1674
|
+
modelRegistry: {
|
|
1675
|
+
refresh: () => { refreshes++; },
|
|
1676
|
+
getAvailable: () => [{ provider: "bluebox-azure-openai", id: "gpt-5_6-sol", reasoning: true }],
|
|
1677
|
+
},
|
|
1678
|
+
select: async (_title, choices) => {
|
|
1679
|
+
const projectAgent = choices.find((choice) => choice.startsWith("worker [project]"));
|
|
1680
|
+
if (projectAgent) return projectAgent;
|
|
1681
|
+
assert.ok(choices.includes("bluebox-azure-openai/gpt-5_6-sol"));
|
|
1682
|
+
return "bluebox-azure-openai/gpt-5_6-sol";
|
|
1683
|
+
},
|
|
1684
|
+
}));
|
|
1685
|
+
|
|
1686
|
+
assert.equal(refreshes, 1);
|
|
1687
|
+
assert.match(fs.readFileSync(agentPath, "utf-8"), /^model: bluebox-azure-openai\/gpt-5_6-sol$/m);
|
|
1688
|
+
assert.equal(sent.length, 1, "only the compact confirmation should enter the conversation");
|
|
1689
|
+
assert.match((sent[0] as { content?: string }).content ?? "", /Updated 'worker' model/);
|
|
1690
|
+
});
|
|
1691
|
+
});
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
it("updates a profile-managed user agent through settings instead of pinning frontmatter", async () => {
|
|
1695
|
+
await withIsolatedHome(async () => {
|
|
1696
|
+
await withTempProject("pi-subagents-admin-profile-", async (root) => {
|
|
1697
|
+
const userAgentDir = path.join(process.env.HOME!, ".selesai", "agent", "agents");
|
|
1698
|
+
fs.mkdirSync(userAgentDir, { recursive: true });
|
|
1699
|
+
const agentPath = path.join(userAgentDir, "worker.md");
|
|
1700
|
+
fs.writeFileSync(agentPath, `---\nname: worker\ndescription: Profile worker\n---\n\nDo work.\n`, "utf-8");
|
|
1701
|
+
const settingsPath = path.join(process.env.HOME!, ".selesai", "agent", "settings.json");
|
|
1702
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
1703
|
+
subagents: { disableBuiltins: true, agentOverrides: { worker: { model: "anthropic/claude-opus-4-8" } } },
|
|
1704
|
+
}, null, 2));
|
|
1705
|
+
|
|
1706
|
+
const sent: unknown[] = [];
|
|
1707
|
+
const commands = registerAdmin(sent);
|
|
1708
|
+
await commands.get("subagents")!.handler("worker model", createCommandContext({
|
|
1709
|
+
cwd: root,
|
|
1710
|
+
hasUI: true,
|
|
1711
|
+
modelRegistry: { getAvailable: () => [{ provider: "bluebox-azure-openai", id: "gpt-5_6-luna", reasoning: true }] },
|
|
1712
|
+
select: async (_title, choices) =>
|
|
1713
|
+
choices.find((choice) => choice.startsWith("worker [user]"))
|
|
1714
|
+
?? "bluebox-azure-openai/gpt-5_6-luna",
|
|
1715
|
+
}));
|
|
1716
|
+
|
|
1717
|
+
assert.doesNotMatch(fs.readFileSync(agentPath, "utf-8"), /^model:/m);
|
|
1718
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
1719
|
+
assert.equal(settings.subagents.disableBuiltins, true);
|
|
1720
|
+
assert.equal(settings.subagents.agentOverrides.worker.model, "bluebox-azure-openai/gpt-5_6-luna");
|
|
1721
|
+
});
|
|
1722
|
+
});
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
it("does not snapshot global disableThinking into an unrelated builtin override", async () => {
|
|
1726
|
+
await withIsolatedHome(async () => {
|
|
1727
|
+
await withTempProject("pi-subagents-admin-global-thinking-", async (root) => {
|
|
1728
|
+
const settingsPath = path.join(process.env.SELESAI_CODING_AGENT_DIR!, "settings.json");
|
|
1729
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
1730
|
+
fs.writeFileSync(settingsPath, JSON.stringify({ subagents: { disableThinking: true } }, null, 2), "utf-8");
|
|
1731
|
+
const commands = registerAdmin([]);
|
|
1732
|
+
await commands.get("subagents")!.handler("worker model", createCommandContext({
|
|
1733
|
+
cwd: root,
|
|
1734
|
+
hasUI: true,
|
|
1735
|
+
modelRegistry: { getAvailable: () => [{ provider: "test", id: "new-model", reasoning: true }] },
|
|
1736
|
+
select: async () => "test/new-model",
|
|
1737
|
+
}));
|
|
1738
|
+
|
|
1739
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(settingsPath, "utf-8")).subagents, {
|
|
1740
|
+
disableThinking: true,
|
|
1741
|
+
agentOverrides: { worker: { model: "test/new-model" } },
|
|
1742
|
+
});
|
|
1743
|
+
});
|
|
1744
|
+
});
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
it("offers and saves max thinking only when model metadata declares it", async () => {
|
|
1748
|
+
await withIsolatedHome(async () => {
|
|
1749
|
+
await withTempProject("pi-subagents-admin-thinking-", async (root) => {
|
|
1750
|
+
const settingsPath = path.join(process.env.SELESAI_CODING_AGENT_DIR!, "settings.json");
|
|
1751
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
1752
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
1753
|
+
subagents: { agentOverrides: { worker: { model: "anthropic/claude-opus-4-8", thinking: "high" } } },
|
|
1754
|
+
}, null, 2));
|
|
1755
|
+
const agentPath = path.join(root, ".selesai", "agents", "worker.md");
|
|
1756
|
+
fs.writeFileSync(agentPath, `---\nname: worker\ndescription: Test worker\nmodel: bluebox-azure-openai/gpt-5_6-sol\n---\n\nDo work.\n`, "utf-8");
|
|
1757
|
+
const commands = registerAdmin([]);
|
|
1758
|
+
await commands.get("subagents")!.handler("worker thinking", createCommandContext({
|
|
1759
|
+
cwd: root,
|
|
1760
|
+
hasUI: true,
|
|
1761
|
+
modelRegistry: {
|
|
1762
|
+
getAvailable: () => [{
|
|
1763
|
+
provider: "bluebox-azure-openai",
|
|
1764
|
+
id: "gpt-5_6-sol",
|
|
1765
|
+
reasoning: true,
|
|
1766
|
+
thinkingLevelMap: { minimal: null, xhigh: "xhigh", max: "max" },
|
|
1767
|
+
}],
|
|
1768
|
+
},
|
|
1769
|
+
select: async (_title, choices) => {
|
|
1770
|
+
const projectAgent = choices.find((choice) => choice.startsWith("worker [project]"));
|
|
1771
|
+
if (projectAgent) return projectAgent;
|
|
1772
|
+
assert.ok(choices.includes("max"));
|
|
1773
|
+
assert.equal(choices.includes("minimal"), false);
|
|
1774
|
+
return "max";
|
|
1775
|
+
},
|
|
1776
|
+
}));
|
|
1777
|
+
assert.match(fs.readFileSync(agentPath, "utf-8"), /^thinking: max$/m);
|
|
1778
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(settingsPath, "utf-8")).subagents.agentOverrides.worker, {
|
|
1779
|
+
model: "anthropic/claude-opus-4-8",
|
|
1780
|
+
thinking: "high",
|
|
1781
|
+
});
|
|
1782
|
+
});
|
|
1783
|
+
});
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
it("uses the inherited session model's thinking capabilities and warns on refresh failure", async () => {
|
|
1787
|
+
await withIsolatedHome(async () => {
|
|
1788
|
+
await withTempProject("pi-subagents-admin-inherited-thinking-", async (root) => {
|
|
1789
|
+
fs.writeFileSync(path.join(root, ".selesai", "agents", "auditor.md"), "---\nname: auditor\ndescription: Audit things\n---\n\nAudit.\n", "utf-8");
|
|
1790
|
+
const notifications: Array<{ message: string; type?: string }> = [];
|
|
1791
|
+
const commands = registerAdmin([]);
|
|
1792
|
+
await commands.get("subagents")!.handler("auditor thinking", createCommandContext({
|
|
1793
|
+
cwd: root,
|
|
1794
|
+
hasUI: true,
|
|
1795
|
+
model: { provider: "test", id: "plain-model" },
|
|
1796
|
+
modelRegistry: {
|
|
1797
|
+
refresh: () => { throw new Error("invalid models.json"); },
|
|
1798
|
+
getAvailable: () => [{ provider: "test", id: "plain-model", reasoning: false }],
|
|
1799
|
+
},
|
|
1800
|
+
select: async (title, choices) => {
|
|
1801
|
+
assert.match(title, /Session model: test\/plain-model/);
|
|
1802
|
+
assert.deepEqual(choices, ["Default / inherit session thinking", "off"]);
|
|
1803
|
+
return undefined;
|
|
1804
|
+
},
|
|
1805
|
+
notify: (message, type) => notifications.push({ message, type }),
|
|
1806
|
+
}));
|
|
1807
|
+
|
|
1808
|
+
assert.equal(notifications.length, 1);
|
|
1809
|
+
assert.equal(notifications[0]?.type, "warning");
|
|
1810
|
+
assert.match(notifications[0]?.message ?? "", /using the last loaded choices.*invalid models\.json/);
|
|
1811
|
+
});
|
|
1812
|
+
});
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
it("does not pin inherited settings when editing a project-owned field", async () => {
|
|
1816
|
+
await withIsolatedHome(async () => {
|
|
1817
|
+
await withTempProject("pi-subagents-admin-cross-scope-", async (root) => {
|
|
1818
|
+
const agentPath = path.join(root, ".selesai", "agents", "worker.md");
|
|
1819
|
+
fs.writeFileSync(agentPath, "---\nname: worker\ndescription: Test worker\n---\n\nDo work.\n", "utf-8");
|
|
1820
|
+
const settingsPath = path.join(process.env.HOME!, ".selesai", "agent", "settings.json");
|
|
1821
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
1822
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
1823
|
+
subagents: { agentOverrides: { worker: { thinking: "high" } } },
|
|
1824
|
+
}, null, 2));
|
|
1825
|
+
|
|
1826
|
+
const commands = registerAdmin([]);
|
|
1827
|
+
await commands.get("subagents")!.handler("worker model", createCommandContext({
|
|
1828
|
+
cwd: root,
|
|
1829
|
+
hasUI: true,
|
|
1830
|
+
modelRegistry: { getAvailable: () => [{ provider: "bluebox-azure-openai", id: "gpt-5_6-sol", reasoning: true }] },
|
|
1831
|
+
select: async (_title, choices) => {
|
|
1832
|
+
const projectAgent = choices.find((choice) => choice.startsWith("worker [project]"));
|
|
1833
|
+
if (projectAgent) return projectAgent;
|
|
1834
|
+
assert.ok(choices.includes("bluebox-azure-openai/gpt-5_6-sol"));
|
|
1835
|
+
return "bluebox-azure-openai/gpt-5_6-sol";
|
|
1836
|
+
},
|
|
1837
|
+
}));
|
|
1838
|
+
|
|
1839
|
+
const updated = fs.readFileSync(agentPath, "utf-8");
|
|
1840
|
+
assert.match(updated, /^model: bluebox-azure-openai\/gpt-5_6-sol$/m);
|
|
1841
|
+
assert.doesNotMatch(updated, /^thinking:/m);
|
|
1842
|
+
assert.doesNotMatch(updated, /^systemPromptMode:/m);
|
|
1843
|
+
assert.doesNotMatch(updated, /^inheritProjectContext:/m);
|
|
1844
|
+
assert.equal(JSON.parse(fs.readFileSync(settingsPath, "utf-8")).subagents.agentOverrides.worker.thinking, "high");
|
|
1845
|
+
});
|
|
1846
|
+
});
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
it("does not write definitions from SELESAI_SUBAGENT_EXTRA_AGENT_DIRS", async () => {
|
|
1850
|
+
await withIsolatedHome(async () => {
|
|
1851
|
+
await withTempProject("pi-subagents-admin-extra-", async (root) => {
|
|
1852
|
+
const extraDir = path.join(root, "extra-agents");
|
|
1853
|
+
const agentPath = path.join(extraDir, "worker.md");
|
|
1854
|
+
fs.mkdirSync(extraDir, { recursive: true });
|
|
1855
|
+
const original = "---\nname: worker\ndescription: Read-only worker\nmodel: anthropic/claude-sonnet-4-6\n---\n\nDo work.\n";
|
|
1856
|
+
fs.writeFileSync(agentPath, original, "utf-8");
|
|
1857
|
+
const previousExtraDirs = process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
|
|
1858
|
+
process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS = extraDir;
|
|
1859
|
+
try {
|
|
1860
|
+
const commands = registerAdmin([]);
|
|
1861
|
+
await commands.get("subagents")!.handler("worker model", createCommandContext({
|
|
1862
|
+
cwd: root,
|
|
1863
|
+
hasUI: true,
|
|
1864
|
+
modelRegistry: { getAvailable: () => [{ provider: "bluebox-azure-openai", id: "gpt-5_6-sol", reasoning: true }] },
|
|
1865
|
+
select: async (_title, choices) =>
|
|
1866
|
+
choices.find((choice) => choice.startsWith("worker [user]"))
|
|
1867
|
+
?? "bluebox-azure-openai/gpt-5_6-sol",
|
|
1868
|
+
}));
|
|
1869
|
+
assert.equal(fs.readFileSync(agentPath, "utf-8"), original);
|
|
1870
|
+
} finally {
|
|
1871
|
+
if (previousExtraDirs === undefined) delete process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
|
|
1872
|
+
else process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS = previousExtraDirs;
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
});
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
it("respects package-owned fields and uses settings only for unset package fields", async () => {
|
|
1879
|
+
await withIsolatedHome(async () => {
|
|
1880
|
+
await withTempProject("pi-subagents-admin-package-", async (root) => {
|
|
1881
|
+
const packageRoot = path.join(root, ".selesai", "npm", "node_modules", "admin-package");
|
|
1882
|
+
const agentPath = path.join(packageRoot, "agents", "package-admin-agent.md");
|
|
1883
|
+
fs.mkdirSync(path.dirname(agentPath), { recursive: true });
|
|
1884
|
+
fs.writeFileSync(path.join(packageRoot, "package.json"), JSON.stringify({
|
|
1885
|
+
name: "admin-package",
|
|
1886
|
+
"pi-subagents": { agents: ["./agents"] },
|
|
1887
|
+
}), "utf-8");
|
|
1888
|
+
const original = "---\nname: package-admin-agent\ndescription: Package agent\nmodel: test/original\n---\n\nPackage prompt.\n";
|
|
1889
|
+
fs.writeFileSync(agentPath, original, "utf-8");
|
|
1890
|
+
const settingsPath = path.join(root, ".selesai", "settings.json");
|
|
1891
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
1892
|
+
subagents: { agentOverrides: { "package-admin-agent": { thinking: "low" } } },
|
|
1893
|
+
}, null, 2), "utf-8");
|
|
1894
|
+
|
|
1895
|
+
const sent: unknown[] = [];
|
|
1896
|
+
const commands = registerAdmin(sent);
|
|
1897
|
+
await commands.get("subagents")!.handler("package-admin-agent model", createCommandContext({
|
|
1898
|
+
cwd: root,
|
|
1899
|
+
hasUI: true,
|
|
1900
|
+
modelRegistry: { getAvailable: () => [{ provider: "test", id: "replacement", reasoning: true }] },
|
|
1901
|
+
select: async () => "test/replacement",
|
|
1902
|
+
}));
|
|
1903
|
+
await commands.get("subagents")!.handler("package-admin-agent prompt", createCommandContext({
|
|
1904
|
+
cwd: root,
|
|
1905
|
+
hasUI: true,
|
|
1906
|
+
}));
|
|
1907
|
+
|
|
1908
|
+
assert.equal(fs.readFileSync(agentPath, "utf-8"), original);
|
|
1909
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(settingsPath, "utf-8")).subagents.agentOverrides, {
|
|
1910
|
+
"package-admin-agent": { thinking: "low" },
|
|
1911
|
+
});
|
|
1912
|
+
assert.match((sent[0] as { content?: string }).content ?? "", /model.*read-only package definition/);
|
|
1913
|
+
assert.match((sent[1] as { content?: string }).content ?? "", /systemPrompt.*read-only package definition/);
|
|
1914
|
+
|
|
1915
|
+
const configurablePath = path.join(packageRoot, "agents", "configurable-package-agent.md");
|
|
1916
|
+
const configurable = "---\nname: configurable-package-agent\ndescription: Configurable package agent\n---\n\nPackage prompt.\n";
|
|
1917
|
+
fs.writeFileSync(configurablePath, configurable, "utf-8");
|
|
1918
|
+
await commands.get("subagents")!.handler("configurable-package-agent model", createCommandContext({
|
|
1919
|
+
cwd: root,
|
|
1920
|
+
hasUI: true,
|
|
1921
|
+
modelRegistry: { getAvailable: () => [{ provider: "test", id: "replacement", reasoning: true }] },
|
|
1922
|
+
select: async (title) => title.startsWith("Select model") ? "test/replacement" : "project",
|
|
1923
|
+
}));
|
|
1924
|
+
assert.equal(fs.readFileSync(configurablePath, "utf-8"), configurable);
|
|
1925
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(settingsPath, "utf-8")).subagents.agentOverrides, {
|
|
1926
|
+
"package-admin-agent": { thinking: "low" },
|
|
1927
|
+
"configurable-package-agent": { model: "test/replacement" },
|
|
1928
|
+
});
|
|
1929
|
+
});
|
|
1930
|
+
});
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
it("edits a system prompt through Pi's native editor", async () => {
|
|
1934
|
+
await withIsolatedHome(async () => {
|
|
1935
|
+
await withTempProject("pi-subagents-admin-prompt-", async (root) => {
|
|
1936
|
+
const agentPath = path.join(root, ".selesai", "agents", "worker.md");
|
|
1937
|
+
fs.writeFileSync(agentPath, `---\nname: worker\ndescription: Test worker\n---\n\nOriginal prompt.\n`, "utf-8");
|
|
1938
|
+
const status: Array<[string, string | undefined]> = [];
|
|
1939
|
+
let editorArgs: [string, string] | undefined;
|
|
1940
|
+
const commands = registerAdmin([]);
|
|
1941
|
+
await commands.get("subagents")!.handler("worker prompt", createCommandContext({
|
|
1942
|
+
cwd: root,
|
|
1943
|
+
hasUI: true,
|
|
1944
|
+
select: async (_title, choices) => choices.find((choice) => choice.startsWith("worker [project]")),
|
|
1945
|
+
editor: async (title, prefill) => {
|
|
1946
|
+
editorArgs = [title, prefill];
|
|
1947
|
+
return "Edited prompt.\n";
|
|
1948
|
+
},
|
|
1949
|
+
setStatus: (key, text) => status.push([key, text]),
|
|
1950
|
+
}));
|
|
1951
|
+
assert.deepEqual(editorArgs, ["Edit 'worker' system prompt", "Original prompt."]);
|
|
1952
|
+
assert.match(fs.readFileSync(agentPath, "utf-8"), /Edited prompt\./);
|
|
1953
|
+
assert.deepEqual(status, []);
|
|
1954
|
+
});
|
|
1955
|
+
});
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
it("cancels native system-prompt editing without mutation", async () => {
|
|
1959
|
+
await withIsolatedHome(async () => {
|
|
1960
|
+
await withTempProject("pi-subagents-admin-prompt-cancel-", async (root) => {
|
|
1961
|
+
const agentPath = path.join(root, ".selesai", "agents", "worker.md");
|
|
1962
|
+
const original = `---\nname: worker\ndescription: Test worker\n---\n\nOriginal prompt.\n`;
|
|
1963
|
+
fs.writeFileSync(agentPath, original, "utf-8");
|
|
1964
|
+
const sent: unknown[] = [];
|
|
1965
|
+
const commands = registerAdmin(sent);
|
|
1966
|
+
await commands.get("subagents")!.handler("worker prompt", createCommandContext({
|
|
1967
|
+
cwd: root,
|
|
1968
|
+
hasUI: true,
|
|
1969
|
+
select: async (_title, choices) => choices.find((choice) => choice.startsWith("worker [project]")),
|
|
1970
|
+
editor: async () => undefined,
|
|
1971
|
+
}));
|
|
1972
|
+
assert.equal(fs.readFileSync(agentPath, "utf-8"), original);
|
|
1973
|
+
assert.deepEqual(sent, []);
|
|
1974
|
+
});
|
|
1975
|
+
});
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
it("reports native-editor failures without rewriting the agent", async () => {
|
|
1979
|
+
await withIsolatedHome(async () => {
|
|
1980
|
+
await withTempProject("pi-subagents-admin-editor-failure-", async (root) => {
|
|
1981
|
+
const agentPath = path.join(root, ".selesai", "agents", "worker.md");
|
|
1982
|
+
const original = `---\nname: worker\ndescription: Test worker\n---\n\nOriginal prompt.\n`;
|
|
1983
|
+
fs.writeFileSync(agentPath, original, "utf-8");
|
|
1984
|
+
const sent: unknown[] = [];
|
|
1985
|
+
const commands = registerAdmin(sent);
|
|
1986
|
+
await commands.get("subagents")!.handler("worker prompt", createCommandContext({
|
|
1987
|
+
cwd: root,
|
|
1988
|
+
hasUI: true,
|
|
1989
|
+
select: async (_title, choices) => choices.find((choice) => choice.startsWith("worker [project]")),
|
|
1990
|
+
editor: async () => { throw new Error("editor unavailable"); },
|
|
1991
|
+
}));
|
|
1992
|
+
assert.equal(fs.readFileSync(agentPath, "utf-8"), original);
|
|
1993
|
+
assert.match((sent.at(-1) as { content?: string }).content ?? "", /Failed to update 'worker': editor unavailable/);
|
|
1994
|
+
});
|
|
1995
|
+
});
|
|
1996
|
+
});
|
|
1997
|
+
|
|
1998
|
+
it("shows metadata for a named subagent without requiring UI", async () => {
|
|
1999
|
+
await withIsolatedHome(async () => {
|
|
2000
|
+
await withTempProject("pi-subagents-admin-metadata-", async (root) => {
|
|
2001
|
+
fs.writeFileSync(path.join(root, ".selesai", "agents", "auditor.md"), `---\nname: auditor\ndescription: Audit things\nthinking: false\n---\n\nAudit.\n`, "utf-8");
|
|
2002
|
+
const sent: unknown[] = [];
|
|
2003
|
+
const commands = registerAdmin(sent);
|
|
2004
|
+
await commands.get("subagents")!.handler("auditor", createCommandContext({ cwd: root }));
|
|
2005
|
+
assert.equal(sent.length, 1);
|
|
2006
|
+
const content = (sent[0] as { content?: string }).content ?? "";
|
|
2007
|
+
assert.match(content, /Agent: auditor \(project\)/);
|
|
2008
|
+
assert.match(content, /^Thinking: off$/m);
|
|
2009
|
+
});
|
|
2010
|
+
});
|
|
2011
|
+
});
|
|
2012
|
+
});
|
|
2013
|
+
|
|
2014
|
+
|
|
1252
2015
|
describe("subagents-doctor slash command", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
|
|
1253
2016
|
beforeEach(() => {
|
|
1254
2017
|
clearSlashSnapshots?.();
|
|
@@ -1259,11 +2022,237 @@ describe("subagents-doctor slash command", { skip: !available ? "slash-commands.
|
|
|
1259
2022
|
assert.deepEqual(params, { action: "doctor" });
|
|
1260
2023
|
});
|
|
1261
2024
|
|
|
1262
|
-
it("
|
|
2025
|
+
it("keeps the textual fleet status fallback when no UI is available", async () => {
|
|
1263
2026
|
const { params } = await captureSlashCommandParams("subagents-fleet", "", process.cwd());
|
|
1264
2027
|
assert.deepEqual(params, { action: "status", view: "fleet" });
|
|
1265
2028
|
});
|
|
1266
2029
|
|
|
2030
|
+
it("opens the native fleet from both the slash command and direct shortcut", async () => {
|
|
2031
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
2032
|
+
const shortcuts = new Map<string, { description: string; handler(ctx: unknown): Promise<void> }>();
|
|
2033
|
+
let opened = 0;
|
|
2034
|
+
let rendered = "";
|
|
2035
|
+
const custom = async (factory: unknown, options: unknown) => {
|
|
2036
|
+
opened++;
|
|
2037
|
+
const component = (factory as (tui: unknown, theme: unknown, keybindings: unknown, done: (value: undefined) => void) => { render(width: number): string[]; dispose(): void })(
|
|
2038
|
+
{ terminal: { rows: 30, columns: 100 }, requestRender() {} },
|
|
2039
|
+
{ fg: (_name: string, text: string) => text, bold: (text: string) => text },
|
|
2040
|
+
undefined,
|
|
2041
|
+
() => {},
|
|
2042
|
+
);
|
|
2043
|
+
rendered = component.render(100).join("\n");
|
|
2044
|
+
component.dispose();
|
|
2045
|
+
assert.deepEqual(options, { overlay: true, overlayOptions: { anchor: "center", width: "95%", minWidth: 60, maxHeight: "85%", margin: 1 } });
|
|
2046
|
+
return undefined;
|
|
2047
|
+
};
|
|
2048
|
+
const pi = {
|
|
2049
|
+
events: createEventBus(),
|
|
2050
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
2051
|
+
registerShortcut(key: string, spec: { description: string; handler(ctx: unknown): Promise<void> }) { shortcuts.set(key, spec); },
|
|
2052
|
+
sendMessage(_message: unknown) {},
|
|
2053
|
+
};
|
|
2054
|
+
const state = createState(process.cwd());
|
|
2055
|
+
const ctx = createCommandContext({ hasUI: true, custom });
|
|
2056
|
+
registerSlashCommands!(pi, state);
|
|
2057
|
+
|
|
2058
|
+
await commands.get("subagents-fleet")!.handler("", ctx);
|
|
2059
|
+
assert.equal(shortcuts.size, 1);
|
|
2060
|
+
const shortcut = [...shortcuts.values()][0]!;
|
|
2061
|
+
assert.equal(shortcut.description, "Open subagent fleet inspector");
|
|
2062
|
+
await shortcut.handler(ctx);
|
|
2063
|
+
|
|
2064
|
+
assert.equal(opened, 2);
|
|
2065
|
+
assert.match(rendered, /Subagent fleet/);
|
|
2066
|
+
assert.match(rendered, /inspection only/);
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
it("routes subagents-stop with an id directly to the stop action", async () => {
|
|
2070
|
+
const { params } = await captureSlashCommandParams("subagents-stop", "run-123", process.cwd());
|
|
2071
|
+
assert.deepEqual(params, { action: "stop", id: "run-123" });
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
it("prints exact stop commands when subagents-stop has no UI", async () => {
|
|
2075
|
+
await withIsolatedHome(async () => {
|
|
2076
|
+
const runId = `slash-stop-${Date.now().toString(36)}`;
|
|
2077
|
+
const asyncDir = path.join(ASYNC_DIR, runId);
|
|
2078
|
+
fs.mkdirSync(asyncDir, { recursive: true });
|
|
2079
|
+
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
|
|
2080
|
+
runId,
|
|
2081
|
+
sessionId: "session-test",
|
|
2082
|
+
mode: "single",
|
|
2083
|
+
state: "running",
|
|
2084
|
+
pid: process.pid,
|
|
2085
|
+
startedAt: Date.now(),
|
|
2086
|
+
lastUpdate: Date.now(),
|
|
2087
|
+
steps: [{ agent: "worker", status: "running", startedAt: Date.now() }],
|
|
2088
|
+
}, null, 2));
|
|
2089
|
+
try {
|
|
2090
|
+
const sent: unknown[] = [];
|
|
2091
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
2092
|
+
const pi = {
|
|
2093
|
+
events: createEventBus(),
|
|
2094
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
2095
|
+
registerShortcut() {},
|
|
2096
|
+
sendMessage(message: unknown) { sent.push(message); },
|
|
2097
|
+
};
|
|
2098
|
+
const state = createState(process.cwd());
|
|
2099
|
+
state.currentSessionId = "session-test";
|
|
2100
|
+
registerSlashCommands!(pi, state);
|
|
2101
|
+
await commands.get("subagents-stop")!.handler("", createCommandContext({ hasUI: false }));
|
|
2102
|
+
|
|
2103
|
+
const content = String((sent[0] as { content?: unknown }).content ?? "");
|
|
2104
|
+
assert.match(content, new RegExp(`subagent\\({ action: "stop", id: "${runId}" }\\)`));
|
|
2105
|
+
assert.match(content, new RegExp(`/subagents-stop ${runId}`));
|
|
2106
|
+
} finally {
|
|
2107
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
it("prints cancel commands for scheduled subagent runs when subagents-stop has no UI", async () => {
|
|
2113
|
+
await withIsolatedHome(async () => {
|
|
2114
|
+
await withTempProject("pi-slash-stop-scheduled-", async (root) => {
|
|
2115
|
+
const storePath = scheduledRunStorePath(root, "session-test");
|
|
2116
|
+
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
2117
|
+
fs.writeFileSync(storePath, JSON.stringify({
|
|
2118
|
+
version: 1,
|
|
2119
|
+
cwd: root,
|
|
2120
|
+
sessionId: "session-test",
|
|
2121
|
+
jobs: [{
|
|
2122
|
+
id: "job-1",
|
|
2123
|
+
name: "nightly scout",
|
|
2124
|
+
schedule: "+10m",
|
|
2125
|
+
runAt: Date.now() + 600_000,
|
|
2126
|
+
state: "scheduled",
|
|
2127
|
+
createdAt: Date.now(),
|
|
2128
|
+
updatedAt: Date.now(),
|
|
2129
|
+
cwd: root,
|
|
2130
|
+
sessionId: "session-test",
|
|
2131
|
+
params: { agent: "scout", task: "later", async: true },
|
|
2132
|
+
}],
|
|
2133
|
+
}, null, 2));
|
|
2134
|
+
const sent: unknown[] = [];
|
|
2135
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
2136
|
+
const state = createState(root);
|
|
2137
|
+
state.currentSessionId = "session-test";
|
|
2138
|
+
const pi = {
|
|
2139
|
+
events: createEventBus(),
|
|
2140
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
2141
|
+
registerShortcut() {},
|
|
2142
|
+
sendMessage(message: unknown) { sent.push(message); },
|
|
2143
|
+
};
|
|
2144
|
+
registerSlashCommands!(pi, state);
|
|
2145
|
+
await commands.get("subagents-stop")!.handler("", createCommandContext({ cwd: root, hasUI: false }));
|
|
2146
|
+
|
|
2147
|
+
const content = String((sent[0] as { content?: unknown }).content ?? "");
|
|
2148
|
+
assert.match(content, /job-1 · nightly scout/);
|
|
2149
|
+
assert.match(content, /cancel scheduled run: subagent\({ action: "schedule-cancel", id: "job-1" }\)/);
|
|
2150
|
+
assert.doesNotMatch(content, /\/subagents-stop job-1/);
|
|
2151
|
+
});
|
|
2152
|
+
});
|
|
2153
|
+
});
|
|
2154
|
+
|
|
2155
|
+
it("routes selected scheduled subagents-stop targets through schedule-cancel", async () => {
|
|
2156
|
+
await withIsolatedHome(async () => {
|
|
2157
|
+
await withTempProject("pi-slash-stop-selected-scheduled-", async (root) => {
|
|
2158
|
+
const storePath = scheduledRunStorePath(root, "session-test");
|
|
2159
|
+
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
2160
|
+
fs.writeFileSync(storePath, JSON.stringify({
|
|
2161
|
+
version: 1,
|
|
2162
|
+
cwd: root,
|
|
2163
|
+
sessionId: "session-test",
|
|
2164
|
+
jobs: [{
|
|
2165
|
+
id: "job-2",
|
|
2166
|
+
name: "delayed worker",
|
|
2167
|
+
schedule: "+10m",
|
|
2168
|
+
runAt: Date.now() + 600_000,
|
|
2169
|
+
state: "scheduled",
|
|
2170
|
+
createdAt: Date.now(),
|
|
2171
|
+
updatedAt: Date.now(),
|
|
2172
|
+
cwd: root,
|
|
2173
|
+
sessionId: "session-test",
|
|
2174
|
+
params: { agent: "worker", task: "later", async: true },
|
|
2175
|
+
}],
|
|
2176
|
+
}, null, 2));
|
|
2177
|
+
|
|
2178
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
2179
|
+
const events = createEventBus();
|
|
2180
|
+
let requestedParams: unknown;
|
|
2181
|
+
events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
|
|
2182
|
+
const payload = data as { requestId: string; params?: unknown };
|
|
2183
|
+
requestedParams = payload.params;
|
|
2184
|
+
events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId: payload.requestId });
|
|
2185
|
+
events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
|
|
2186
|
+
requestId: payload.requestId,
|
|
2187
|
+
result: { content: [{ type: "text", text: "cancelled" }], details: { mode: "management", results: [] } },
|
|
2188
|
+
isError: false,
|
|
2189
|
+
});
|
|
2190
|
+
});
|
|
2191
|
+
const state = createState(root);
|
|
2192
|
+
state.currentSessionId = "session-test";
|
|
2193
|
+
const pi = {
|
|
2194
|
+
events,
|
|
2195
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
2196
|
+
registerShortcut() {},
|
|
2197
|
+
sendMessage(_message: unknown) {},
|
|
2198
|
+
};
|
|
2199
|
+
registerSlashCommands!(pi, state);
|
|
2200
|
+
await commands.get("subagents-stop")!.handler("", createCommandContext({
|
|
2201
|
+
cwd: root,
|
|
2202
|
+
hasUI: true,
|
|
2203
|
+
custom: async () => ({
|
|
2204
|
+
confirmed: true,
|
|
2205
|
+
target: { kind: "scheduled", id: "job-2", label: "job-2", detail: "scheduled", actionLabel: "cancel scheduled run" },
|
|
2206
|
+
}),
|
|
2207
|
+
}));
|
|
2208
|
+
|
|
2209
|
+
assert.deepEqual(requestedParams, { action: "schedule-cancel", id: "job-2" });
|
|
2210
|
+
});
|
|
2211
|
+
});
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
it("prints fallback text instead of opening or enumerating the selector in child-safe fanout mode", async () => {
|
|
2215
|
+
await withIsolatedHome(async () => {
|
|
2216
|
+
const previous = process.env[SUBAGENT_FANOUT_CHILD_ENV];
|
|
2217
|
+
const runId = `slash-stop-child-safe-${Date.now().toString(36)}`;
|
|
2218
|
+
const asyncDir = path.join(ASYNC_DIR, runId);
|
|
2219
|
+
process.env[SUBAGENT_FANOUT_CHILD_ENV] = "1";
|
|
2220
|
+
fs.mkdirSync(asyncDir, { recursive: true });
|
|
2221
|
+
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
|
|
2222
|
+
runId,
|
|
2223
|
+
sessionId: "session-test",
|
|
2224
|
+
mode: "single",
|
|
2225
|
+
state: "running",
|
|
2226
|
+
pid: process.pid,
|
|
2227
|
+
startedAt: Date.now(),
|
|
2228
|
+
lastUpdate: Date.now(),
|
|
2229
|
+
steps: [{ agent: "worker", status: "running", startedAt: Date.now() }],
|
|
2230
|
+
}, null, 2));
|
|
2231
|
+
try {
|
|
2232
|
+
const sent: unknown[] = [];
|
|
2233
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
2234
|
+
const pi = {
|
|
2235
|
+
events: createEventBus(),
|
|
2236
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
2237
|
+
registerShortcut() {},
|
|
2238
|
+
sendMessage(message: unknown) { sent.push(message); },
|
|
2239
|
+
};
|
|
2240
|
+
const state = createState(process.cwd());
|
|
2241
|
+
state.currentSessionId = "session-test";
|
|
2242
|
+
registerSlashCommands!(pi, state);
|
|
2243
|
+
await commands.get("subagents-stop")!.handler("", createCommandContext({ hasUI: true }));
|
|
2244
|
+
|
|
2245
|
+
const content = String((sent[0] as { content?: unknown }).content ?? "");
|
|
2246
|
+
assert.match(content, /Selector unavailable in child-safe fanout mode\./);
|
|
2247
|
+
assert.doesNotMatch(content, new RegExp(runId));
|
|
2248
|
+
} finally {
|
|
2249
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
2250
|
+
if (previous === undefined) delete process.env[SUBAGENT_FANOUT_CHILD_ENV];
|
|
2251
|
+
else process.env[SUBAGENT_FANOUT_CHILD_ENV] = previous;
|
|
2252
|
+
}
|
|
2253
|
+
});
|
|
2254
|
+
});
|
|
2255
|
+
|
|
1267
2256
|
it("does not register the removed subagents-status overlay command", async () => {
|
|
1268
2257
|
await withIsolatedHome(async () => {
|
|
1269
2258
|
const commands = new Map<string, RegisteredSlashCommand>();
|