@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,290 @@
|
|
|
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 { afterEach, beforeEach, describe, it } from "node:test";
|
|
6
|
+
import { resolveWatchdogConfig, resolveWatchdogConfigStrict, writeUserWatchdogEnabled, writeWatchdogModelSettings } from "../../src/watchdog/settings.ts";
|
|
7
|
+
|
|
8
|
+
let tempHome = "";
|
|
9
|
+
let tempProject = "";
|
|
10
|
+
const originalHome = process.env.HOME;
|
|
11
|
+
const originalUserProfile = process.env.USERPROFILE;
|
|
12
|
+
const originalPiCodingAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
13
|
+
|
|
14
|
+
function writeJson(filePath: string, value: unknown): void {
|
|
15
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
16
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function userSettingsPath(): string {
|
|
20
|
+
return path.join(tempHome, ".selesai", "agent", "settings.json");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function projectSettingsPath(): string {
|
|
24
|
+
return path.join(tempProject, ".selesai", "settings.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("watchdog settings", () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-watchdog-home-"));
|
|
30
|
+
tempProject = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-watchdog-project-"));
|
|
31
|
+
process.env.HOME = tempHome;
|
|
32
|
+
process.env.USERPROFILE = tempHome;
|
|
33
|
+
delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
38
|
+
else process.env.HOME = originalHome;
|
|
39
|
+
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
|
40
|
+
else process.env.USERPROFILE = originalUserProfile;
|
|
41
|
+
if (originalPiCodingAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
42
|
+
else process.env.SELESAI_CODING_AGENT_DIR = originalPiCodingAgentDir;
|
|
43
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
44
|
+
fs.rmSync(tempProject, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("resolves a default-off config when no watchdog settings exist", () => {
|
|
48
|
+
const result = resolveWatchdogConfig(tempProject);
|
|
49
|
+
|
|
50
|
+
assert.equal(result.ok, true);
|
|
51
|
+
assert.equal(result.config.enabled, false);
|
|
52
|
+
assert.equal(result.config.main.enabled, false);
|
|
53
|
+
assert.equal(result.config.children.enabled, false);
|
|
54
|
+
assert.equal(result.config.asyncCompletion.enabled, false);
|
|
55
|
+
assert.equal(result.config.delivery, "held");
|
|
56
|
+
assert.equal(result.config.showDuringRun, false);
|
|
57
|
+
assert.equal(result.config.agentEndTimeoutMs, 30_000);
|
|
58
|
+
assert.equal(result.config.children.watchdogTailTimeoutMs, 120_000);
|
|
59
|
+
assert.equal(result.config.autoFollow.maxAttempts, 3);
|
|
60
|
+
assert.deepEqual(result.config.lsp, { enabled: true, timeoutMs: 3000, maxFiles: 20, maxDiagnostics: 50 });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("lets root enabled opt the main watchdog in while children stay default-off", () => {
|
|
64
|
+
writeJson(userSettingsPath(), {
|
|
65
|
+
subagents: {
|
|
66
|
+
watchdog: {
|
|
67
|
+
enabled: true,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const config = resolveWatchdogConfig(tempProject).config;
|
|
73
|
+
|
|
74
|
+
assert.equal(config.enabled, true);
|
|
75
|
+
assert.equal(config.main.enabled, true);
|
|
76
|
+
assert.equal(config.children.enabled, false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("merges user, project, and session overrides field by field", () => {
|
|
80
|
+
writeJson(userSettingsPath(), {
|
|
81
|
+
subagents: {
|
|
82
|
+
watchdog: {
|
|
83
|
+
enabled: true,
|
|
84
|
+
autoFollow: { blockers: true, maxAttempts: 5 },
|
|
85
|
+
children: {
|
|
86
|
+
autoFollow: { maxAttempts: 4 },
|
|
87
|
+
overrides: {
|
|
88
|
+
worker: { enabled: true, model: "anthropic/claude-test" },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
guidance: { systemPromptPath: "/tmp/user-watchdog.md" },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
writeJson(projectSettingsPath(), {
|
|
96
|
+
subagents: {
|
|
97
|
+
watchdog: {
|
|
98
|
+
autoFollow: { stalemateRepeats: 2 },
|
|
99
|
+
lsp: { enabled: false, timeoutMs: 1500, maxFiles: 4, maxDiagnostics: 7 },
|
|
100
|
+
main: { model: "openai/gpt-test" },
|
|
101
|
+
children: {
|
|
102
|
+
autoFollow: { blockers: false },
|
|
103
|
+
overrides: {
|
|
104
|
+
worker: { thinking: "medium" },
|
|
105
|
+
reviewer: { enabled: false },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const result = resolveWatchdogConfig(tempProject, {
|
|
113
|
+
session: {
|
|
114
|
+
autoFollow: { maxAttempts: null },
|
|
115
|
+
children: {
|
|
116
|
+
overrides: {
|
|
117
|
+
worker: { enabled: false },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
assert.equal(result.ok, true);
|
|
124
|
+
assert.equal(result.config.main.enabled, true);
|
|
125
|
+
assert.equal(result.config.main.model, "openai/gpt-test");
|
|
126
|
+
assert.deepEqual(result.config.autoFollow, { blockers: true, maxAttempts: null, stalemateRepeats: 2 });
|
|
127
|
+
assert.equal(result.config.guidance.systemPromptPath, "/tmp/user-watchdog.md");
|
|
128
|
+
assert.deepEqual(result.config.lsp, { enabled: false, timeoutMs: 1500, maxFiles: 4, maxDiagnostics: 7 });
|
|
129
|
+
assert.deepEqual(result.config.children.autoFollow, { blockers: false, maxAttempts: 4, stalemateRepeats: 3 });
|
|
130
|
+
assert.deepEqual(result.config.children.overrides.worker, {
|
|
131
|
+
enabled: false,
|
|
132
|
+
model: "anthropic/claude-test",
|
|
133
|
+
thinking: "medium",
|
|
134
|
+
});
|
|
135
|
+
assert.deepEqual(result.config.children.overrides.reviewer, { enabled: false });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("returns exact validation errors at the watchdog boundary", () => {
|
|
139
|
+
writeJson(userSettingsPath(), {
|
|
140
|
+
subagents: {
|
|
141
|
+
watchdog: {
|
|
142
|
+
enabled: "yes",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const result = resolveWatchdogConfig(tempProject);
|
|
148
|
+
|
|
149
|
+
assert.equal(result.ok, false);
|
|
150
|
+
assert.deepEqual(result.errors, [{
|
|
151
|
+
scope: "user",
|
|
152
|
+
path: userSettingsPath(),
|
|
153
|
+
message: `Watchdog settings in '${userSettingsPath()}' have invalid 'subagents.watchdog.enabled'; expected a boolean.`,
|
|
154
|
+
}]);
|
|
155
|
+
assert.equal(result.config.enabled, false);
|
|
156
|
+
assert.equal(result.config.main.enabled, false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("throws the same exact validation error in strict mode", () => {
|
|
160
|
+
writeJson(userSettingsPath(), {
|
|
161
|
+
subagents: {
|
|
162
|
+
watchdog: {
|
|
163
|
+
children: { overrides: { worker: { mode: "fast" } } },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
assert.throws(
|
|
169
|
+
() => resolveWatchdogConfigStrict(tempProject),
|
|
170
|
+
(error: unknown) => error instanceof Error
|
|
171
|
+
&& error.message === `Watchdog settings in '${userSettingsPath()}' have unknown field 'subagents.watchdog.children.overrides.worker.mode'.`,
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("rejects invalid LSP config at settings load", () => {
|
|
176
|
+
writeJson(userSettingsPath(), {
|
|
177
|
+
subagents: {
|
|
178
|
+
watchdog: {
|
|
179
|
+
lsp: { timeoutMs: 0 },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const result = resolveWatchdogConfig(tempProject);
|
|
185
|
+
|
|
186
|
+
assert.equal(result.ok, false);
|
|
187
|
+
assert.equal(result.config.main.enabled, false);
|
|
188
|
+
assert.match(result.errors[0]?.message ?? "", /invalid 'subagents\.watchdog\.lsp\.timeoutMs'/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("rejects unsupported watchdog thinking values at settings load", () => {
|
|
192
|
+
writeJson(userSettingsPath(), {
|
|
193
|
+
subagents: {
|
|
194
|
+
watchdog: {
|
|
195
|
+
main: { thinking: "maximum" },
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const result = resolveWatchdogConfig(tempProject);
|
|
201
|
+
|
|
202
|
+
assert.equal(result.ok, false);
|
|
203
|
+
assert.equal(result.config.main.enabled, false);
|
|
204
|
+
assert.match(result.errors[0]?.message ?? "", /invalid 'subagents\.watchdog\.main\.thinking'/);
|
|
205
|
+
assert.match(result.errors[0]?.message ?? "", /'high'/);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("writes watchdog model settings without toggling enablement", () => {
|
|
209
|
+
writeJson(userSettingsPath(), {
|
|
210
|
+
subagents: {
|
|
211
|
+
watchdog: {
|
|
212
|
+
enabled: false,
|
|
213
|
+
main: { enabled: false },
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
writeWatchdogModelSettings({
|
|
219
|
+
scope: "user",
|
|
220
|
+
target: { kind: "main" },
|
|
221
|
+
model: "anthropic/claude-opus-4-8",
|
|
222
|
+
thinking: "high",
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const settings = JSON.parse(fs.readFileSync(userSettingsPath(), "utf-8"));
|
|
226
|
+
assert.equal(settings.subagents.watchdog.enabled, false);
|
|
227
|
+
assert.equal(settings.subagents.watchdog.main.enabled, false);
|
|
228
|
+
assert.equal(settings.subagents.watchdog.main.model, "anthropic/claude-opus-4-8");
|
|
229
|
+
assert.equal(settings.subagents.watchdog.main.thinking, "high");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("clears watchdog model and thinking overrides", () => {
|
|
233
|
+
writeJson(userSettingsPath(), {
|
|
234
|
+
subagents: {
|
|
235
|
+
watchdog: {
|
|
236
|
+
main: { model: "openai-codex/gpt-5.5", thinking: "high" },
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
writeWatchdogModelSettings({
|
|
242
|
+
scope: "user",
|
|
243
|
+
target: { kind: "main" },
|
|
244
|
+
model: null,
|
|
245
|
+
thinking: null,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const settings = JSON.parse(fs.readFileSync(userSettingsPath(), "utf-8"));
|
|
249
|
+
assert.equal("model" in settings.subagents.watchdog.main, false);
|
|
250
|
+
assert.equal("thinking" in settings.subagents.watchdog.main, false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("preserves JSON.parse errors in boundary results", () => {
|
|
254
|
+
fs.mkdirSync(path.dirname(userSettingsPath()), { recursive: true });
|
|
255
|
+
fs.writeFileSync(userSettingsPath(), "{ bad", "utf-8");
|
|
256
|
+
|
|
257
|
+
const result = resolveWatchdogConfig(tempProject);
|
|
258
|
+
|
|
259
|
+
assert.equal(result.ok, false);
|
|
260
|
+
assert.equal(result.errors.length, 1);
|
|
261
|
+
assert.equal(result.errors[0]?.scope, "user");
|
|
262
|
+
assert.match(result.errors[0]?.message ?? "", new RegExp(`^Failed to parse settings file '${userSettingsPath().replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}': `));
|
|
263
|
+
assert.equal(result.config.main.enabled, false);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("writes persistent on/off toggles to root and main enabled", () => {
|
|
267
|
+
writeJson(userSettingsPath(), {
|
|
268
|
+
subagents: {
|
|
269
|
+
watchdog: {
|
|
270
|
+
enabled: false,
|
|
271
|
+
main: { enabled: false, model: "openai/watchdog" },
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
writeUserWatchdogEnabled(true);
|
|
277
|
+
let settings = JSON.parse(fs.readFileSync(userSettingsPath(), "utf-8"));
|
|
278
|
+
assert.equal(settings.subagents.watchdog.enabled, true);
|
|
279
|
+
assert.equal(settings.subagents.watchdog.main.enabled, true);
|
|
280
|
+
assert.equal(settings.subagents.watchdog.main.model, "openai/watchdog");
|
|
281
|
+
assert.equal(resolveWatchdogConfig(tempProject).config.main.enabled, true);
|
|
282
|
+
|
|
283
|
+
writeUserWatchdogEnabled(false);
|
|
284
|
+
settings = JSON.parse(fs.readFileSync(userSettingsPath(), "utf-8"));
|
|
285
|
+
assert.equal(settings.subagents.watchdog.enabled, false);
|
|
286
|
+
assert.equal(settings.subagents.watchdog.main.enabled, false);
|
|
287
|
+
assert.equal(settings.subagents.watchdog.main.model, "openai/watchdog");
|
|
288
|
+
assert.equal(resolveWatchdogConfig(tempProject).config.main.enabled, false);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
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 { afterEach, beforeEach, describe, it } from "node:test";
|
|
6
|
+
import { MainWatchdogRuntime } from "../../src/watchdog/runtime.ts";
|
|
7
|
+
import { handleWatchdogToolAction } from "../../src/watchdog/tool-actions.ts";
|
|
8
|
+
|
|
9
|
+
let tempHome = "";
|
|
10
|
+
let tempProject = "";
|
|
11
|
+
const originalHome = process.env.HOME;
|
|
12
|
+
const originalUserProfile = process.env.USERPROFILE;
|
|
13
|
+
const originalPiCodingAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
14
|
+
|
|
15
|
+
function createCtx(current: { provider: string; id: string }) {
|
|
16
|
+
const models = [
|
|
17
|
+
{ provider: "openai-codex", id: "gpt-5.5", reasoning: true },
|
|
18
|
+
{ provider: "anthropic", id: "claude-opus-4-8", reasoning: true },
|
|
19
|
+
];
|
|
20
|
+
return {
|
|
21
|
+
cwd: tempProject,
|
|
22
|
+
model: current,
|
|
23
|
+
modelRegistry: {
|
|
24
|
+
getAvailable: () => models,
|
|
25
|
+
find: (provider: string, id: string) => models.find((model) => model.provider === provider && model.id === id),
|
|
26
|
+
hasConfiguredAuth: (model: { provider: string; id: string }) => Boolean(model),
|
|
27
|
+
},
|
|
28
|
+
sessionManager: {
|
|
29
|
+
getSessionFile: () => null,
|
|
30
|
+
getSessionId: () => "session-test",
|
|
31
|
+
},
|
|
32
|
+
} as never;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function text(result: { content: Array<{ text?: string }> }): string {
|
|
36
|
+
return result.content.map((entry) => entry.text ?? "").join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("watchdog tool actions", () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-watchdog-tool-home-"));
|
|
42
|
+
tempProject = fs.mkdtempSync(path.join(os.tmpdir(), "pi-watchdog-tool-project-"));
|
|
43
|
+
process.env.HOME = tempHome;
|
|
44
|
+
process.env.USERPROFILE = tempHome;
|
|
45
|
+
process.env.SELESAI_CODING_AGENT_DIR = path.join(tempHome, ".selesai", "agent");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
50
|
+
else process.env.HOME = originalHome;
|
|
51
|
+
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
|
52
|
+
else process.env.USERPROFILE = originalUserProfile;
|
|
53
|
+
if (originalPiCodingAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
54
|
+
else process.env.SELESAI_CODING_AGENT_DIR = originalPiCodingAgentDir;
|
|
55
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
56
|
+
fs.rmSync(tempProject, { recursive: true, force: true });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("configures the recommended model for the current session without writing settings", () => {
|
|
60
|
+
const runtime = new MainWatchdogRuntime({ cwd: tempProject });
|
|
61
|
+
const result = handleWatchdogToolAction("watchdog.configure", { model: "recommended" }, createCtx({ provider: "openai-codex", id: "gpt-5.5" }), runtime);
|
|
62
|
+
|
|
63
|
+
assert.equal(result.isError, undefined);
|
|
64
|
+
assert.match(text(result), /session model configured: anthropic\/claude-opus-4-8:high/);
|
|
65
|
+
assert.equal(fs.existsSync(path.join(tempHome, ".selesai", "agent", "settings.json")), false);
|
|
66
|
+
assert.equal(runtime.getSnapshot(tempProject).config.main.model, "anthropic/claude-opus-4-8");
|
|
67
|
+
assert.equal(runtime.getSnapshot(tempProject).config.main.thinking, "high");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("preserves omitted session model fields when configuring thinking", () => {
|
|
71
|
+
const runtime = new MainWatchdogRuntime({ cwd: tempProject });
|
|
72
|
+
const ctx = createCtx({ provider: "openai-codex", id: "gpt-5.5" });
|
|
73
|
+
|
|
74
|
+
handleWatchdogToolAction("watchdog.configure", { model: "recommended" }, ctx, runtime);
|
|
75
|
+
const result = handleWatchdogToolAction("watchdog.configure", { thinking: "low" }, ctx, runtime);
|
|
76
|
+
|
|
77
|
+
assert.equal(result.isError, undefined);
|
|
78
|
+
assert.equal(runtime.getSnapshot(tempProject).config.main.model, "anthropic/claude-opus-4-8");
|
|
79
|
+
assert.equal(runtime.getSnapshot(tempProject).config.main.thinking, "low");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("persists the recommended model when user scope is explicit", () => {
|
|
83
|
+
const runtime = new MainWatchdogRuntime({ cwd: tempProject });
|
|
84
|
+
const result = handleWatchdogToolAction("watchdog.configure", { scope: "user", model: "recommended" }, createCtx({ provider: "anthropic", id: "claude-opus-4-8" }), runtime);
|
|
85
|
+
|
|
86
|
+
assert.equal(result.isError, undefined);
|
|
87
|
+
const settings = JSON.parse(fs.readFileSync(path.join(tempHome, ".selesai", "agent", "settings.json"), "utf-8"));
|
|
88
|
+
assert.equal(settings.subagents.watchdog.main.model, "openai-codex/gpt-5.5");
|
|
89
|
+
assert.equal(settings.subagents.watchdog.main.thinking, "high");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { formatWatchdogReviewMessage, formatWatchdogTurnDelta } from "../../src/watchdog/turn-delta.ts";
|
|
4
|
+
import { SUBAGENT_WATCHDOG_WARNING_TYPE } from "../../src/watchdog/types.ts";
|
|
5
|
+
|
|
6
|
+
describe("watchdog turn delta formatter", () => {
|
|
7
|
+
it("formats user prompt, assistant text, thinking, tool calls, and tool results", () => {
|
|
8
|
+
const delta = formatWatchdogTurnDelta({
|
|
9
|
+
includeUserPrompt: true,
|
|
10
|
+
userPrompt: "Implement the parser.",
|
|
11
|
+
messages: [
|
|
12
|
+
{
|
|
13
|
+
role: "assistant",
|
|
14
|
+
content: [
|
|
15
|
+
{ type: "text", text: "I will inspect the settings." },
|
|
16
|
+
{ type: "thinking", thinking: "The settings parser is the risky part." },
|
|
17
|
+
{ type: "toolCall", name: "bash", arguments: { command: "printf 'a\\nb'" } },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
events: [
|
|
22
|
+
{
|
|
23
|
+
type: "tool_execution_end",
|
|
24
|
+
toolName: "bash",
|
|
25
|
+
result: { content: [{ type: "text", text: "a\nb" }] },
|
|
26
|
+
isError: false,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
assert.match(delta, /User prompt:\nImplement the parser\./);
|
|
32
|
+
assert.match(delta, /Assistant:\nI will inspect the settings\./);
|
|
33
|
+
assert.match(delta, /Thinking:\nThe settings parser is the risky part\./);
|
|
34
|
+
assert.match(delta, /Tool call: bash\nArguments:\ncommand:/);
|
|
35
|
+
assert.match(delta, /printf 'a\\nb'/);
|
|
36
|
+
assert.match(delta, /Tool result: bash\nResult:\na\nb/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("uses a successful tool diff instead of noisy raw edit blobs", () => {
|
|
40
|
+
const delta = formatWatchdogTurnDelta({
|
|
41
|
+
messages: [
|
|
42
|
+
{
|
|
43
|
+
role: "assistant",
|
|
44
|
+
content: [{
|
|
45
|
+
type: "toolCall",
|
|
46
|
+
name: "edit",
|
|
47
|
+
arguments: {
|
|
48
|
+
path: "src/file.ts",
|
|
49
|
+
oldText: "huge old text",
|
|
50
|
+
newText: "huge new text",
|
|
51
|
+
edits: [{ oldText: "nested old text", newText: "nested new text" }],
|
|
52
|
+
},
|
|
53
|
+
}],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
role: "toolResult",
|
|
57
|
+
toolName: "edit",
|
|
58
|
+
content: "oldText: huge\nnewText: huge",
|
|
59
|
+
details: { diff: "--- a/file.ts\n+++ b/file.ts\n@@\n-old\n+new" },
|
|
60
|
+
isError: false,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
assert.match(delta, /Tool result: edit\nDiff:\n--- a\/file\.ts/);
|
|
66
|
+
assert.match(delta, /oldText: \[omitted 13 chars; use tool result diff\]/);
|
|
67
|
+
assert.match(delta, /newText: \[omitted 13 chars; use tool result diff\]/);
|
|
68
|
+
assert.match(delta, /oldText: \[omitted 15 chars; use tool result diff\]/);
|
|
69
|
+
assert.match(delta, /newText: \[omitted 15 chars; use tool result diff\]/);
|
|
70
|
+
assert.doesNotMatch(delta, /huge old text/);
|
|
71
|
+
assert.doesNotMatch(delta, /huge new text/);
|
|
72
|
+
assert.doesNotMatch(delta, /nested old text/);
|
|
73
|
+
assert.doesNotMatch(delta, /nested new text/);
|
|
74
|
+
assert.doesNotMatch(delta, /oldText: huge/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("marks failed tool results from isError and does not replace them with diffs", () => {
|
|
78
|
+
const delta = formatWatchdogTurnDelta({
|
|
79
|
+
messages: [{ role: "toolResult", toolName: "edit", isError: true, content: [{ type: "text", text: "edit failed" }], details: { diff: "should not render" } }],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
assert.match(delta, /Tool result: edit\nError: tool reported an error\nOutput:\nedit failed/);
|
|
83
|
+
assert.doesNotMatch(delta, /should not render/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("formats turn_end events with assistant message and tool results", () => {
|
|
87
|
+
const delta = formatWatchdogTurnDelta({
|
|
88
|
+
events: [
|
|
89
|
+
{
|
|
90
|
+
type: "turn_end",
|
|
91
|
+
message: {
|
|
92
|
+
role: "assistant",
|
|
93
|
+
content: [
|
|
94
|
+
{ type: "text", text: "I will run a check." },
|
|
95
|
+
{ type: "toolCall", name: "bash", arguments: { command: "npm test" } },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
toolResults: [
|
|
99
|
+
{ role: "toolResult", toolName: "bash", isError: true, content: [{ type: "text", text: "tests failed" }] },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
assert.match(delta, /Assistant:\nI will run a check\./);
|
|
106
|
+
assert.match(delta, /Tool call: bash\nArguments:\ncommand: npm test/);
|
|
107
|
+
assert.match(delta, /Tool result: bash\nError: tool reported an error\nOutput:\ntests failed/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("filters watchdog warning custom messages from its own review stream", () => {
|
|
111
|
+
const warningMessage = {
|
|
112
|
+
role: "custom",
|
|
113
|
+
customType: SUBAGENT_WATCHDOG_WARNING_TYPE,
|
|
114
|
+
content: "prior watchdog warning",
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
assert.equal(formatWatchdogReviewMessage(warningMessage), undefined);
|
|
118
|
+
assert.equal(formatWatchdogTurnDelta({ messages: [warningMessage] }), "");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("marks final assistant stop when represented", () => {
|
|
122
|
+
const delta = formatWatchdogTurnDelta({
|
|
123
|
+
messages: [{ role: "assistant", content: "Done.", stopReason: "stop" }],
|
|
124
|
+
finalAssistantStop: true,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
assert.match(delta, /Assistant stop: stop/);
|
|
128
|
+
assert.match(delta, /Final assistant stop: stop without tool call/);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -41,11 +41,14 @@ describe("nested widget rendering", () => {
|
|
|
41
41
|
it("uses aggregate lines when collapsed and full child rows when expanded", () => {
|
|
42
42
|
const child = nested("nested-reviewer", "root-run", "running", { currentTool: "read" });
|
|
43
43
|
const collapsed = buildWidgetLines([job(child)], theme as any, 120, false).join("\n");
|
|
44
|
-
assert.match(collapsed, /↳ \+1 nested run \(1 running\)/);
|
|
44
|
+
assert.match(collapsed, /↳ \[\d{2}:\d{2}:\d{2}\] \+1 nested run \(1 running\)/);
|
|
45
45
|
assert.doesNotMatch(collapsed, /nested-reviewer · running/);
|
|
46
46
|
|
|
47
47
|
const expanded = buildWidgetLines([job(child)], theme as any, 120, true).join("\n");
|
|
48
|
-
assert.match(expanded, /↳ . nested-reviewer · running · read/);
|
|
48
|
+
assert.match(expanded, /↳ \[\d{2}:\d{2}:\d{2}\] . nested-reviewer · running · read/);
|
|
49
|
+
|
|
50
|
+
const epoch = buildWidgetLines([job(nested("epoch", "root-run", "running", { lastUpdate: 0, startedAt: 0 }))], theme as any, 120, false).join("\n");
|
|
51
|
+
assert.match(epoch, /↳ \[\d{2}:\d{2}:\d{2}\] \+1 nested run/);
|
|
49
52
|
});
|
|
50
53
|
|
|
51
54
|
it("collapses descendants beyond the nested depth budget", () => {
|
|
@@ -71,13 +74,38 @@ describe("nested widget rendering", () => {
|
|
|
71
74
|
state.steps![0]!.status = "complete";
|
|
72
75
|
const expanded = buildWidgetLines([state], theme as any, 120, true).join("\n");
|
|
73
76
|
assert.match(expanded, /✓ Step 1\/1: owner · complete/);
|
|
74
|
-
assert.match(expanded, /↳ . still-running · running/);
|
|
77
|
+
assert.match(expanded, /↳ \[\d{2}:\d{2}:\d{2}\] . still-running · running/);
|
|
75
78
|
});
|
|
76
79
|
|
|
77
80
|
it("degrades stale child summaries to id and state", () => {
|
|
78
81
|
const child = nested("missing-metadata", "root-run", "failed", { agent: undefined, error: "owner gone" });
|
|
79
82
|
const expanded = buildWidgetLines([job(child)], theme as any, 120, true).join("\n");
|
|
80
|
-
assert.match(expanded,
|
|
83
|
+
assert.match(expanded, /\[\d{2}:\d{2}:\d{2}\] . missing-metadata · failed · Failed · owner gone/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("timestamps every nested lifecycle state and completed steps", () => {
|
|
87
|
+
const states: NestedRunSummary["state"][] = ["queued", "running", "complete", "failed", "paused", "stopped"];
|
|
88
|
+
const root = nested("matrix-root", "root-run", "running", {
|
|
89
|
+
steps: [{ agent: "completed-step", status: "completed", endedAt: 2_000 }],
|
|
90
|
+
children: states.map((state, index) => nested(`child-${state}`, "matrix-root", state, {
|
|
91
|
+
parentStepIndex: undefined,
|
|
92
|
+
startedAt: 1_000 + index,
|
|
93
|
+
...(state === "running" ? { lastActivityAt: 2_000 + index } : { endedAt: 2_000 + index }),
|
|
94
|
+
})),
|
|
95
|
+
});
|
|
96
|
+
const expanded = buildWidgetLines([job(root)], theme as any, 200, true).join("\n");
|
|
97
|
+
for (const state of states) {
|
|
98
|
+
assert.match(expanded, new RegExp(`\\[\\d{2}:\\d{2}:\\d{2}\\] . child-${state} · ${state}`));
|
|
99
|
+
}
|
|
100
|
+
assert.match(expanded, /\[\d{2}:\d{2}:\d{2}\] . completed-step · completed/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("keeps event-time timestamps stable instead of advancing with wall time", async () => {
|
|
104
|
+
const state = job(nested("nested-reviewer", "root-run", "running", { currentTool: "read", currentToolStartedAt: 0 }));
|
|
105
|
+
const first = buildWidgetLines([state], theme as any, 120, true);
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
107
|
+
const second = buildWidgetLines([state], theme as any, 120, true);
|
|
108
|
+
assert.deepEqual(second, first);
|
|
81
109
|
});
|
|
82
110
|
|
|
83
111
|
it("rerenders when only nested state changes", () => {
|
|
@@ -246,8 +246,12 @@ describe("worktree", () => {
|
|
|
246
246
|
setup = createWorktrees(repoDir, "cleanup", 2);
|
|
247
247
|
const worktreePaths = setup.worktrees.map((worktree) => worktree.path);
|
|
248
248
|
const branches = setup.worktrees.map((worktree) => worktree.branch);
|
|
249
|
-
cleanupWorktrees(setup);
|
|
249
|
+
const cleanup = cleanupWorktrees(setup);
|
|
250
250
|
setup = undefined;
|
|
251
|
+
assert.equal(cleanup.state, "complete");
|
|
252
|
+
assert.equal(cleanup.pruned, true);
|
|
253
|
+
assert.equal(cleanup.tasks.length, 2);
|
|
254
|
+
assert.ok(cleanup.tasks.every((task) => task.worktreeRemoved && task.branchRemoved));
|
|
251
255
|
|
|
252
256
|
for (const worktreePath of worktreePaths) {
|
|
253
257
|
assert.equal(fs.existsSync(worktreePath), false, `worktree path still exists: ${worktreePath}`);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
|
|
6
|
+
const readProjectFile = (file: string): string => readFileSync(join(process.cwd(), file), "utf-8");
|
|
7
|
+
|
|
8
|
+
describe("writer budget guidance", () => {
|
|
9
|
+
it("keeps hard turn and tool caps off mutation-capable workers", () => {
|
|
10
|
+
const readme = readProjectFile("README.md");
|
|
11
|
+
const skill = readProjectFile("skills/pi-subagents/SKILL.md");
|
|
12
|
+
const reviewLoop = readProjectFile("prompts/review-loop.md");
|
|
13
|
+
|
|
14
|
+
for (const text of [readme, skill, reviewLoop]) {
|
|
15
|
+
assert.match(text, /As a conservative orchestration policy, do not (?:pass|set) `turnBudget` or a hard `toolBudget`/);
|
|
16
|
+
assert.match(text, /default tool budget blocks read\/search tools rather than mutation tools/i);
|
|
17
|
+
assert.match(text, /checkpoint after the current tool returns/);
|
|
18
|
+
assert.match(text, /changed files/);
|
|
19
|
+
assert.match(text, /build\/test state/);
|
|
20
|
+
assert.match(text, /commit or PR state/);
|
|
21
|
+
}
|
|
22
|
+
assert.match(readme, /elapsed timeout is not a mutation-safe boundary/i);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("documents deferred hard-limit termination at tool-using boundaries", () => {
|
|
26
|
+
const readme = readProjectFile("README.md");
|
|
27
|
+
assert.match(readme, /termination occurs at the next assistant boundary/);
|
|
28
|
+
assert.match(readme, /`termination-deferred`/);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getOptionsFormatError, normalizeOptions } from "./helpers.ts";
|
|
2
|
+
import { resolveShortcuts } from "./shortcuts.ts";
|
|
3
|
+
import type { BatchedQuestion, RawBatchQuestion } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export function prepareBatchQuestions(value: unknown): { questions: BatchedQuestion[] } | { error: string } {
|
|
6
|
+
if (!Array.isArray(value) || value.length === 0) return { error: "questions must contain at least one question." };
|
|
7
|
+
|
|
8
|
+
const ids = new Set<string>();
|
|
9
|
+
const questions: BatchedQuestion[] = [];
|
|
10
|
+
for (let index = 0; index < value.length; index++) {
|
|
11
|
+
const raw = value[index] as RawBatchQuestion;
|
|
12
|
+
const question = raw?.question?.trim();
|
|
13
|
+
if (!question) return { error: `Question ${index + 1} must include a non-empty question.` };
|
|
14
|
+
|
|
15
|
+
const optionsError = getOptionsFormatError(raw.options);
|
|
16
|
+
if (optionsError) return { error: `Question ${index + 1}: ${optionsError}` };
|
|
17
|
+
const options = normalizeOptions(raw.options);
|
|
18
|
+
const allowFreeform = raw.allowFreeform ?? options.length === 0;
|
|
19
|
+
if (options.length === 0 && !allowFreeform) {
|
|
20
|
+
return { error: `Question ${index + 1}: options are empty and allowFreeform is false.` };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const id = raw.id?.trim() || `q${index + 1}`;
|
|
24
|
+
if (ids.has(id)) return { error: `Question ids must be unique; duplicate id: ${id}` };
|
|
25
|
+
ids.add(id);
|
|
26
|
+
questions.push({
|
|
27
|
+
id,
|
|
28
|
+
label: raw.label?.trim() || `Q${index + 1}`,
|
|
29
|
+
question,
|
|
30
|
+
context: raw.context?.trim() || raw.title?.trim() || undefined,
|
|
31
|
+
options,
|
|
32
|
+
allowMultiple: raw.allowMultiple ?? false,
|
|
33
|
+
allowFreeform,
|
|
34
|
+
allowComment: raw.allowComment ?? false,
|
|
35
|
+
shortcuts: resolveShortcuts(raw.commentToggleKey, process.env.PI_QUESTION_COMMENT_TOGGLE_KEY),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return { questions };
|
|
39
|
+
}
|