@selesai/code 0.13.12 → 0.13.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +77 -1
- package/dist/extensions/pi-subagents/VISION.md +12 -1
- package/dist/extensions/pi-subagents/docs/agents.md +13 -6
- package/dist/extensions/pi-subagents/docs/configuration.md +35 -9
- package/dist/extensions/pi-subagents/docs/extension-api.md +1 -1
- package/dist/extensions/pi-subagents/docs/missions.md +1 -1
- package/dist/extensions/pi-subagents/docs/models.md +6 -6
- package/dist/extensions/pi-subagents/docs/observability.md +6 -3
- package/dist/extensions/pi-subagents/docs/tool-reference.md +3 -3
- package/dist/extensions/pi-subagents/docs/watchdog.md +92 -114
- package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
- package/dist/extensions/pi-subagents/install.mjs +1 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +6 -6
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +4 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -4
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +3 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +120 -124
- package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +5 -1
- package/dist/extensions/pi-subagents/src/api/preflight.ts +4 -0
- package/dist/extensions/pi-subagents/src/api/shared-types.ts +3 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +20 -0
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +6 -2
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +1 -1
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +19 -13
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +26 -8
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +103 -23
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +6 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +54 -3
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +22 -2
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +63 -6
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -12
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +13 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +21 -8
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +59 -6
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +95 -18
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +11 -43
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -0
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +24 -3
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -6
- package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +1 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +33 -18
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +73 -37
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +33 -6
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +4 -2
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +20 -3
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -7
- package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +467 -63
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +3 -1
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +0 -12
- package/dist/extensions/pi-subagents/src/shared/fork-session-cwd.ts +27 -0
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +3 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +42 -4
- package/dist/extensions/pi-subagents/src/shared/utils.ts +18 -7
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
- package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +26 -12
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +61 -2
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +12 -7
- package/dist/extensions/pi-subagents/src/tui/render.ts +227 -14
- package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +56 -34
- package/dist/extensions/pi-subagents/src/watchdog/diff-tool.ts +77 -0
- package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +5 -3
- package/dist/extensions/pi-subagents/src/watchdog/guidance.ts +20 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +16 -13
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +10 -9
- package/dist/extensions/pi-subagents/src/watchdog/render.ts +4 -5
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +15 -4
- package/dist/extensions/pi-subagents/src/watchdog/rules.ts +70 -0
- package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +75 -92
- package/dist/extensions/pi-subagents/src/watchdog/scope.ts +2 -1
- package/dist/extensions/pi-subagents/src/watchdog/settings.ts +48 -104
- package/dist/extensions/pi-subagents/src/watchdog/types.ts +18 -32
- package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +0 -1
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -2
- package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +439 -0
- package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +28 -1
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +335 -21
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +4 -1
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +22 -18
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +0 -1
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +21 -18
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +307 -22
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/support/helpers.ts +25 -0
- package/dist/extensions/pi-subagents/test/support/isolated-temp-root.mjs +15 -4
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -2
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +96 -15
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +53 -5
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +221 -8
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +45 -24
- package/dist/extensions/pi-subagents/test/unit/agent-scan-dirs.test.ts +133 -0
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +28 -2
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +4 -1
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +18 -4
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +16 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +38 -0
- package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +19 -6
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +22 -1
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +2 -6
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +66 -1
- package/dist/extensions/pi-subagents/test/unit/pi-args-permission-system.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +116 -10
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +136 -18
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/project-panes-public-api.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +127 -48
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +17 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -2
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +9 -9
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +9 -3
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +22 -9
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +91 -8
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +0 -1
- package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -2
- package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +73 -13
- package/dist/extensions/pi-subagents/test/unit/watchdog-diff-tool.test.ts +79 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-permission-arbiter.test.ts +2 -4
- package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +7 -8
- package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +48 -3
- package/dist/extensions/pi-subagents/test/unit/watchdog-rules.test.ts +37 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +36 -118
- package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +37 -14
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +45 -26
- package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +223 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +50 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +136 -4
- package/dist/extensions/pi-zentui/README.md +19 -5
- package/dist/extensions/pi-zentui/docs/configuration.md +37 -9
- package/dist/extensions/pi-zentui/extensions/zentui/config.ts +29 -0
- package/dist/extensions/pi-zentui/extensions/zentui/index.ts +31 -2
- package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +55 -11
- package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +115 -3
- package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +119 -2
- package/dist/extensions/pi-zentui/extensions/zentui/thinking-experimental.ts +1768 -0
- package/dist/extensions/pi-zentui/extensions/zentui/thinking-status.ts +69 -0
- package/dist/extensions/pi-zentui/extensions/zentui/thinking-steps.ts +155 -0
- package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +2 -1
- package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +16 -2
- package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
- package/dist/extensions/pi-zentui/test/config-load-lifecycle.test.ts +124 -0
- package/dist/extensions/pi-zentui/test/config.test.ts +82 -2
- package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +19 -4
- package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +58 -9
- package/dist/extensions/pi-zentui/test/package-contents.mjs +33 -0
- package/dist/extensions/pi-zentui/test/prototype-patch-registry.test.ts +131 -0
- package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +4 -4
- package/dist/extensions/pi-zentui/test/settings-command.test.ts +289 -7
- package/dist/extensions/pi-zentui/test/settings-previews.test.ts +106 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-docs.test.ts +51 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-loader-diagnostics.mjs +54 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-missing-export.test.ts +44 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-rows.test.ts +297 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-tui.mjs +2329 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental.test.ts +1668 -0
- package/dist/extensions/pi-zentui/test/thinking-status.test.ts +85 -0
- package/dist/extensions/pi-zentui/test/thinking-steps.test.ts +122 -0
- package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +58 -0
- package/dist/skills/brandkit/SKILL.md +0 -2
- package/dist/skills/{industrial-brutalist-ui → brutalist}/SKILL.md +1 -3
- package/dist/skills/gpt-taste/SKILL.md +0 -2
- package/dist/skills/image-to-code/SKILL.md +0 -2
- package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -2
- package/dist/skills/imagegen-frontend-web/SKILL.md +0 -2
- package/dist/skills/{minimalist-ui → minimalist}/SKILL.md +1 -3
- package/dist/skills/{full-output-enforcement → output}/SKILL.md +1 -3
- package/dist/skills/{redesign-existing-projects → redesign}/SKILL.md +1 -3
- package/dist/skills/{high-end-visual-design → soft}/SKILL.md +1 -3
- package/dist/skills/stitch/DESIGN.md +121 -0
- package/dist/skills/{stitch-design-taste → stitch}/SKILL.md +1 -3
- package/dist/skills/{design-taste-frontend → taste}/SKILL.md +1 -3
- package/dist/skills/{design-taste-frontend-v1 → taste-v1}/SKILL.md +2 -4
- package/package.json +1 -1
|
@@ -17,7 +17,6 @@ describe("watchdog warning formatting and rendering", () => {
|
|
|
17
17
|
agent: "main",
|
|
18
18
|
runId: "run-1",
|
|
19
19
|
stale: true,
|
|
20
|
-
autoFollowAttempt: 2,
|
|
21
20
|
});
|
|
22
21
|
|
|
23
22
|
assert.match(content, /^<subagent_watchdog severity="blocker" category="correctness" source="main" guidance="weigh, don't blindly obey">/);
|
|
@@ -28,7 +27,7 @@ describe("watchdog warning formatting and rendering", () => {
|
|
|
28
27
|
assert.match(content, /<agent>main<\/agent>/);
|
|
29
28
|
assert.match(content, /<run_id>run-1<\/run_id>/);
|
|
30
29
|
assert.match(content, /<stale>true<\/stale>/);
|
|
31
|
-
assert.
|
|
30
|
+
assert.doesNotMatch(content, /auto_follow/);
|
|
32
31
|
assert.match(content, /<blocker_guidance>/);
|
|
33
32
|
});
|
|
34
33
|
|
|
@@ -54,8 +53,8 @@ describe("watchdog warning formatting and rendering", () => {
|
|
|
54
53
|
category: "loop-risk",
|
|
55
54
|
source: "main",
|
|
56
55
|
summary: "Repeated blocker",
|
|
57
|
-
evidence: "The same issue survived another
|
|
58
|
-
recommendedAction: "
|
|
56
|
+
evidence: "The same issue survived another continuation.",
|
|
57
|
+
recommendedAction: "Ask for user review.",
|
|
59
58
|
state: "stalemate",
|
|
60
59
|
stalemateRepeats: 3,
|
|
61
60
|
};
|
|
@@ -65,10 +64,10 @@ describe("watchdog warning formatting and rendering", () => {
|
|
|
65
64
|
const failed = formatWatchdogWarningRenderText({ ...base, state: "failed", error: "provider aborted" });
|
|
66
65
|
const displayed = formatWatchdogWarningRenderText({ ...base, state: "displayed" });
|
|
67
66
|
|
|
68
|
-
assert.match(stalemate, /Subagent watchdog Blocker \(stalemate
|
|
69
|
-
assert.match(stalemate, /
|
|
70
|
-
assert.match(stale, /Subagent watchdog Concern \(stale
|
|
71
|
-
assert.match(stale, /
|
|
67
|
+
assert.match(stalemate, /Subagent watchdog Blocker \(stalemate\): Repeated blocker/);
|
|
68
|
+
assert.match(stalemate, /Same warning 3 times in a row; the watchdog stopped continuing the run\./);
|
|
69
|
+
assert.match(stale, /Subagent watchdog Concern \(stale\): Repeated blocker/);
|
|
70
|
+
assert.match(stale, /arrived after the watchdog catch-up timeout\./);
|
|
72
71
|
assert.match(failed, /failed review/);
|
|
73
72
|
assert.match(failed, /Failure: provider aborted/);
|
|
74
73
|
assert.match(displayed, /Subagent watchdog Blocker \(displayed\): Repeated blocker/);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
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";
|
|
2
5
|
import { describe, it } from "node:test";
|
|
3
6
|
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
|
7
|
+
import { WATCHDOG_GUIDANCE_MAX_CHARS } from "../../src/watchdog/guidance.ts";
|
|
4
8
|
import {
|
|
5
9
|
createAssistantMessageEventStream,
|
|
6
10
|
fauxAssistantMessage,
|
|
@@ -35,14 +39,11 @@ function cloneConfig(): ResolvedWatchdogConfig {
|
|
|
35
39
|
return {
|
|
36
40
|
...DEFAULT_WATCHDOG_CONFIG,
|
|
37
41
|
guidance: { ...DEFAULT_WATCHDOG_CONFIG.guidance },
|
|
38
|
-
autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.autoFollow },
|
|
39
42
|
main: { ...DEFAULT_WATCHDOG_CONFIG.main },
|
|
40
43
|
children: {
|
|
41
44
|
...DEFAULT_WATCHDOG_CONFIG.children,
|
|
42
|
-
autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.children.autoFollow },
|
|
43
45
|
overrides: { ...DEFAULT_WATCHDOG_CONFIG.children.overrides },
|
|
44
46
|
},
|
|
45
|
-
asyncCompletion: { ...DEFAULT_WATCHDOG_CONFIG.asyncCompletion },
|
|
46
47
|
lsp: { ...DEFAULT_WATCHDOG_CONFIG.lsp },
|
|
47
48
|
};
|
|
48
49
|
}
|
|
@@ -240,6 +241,50 @@ describe("main watchdog review adapter", () => {
|
|
|
240
241
|
assert.equal(result?.stopReason, "aborted");
|
|
241
242
|
});
|
|
242
243
|
|
|
244
|
+
it("includes WATCHDOG.md guidance in the system prompt", async () => {
|
|
245
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "watchdog-review-guidance-"));
|
|
246
|
+
const originalAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
247
|
+
try {
|
|
248
|
+
process.env.SELESAI_CODING_AGENT_DIR = path.join(dir, "agent");
|
|
249
|
+
fs.mkdirSync(path.join(dir, "project", ".selesai"), { recursive: true });
|
|
250
|
+
fs.writeFileSync(path.join(dir, "project", ".selesai", "WATCHDOG.md"), "Never accept skipped tests.\n", "utf-8");
|
|
251
|
+
fs.mkdirSync(path.join(dir, "agent"), { recursive: true });
|
|
252
|
+
fs.writeFileSync(path.join(dir, "agent", "WATCHDOG.md"), "u".repeat(WATCHDOG_GUIDANCE_MAX_CHARS), "utf-8");
|
|
253
|
+
const current = model("openai", "gpt-guidance");
|
|
254
|
+
const ctx = { ...(createCtx({ current }) as object), cwd: path.join(dir, "project") } as never;
|
|
255
|
+
const { streamFn, calls } = createStreamFn([fauxAssistantMessage("done", { stopReason: "stop" })]);
|
|
256
|
+
|
|
257
|
+
await createMainWatchdogReview(ctx, { streamFn })(request(enabledConfig(), []));
|
|
258
|
+
const prompt = String(calls[0]?.context.systemPrompt ?? "");
|
|
259
|
+
assert.match(prompt, /Standing instructions from WATCHDOG\.md \(project first, then user\):\nNever accept skipped tests\.\n\nu+$/);
|
|
260
|
+
assert.equal(prompt.split("(project first, then user):\n")[1]?.length, WATCHDOG_GUIDANCE_MAX_CHARS, "combined guidance is capped from the head");
|
|
261
|
+
|
|
262
|
+
const disabled = enabledConfig();
|
|
263
|
+
disabled.guidance = { watchdogMd: false };
|
|
264
|
+
const second = createStreamFn([fauxAssistantMessage("done", { stopReason: "stop" })]);
|
|
265
|
+
await createMainWatchdogReview(ctx, { streamFn: second.streamFn })(request(disabled, []));
|
|
266
|
+
assert.doesNotMatch(String(second.calls[0]?.context.systemPrompt ?? ""), /Standing instructions/);
|
|
267
|
+
} finally {
|
|
268
|
+
if (originalAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
269
|
+
else process.env.SELESAI_CODING_AGENT_DIR = originalAgentDir;
|
|
270
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("offers watchdog_diff only when a repo baseline is available", async () => {
|
|
275
|
+
const current = model("openai", "gpt-diff");
|
|
276
|
+
const ctx = createCtx({ current });
|
|
277
|
+
const withBaseline = createStreamFn([fauxAssistantMessage("done", { stopReason: "stop" })]);
|
|
278
|
+
await createMainWatchdogReview(ctx, { streamFn: withBaseline.streamFn, diffBaseline: () => ({ root: "/tmp/watchdog-review", ref: "abc123" }) })(request(enabledConfig(), []));
|
|
279
|
+
assert.deepEqual(withBaseline.calls[0]?.context.tools?.map((tool) => tool.name).sort(), ["find", "grep", "ls", "read", "watchdog_diff", "watchdog_warn"]);
|
|
280
|
+
assert.match(String(withBaseline.calls[0]?.context.systemPrompt ?? ""), /watchdog_diff/);
|
|
281
|
+
|
|
282
|
+
const without = createStreamFn([fauxAssistantMessage("done", { stopReason: "stop" })]);
|
|
283
|
+
await createMainWatchdogReview(ctx, { streamFn: without.streamFn, diffBaseline: () => undefined })(request(enabledConfig(), []));
|
|
284
|
+
assert.deepEqual(without.calls[0]?.context.tools?.map((tool) => tool.name).sort(), ["find", "grep", "ls", "read", "watchdog_warn"]);
|
|
285
|
+
assert.doesNotMatch(String(without.calls[0]?.context.systemPrompt ?? ""), /watchdog_diff/);
|
|
286
|
+
});
|
|
287
|
+
|
|
243
288
|
it("does not expose mutating tools to the watchdog agent", async () => {
|
|
244
289
|
const current = model("openai", "gpt-readonly");
|
|
245
290
|
const ctx = createCtx({ current });
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { evaluateLaunchRule, ruleViolationWarning, watchdogGlobMatch } from "../../src/watchdog/rules.ts";
|
|
4
|
+
import type { WatchdogRulesConfig } from "../../src/watchdog/types.ts";
|
|
5
|
+
|
|
6
|
+
describe("watchdog launch rules", () => {
|
|
7
|
+
it("matches globs anchored and case-sensitively", () => {
|
|
8
|
+
assert.equal(watchdogGlobMatch("openai-codex/gpt-5.6-*:max", "openai-codex/gpt-5.6-luna:max"), true);
|
|
9
|
+
assert.equal(watchdogGlobMatch("openai-codex/gpt-5.6-*", "openai-codex/gpt-5.6-luna:max"), true);
|
|
10
|
+
assert.equal(watchdogGlobMatch("gpt-5.6-*", "openai-codex/gpt-5.6-luna"), false, "patterns are anchored at the start");
|
|
11
|
+
assert.equal(watchdogGlobMatch("*", "anything/at-all"), true);
|
|
12
|
+
assert.equal(watchdogGlobMatch("anthropic/claude-opus-4.?", "anthropic/claude-opus-4.8"), true);
|
|
13
|
+
assert.equal(watchdogGlobMatch("anthropic/claude-opus-4.8", "anthropic/claude-opus-4x8"), false);
|
|
14
|
+
assert.equal(watchdogGlobMatch("Anthropic/*", "anthropic/claude"), false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("applies deny before allow, matches the base model when the suffix differs, and shapes a concern", () => {
|
|
18
|
+
const config: WatchdogRulesConfig = { action: "warn", roleModels: { scout: { allow: ["openai-codex/gpt-5.6-luna"], deny: ["openai-codex/gpt-5.6-sol*"], note: "scout is cheap" } } };
|
|
19
|
+
assert.equal(evaluateLaunchRule(config, "scout", "openai-codex/gpt-5.6-luna:max"), undefined);
|
|
20
|
+
const denied = evaluateLaunchRule(config, "scout", "openai-codex/gpt-5.6-sol:high");
|
|
21
|
+
assert.match(denied?.summary ?? "", /denied model 'openai-codex\/gpt-5\.6-sol:high'/);
|
|
22
|
+
assert.match(denied?.evidence ?? "", /scout is cheap/);
|
|
23
|
+
const outside = evaluateLaunchRule(config, "scout", "anthropic/claude-opus-4-8");
|
|
24
|
+
assert.match(outside?.summary ?? "", /not in its allowed list/);
|
|
25
|
+
assert.match(outside?.recommendedAction ?? "", /openai-codex\/gpt-5\.6-luna/);
|
|
26
|
+
assert.equal(evaluateLaunchRule(config, "worker", "anthropic/claude-opus-4-8"), undefined, "other roles are unaffected");
|
|
27
|
+
assert.equal(evaluateLaunchRule(config, "scout", undefined), undefined, "an unknown model cannot be judged");
|
|
28
|
+
assert.equal(evaluateLaunchRule(undefined, "scout", "x/y"), undefined);
|
|
29
|
+
|
|
30
|
+
const warning = ruleViolationWarning(denied!);
|
|
31
|
+
assert.equal(warning.severity, "concern");
|
|
32
|
+
assert.equal(warning.category, "missed-constraint");
|
|
33
|
+
assert.equal(warning.confidence, "high");
|
|
34
|
+
assert.equal(warning.source, "main");
|
|
35
|
+
assert.equal(warning.agent, "scout");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -6,22 +6,19 @@ import * as path from "node:path";
|
|
|
6
6
|
import { describe, it } from "node:test";
|
|
7
7
|
import { DEFAULT_WATCHDOG_CONFIG } from "../../src/watchdog/settings.ts";
|
|
8
8
|
import { MainWatchdogRuntime, type WatchdogReviewFunction } from "../../src/watchdog/runtime.ts";
|
|
9
|
-
import type { ResolvedWatchdogConfig, WatchdogLspResult, WatchdogSettingsResult, WatchdogWarning } from "../../src/watchdog/types.ts";
|
|
9
|
+
import type { ResolvedWatchdogConfig, WatchdogLspResult, WatchdogSettingsResult, WatchdogWarning, WatchdogWarningDetails } from "../../src/watchdog/types.ts";
|
|
10
10
|
|
|
11
11
|
function cloneConfig(): ResolvedWatchdogConfig {
|
|
12
12
|
return {
|
|
13
13
|
...DEFAULT_WATCHDOG_CONFIG,
|
|
14
14
|
guidance: { ...DEFAULT_WATCHDOG_CONFIG.guidance },
|
|
15
|
-
autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.autoFollow },
|
|
16
15
|
scope: { ...DEFAULT_WATCHDOG_CONFIG.scope },
|
|
17
16
|
cadence: { ...DEFAULT_WATCHDOG_CONFIG.cadence },
|
|
18
17
|
main: { ...DEFAULT_WATCHDOG_CONFIG.main },
|
|
19
18
|
children: {
|
|
20
19
|
...DEFAULT_WATCHDOG_CONFIG.children,
|
|
21
|
-
autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.children.autoFollow },
|
|
22
20
|
overrides: { ...DEFAULT_WATCHDOG_CONFIG.children.overrides },
|
|
23
21
|
},
|
|
24
|
-
asyncCompletion: { ...DEFAULT_WATCHDOG_CONFIG.asyncCompletion },
|
|
25
22
|
lsp: { ...DEFAULT_WATCHDOG_CONFIG.lsp },
|
|
26
23
|
};
|
|
27
24
|
}
|
|
@@ -40,7 +37,6 @@ function enabledConfig(overrides: Partial<ResolvedWatchdogConfig> = {}): Resolve
|
|
|
40
37
|
if (overrides.lsp) config.lsp = { ...config.lsp, ...overrides.lsp };
|
|
41
38
|
if (overrides.scope) config.scope = { ...config.scope, ...overrides.scope };
|
|
42
39
|
if (overrides.cadence) config.cadence = { ...config.cadence, ...overrides.cadence };
|
|
43
|
-
if (overrides.autoFollow) config.autoFollow = { ...config.autoFollow, ...overrides.autoFollow };
|
|
44
40
|
return config;
|
|
45
41
|
}
|
|
46
42
|
|
|
@@ -173,7 +169,6 @@ describe("main watchdog runtime", () => {
|
|
|
173
169
|
assert.equal(snapshot.lastWarning?.state, "displayed");
|
|
174
170
|
assert.equal(displayed.length, 1);
|
|
175
171
|
assert.equal((displayed[0] as { state?: string }).state, "displayed");
|
|
176
|
-
assert.equal(snapshot.autoFollowQueued, false);
|
|
177
172
|
});
|
|
178
173
|
|
|
179
174
|
it("drops stale async warning callbacks after reset", async () => {
|
|
@@ -205,7 +200,7 @@ describe("main watchdog runtime", () => {
|
|
|
205
200
|
assert.equal(snapshot.lastWarning, undefined);
|
|
206
201
|
});
|
|
207
202
|
|
|
208
|
-
it("treats failed review stop reasons as failed
|
|
203
|
+
it("treats failed review stop reasons as failed", async () => {
|
|
209
204
|
const runtime = new MainWatchdogRuntime({
|
|
210
205
|
resolveConfig: () => configResult(enabledConfig()),
|
|
211
206
|
review: () => ({ stopReason: "length" }),
|
|
@@ -218,7 +213,6 @@ describe("main watchdog runtime", () => {
|
|
|
218
213
|
assert.equal(snapshot.status, "failed");
|
|
219
214
|
assert.match(snapshot.lastError ?? "", /stop reason 'length'/);
|
|
220
215
|
assert.equal(snapshot.failedReviews, 1);
|
|
221
|
-
assert.equal(snapshot.autoFollowQueued, false);
|
|
222
216
|
});
|
|
223
217
|
|
|
224
218
|
it("marks unresolved agent-end review work stale on timeout", async () => {
|
|
@@ -240,7 +234,6 @@ describe("main watchdog runtime", () => {
|
|
|
240
234
|
const snapshot = runtime.getSnapshot();
|
|
241
235
|
assert.equal(snapshot.status, "stale");
|
|
242
236
|
assert.equal(snapshot.staleReviews, 1);
|
|
243
|
-
assert.equal(snapshot.autoFollowQueued, false);
|
|
244
237
|
runtime.reset("cleanup");
|
|
245
238
|
});
|
|
246
239
|
|
|
@@ -303,7 +296,6 @@ describe("main watchdog runtime", () => {
|
|
|
303
296
|
const snapshot = runtime.getSnapshot();
|
|
304
297
|
assert.equal(snapshot.status, "stale");
|
|
305
298
|
assert.equal(snapshot.lastWarning, undefined);
|
|
306
|
-
assert.equal(snapshot.autoFollowQueued, false);
|
|
307
299
|
});
|
|
308
300
|
|
|
309
301
|
it("invalidates an in-flight review when refreshed config disables the watchdog", async () => {
|
|
@@ -350,8 +342,9 @@ describe("main watchdog runtime", () => {
|
|
|
350
342
|
runtime.enqueueDelta(`Assistant:\n${"x".repeat(30_000)}`);
|
|
351
343
|
await runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
352
344
|
|
|
353
|
-
assert.equal(reviewedDelta.length, 24_000);
|
|
354
|
-
assert.match(reviewedDelta, /^
|
|
345
|
+
assert.equal(reviewedDelta.length, 24_000, "head + marker + tail fill the cap exactly");
|
|
346
|
+
assert.match(reviewedDelta, /^Assistant:\nx+\n\n\[\.\.\. about \d+ characters omitted \.\.\.\]\n\nx+$/);
|
|
347
|
+
assert.equal(reviewedDelta.indexOf("\n\n[..."), 6_000, "the first 6,000 characters are kept as the head");
|
|
355
348
|
});
|
|
356
349
|
|
|
357
350
|
it("does not record duplicate signatures for stale invalidated reviews", async () => {
|
|
@@ -879,73 +872,6 @@ describe("main watchdog runtime", () => {
|
|
|
879
872
|
assert.deepEqual((delivered[0] as { options?: unknown }).options, { deliverAs: "steer" });
|
|
880
873
|
});
|
|
881
874
|
|
|
882
|
-
it("auto-follows agent-end blockers, respects max attempts, and real prompts reset attempts", async () => {
|
|
883
|
-
const sent: string[] = [];
|
|
884
|
-
const runtime = new MainWatchdogRuntime({
|
|
885
|
-
resolveConfig: () => configResult(enabledConfig({ autoFollow: { blockers: true, maxAttempts: 1, stalemateRepeats: 3 } })),
|
|
886
|
-
sendUserMessage: (message) => sent.push(message),
|
|
887
|
-
review: (request) => {
|
|
888
|
-
assert.equal(request.emitWarning({ ...warning(), severity: "blocker", summary: "Needs fixing" }), true);
|
|
889
|
-
return { stopReason: "stop" };
|
|
890
|
-
},
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
runtime.handleBeforeAgentStart({ prompt: "Patch carefully." }, { cwd: "/tmp/project" });
|
|
894
|
-
runtime.enqueueDelta("Assistant:\nBad patch.");
|
|
895
|
-
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
|
|
896
|
-
assert.equal(sent.length, 1);
|
|
897
|
-
assert.match(sent[0] ?? "", /Watchdog auto-follow/);
|
|
898
|
-
assert.equal(runtime.getSnapshot().autoFollowAttempts, 1);
|
|
899
|
-
|
|
900
|
-
runtime.handleBeforeAgentStart({ prompt: sent[0] }, { cwd: "/tmp/project" });
|
|
901
|
-
runtime.enqueueDelta("Assistant:\nStill bad.");
|
|
902
|
-
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
|
|
903
|
-
assert.equal(sent.length, 1);
|
|
904
|
-
|
|
905
|
-
runtime.handleBeforeAgentStart({ prompt: "Human says try again with a different edit." }, { cwd: "/tmp/project" });
|
|
906
|
-
runtime.enqueueDelta("Assistant:\nBad again in a different way.");
|
|
907
|
-
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
|
|
908
|
-
assert.equal(sent.length, 2);
|
|
909
|
-
assert.equal(runtime.getSnapshot().autoFollowAttempts, 1);
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
it("keeps a racing real prompt authoritative while a queued auto-follow is pending", async () => {
|
|
913
|
-
const sent: string[] = [];
|
|
914
|
-
let reviewedDelta = "";
|
|
915
|
-
let blockerSummary = "First blocker";
|
|
916
|
-
const runtime = new MainWatchdogRuntime({
|
|
917
|
-
resolveConfig: () => configResult(enabledConfig({ autoFollow: { blockers: true, maxAttempts: 1, stalemateRepeats: 5 } })),
|
|
918
|
-
sendUserMessage: (message) => sent.push(message),
|
|
919
|
-
review: (request) => {
|
|
920
|
-
reviewedDelta = request.delta;
|
|
921
|
-
assert.equal(request.emitWarning({ ...warning(), severity: "blocker", summary: blockerSummary }), true);
|
|
922
|
-
return { stopReason: "stop" };
|
|
923
|
-
},
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
runtime.handleBeforeAgentStart({ prompt: "Patch carefully." }, { cwd: "/tmp/project" });
|
|
927
|
-
runtime.enqueueDelta("Assistant:\nBad patch.");
|
|
928
|
-
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
|
|
929
|
-
assert.equal(sent.length, 1);
|
|
930
|
-
assert.equal(runtime.getSnapshot().autoFollowAttempts, 1);
|
|
931
|
-
|
|
932
|
-
// A real user prompt arrives before the queued auto-follow turn starts.
|
|
933
|
-
blockerSummary = "Second blocker";
|
|
934
|
-
runtime.handleBeforeAgentStart({ prompt: "Human: also update the docs." }, { cwd: "/tmp/project" });
|
|
935
|
-
runtime.enqueueDelta("Assistant:\nStill bad.");
|
|
936
|
-
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
|
|
937
|
-
assert.match(reviewedDelta, /also update the docs\./);
|
|
938
|
-
assert.equal(sent.length, 2, "real prompt resets attempts so a new blocker auto-follows again");
|
|
939
|
-
|
|
940
|
-
// The originally queued auto-follow prompt then arrives and stays excluded from scope.
|
|
941
|
-
blockerSummary = "Third blocker";
|
|
942
|
-
runtime.handleBeforeAgentStart({ prompt: sent[0] }, { cwd: "/tmp/project" });
|
|
943
|
-
runtime.enqueueDelta("Assistant:\nStill bad again.");
|
|
944
|
-
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
|
|
945
|
-
assert.doesNotMatch(reviewedDelta, /Watchdog auto-follow: address this blocker/);
|
|
946
|
-
assert.equal(sent.length, 2, "auto-follow turn must not reset the attempt counter");
|
|
947
|
-
});
|
|
948
|
-
|
|
949
875
|
it("cancels an in-flight cadence review so the agent-end boundary review still runs", async () => {
|
|
950
876
|
let releaseFirstReview!: () => void;
|
|
951
877
|
let reviewCalls = 0;
|
|
@@ -979,56 +905,48 @@ describe("main watchdog runtime", () => {
|
|
|
979
905
|
assert.equal(snapshot.status, "idle");
|
|
980
906
|
});
|
|
981
907
|
|
|
982
|
-
it("
|
|
983
|
-
const
|
|
908
|
+
it("marks the third consecutive identical boundary warning as stalemate and delivers it held", async () => {
|
|
909
|
+
const delivered: Array<{ warning: WatchdogWarningDetails; options?: unknown }> = [];
|
|
984
910
|
const runtime = new MainWatchdogRuntime({
|
|
985
|
-
resolveConfig: () => configResult(enabledConfig({
|
|
986
|
-
|
|
911
|
+
resolveConfig: () => configResult(enabledConfig({ stalemateRepeats: 3 })),
|
|
912
|
+
displayWarning: (details, options) => { delivered.push({ warning: details, options }); },
|
|
987
913
|
review: (request) => {
|
|
988
|
-
|
|
914
|
+
request.emitWarning({ ...warning(), severity: "blocker", summary: "Same blocker" });
|
|
989
915
|
return { stopReason: "stop" };
|
|
990
916
|
},
|
|
991
917
|
});
|
|
992
918
|
|
|
993
919
|
runtime.handleBeforeAgentStart({ prompt: "Patch carefully." }, { cwd: "/tmp/project" });
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
runtime.handleBeforeAgentStart({ prompt: sent[0] }, { cwd: "/tmp/project" });
|
|
999
|
-
runtime.enqueueDelta("Assistant:\nStill bad but not identical input.");
|
|
1000
|
-
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
|
|
920
|
+
for (const delta of ["Assistant:\nBad patch.", "Assistant:\nStill bad.", "Assistant:\nStill bad again."]) {
|
|
921
|
+
runtime.enqueueDelta(delta);
|
|
922
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
|
|
923
|
+
}
|
|
1001
924
|
|
|
1002
|
-
|
|
1003
|
-
assert.equal(
|
|
1004
|
-
assert.equal(
|
|
925
|
+
assert.equal(delivered.length, 3, "repeats keep nudging until the stalemate threshold");
|
|
926
|
+
assert.equal(delivered[0]?.options, undefined);
|
|
927
|
+
assert.equal(delivered[1]?.options, undefined);
|
|
928
|
+
assert.deepEqual(delivered[2]?.options, { triggerTurn: false });
|
|
929
|
+
assert.equal(delivered[2]?.warning.state, "stalemate");
|
|
930
|
+
assert.equal(delivered[2]?.warning.stalemateRepeats, 3);
|
|
931
|
+
let snapshot = runtime.getSnapshot();
|
|
932
|
+
assert.equal(snapshot.stalemate, true);
|
|
933
|
+
assert.equal(snapshot.boundaryRepeats, 3);
|
|
1005
934
|
assert.equal(snapshot.lastWarning?.state, "stalemate");
|
|
1006
|
-
});
|
|
1007
935
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const reviewStarted = new Promise<void>((resolve) => { started = resolve; });
|
|
1013
|
-
const sent: string[] = [];
|
|
1014
|
-
const runtime = new MainWatchdogRuntime({
|
|
1015
|
-
resolveConfig: () => configResult(enabledConfig({ agentEndTimeoutMs: 5 })),
|
|
1016
|
-
sendUserMessage: (message) => sent.push(message),
|
|
1017
|
-
review: async (request) => {
|
|
1018
|
-
emitWarning = request.emitWarning;
|
|
1019
|
-
started();
|
|
1020
|
-
await new Promise<void>((resolve) => { finishReview = resolve; });
|
|
1021
|
-
},
|
|
1022
|
-
});
|
|
936
|
+
// After stalemate the same finding is a plain duplicate again: no further delivery.
|
|
937
|
+
runtime.enqueueDelta("Assistant:\nStill bad, fourth time.");
|
|
938
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
|
|
939
|
+
assert.equal(delivered.length, 3);
|
|
1023
940
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
await
|
|
1028
|
-
assert.equal(
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
assert.equal(
|
|
941
|
+
// A real prompt resets the counter.
|
|
942
|
+
runtime.handleBeforeAgentStart({ prompt: "Human: try a different approach." }, { cwd: "/tmp/project" });
|
|
943
|
+
runtime.enqueueDelta("Assistant:\nNew attempt.");
|
|
944
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: "/tmp/project" });
|
|
945
|
+
assert.equal(delivered.length, 4);
|
|
946
|
+
assert.equal(delivered[3]?.options, undefined);
|
|
947
|
+
snapshot = runtime.getSnapshot();
|
|
948
|
+
assert.equal(snapshot.boundaryRepeats, 1);
|
|
949
|
+
assert.equal(snapshot.stalemate, false);
|
|
1032
950
|
});
|
|
1033
951
|
|
|
1034
952
|
it("session on/off overrides explicit main enabled settings", () => {
|
|
@@ -51,12 +51,10 @@ describe("watchdog settings", () => {
|
|
|
51
51
|
assert.equal(result.config.enabled, false);
|
|
52
52
|
assert.equal(result.config.main.enabled, false);
|
|
53
53
|
assert.equal(result.config.children.enabled, false);
|
|
54
|
-
assert.equal(result.config.
|
|
55
|
-
assert.equal(result.config.delivery, "held");
|
|
56
|
-
assert.equal(result.config.showDuringRun, false);
|
|
54
|
+
assert.equal(result.config.guidance.watchdogMd, true);
|
|
57
55
|
assert.equal(result.config.agentEndTimeoutMs, 30_000);
|
|
58
56
|
assert.equal(result.config.children.watchdogTailTimeoutMs, 120_000);
|
|
59
|
-
assert.equal(result.config.
|
|
57
|
+
assert.equal(result.config.stalemateRepeats, 3);
|
|
60
58
|
assert.deepEqual(result.config.scope, { enabled: true });
|
|
61
59
|
assert.deepEqual(result.config.cadence, { everyNTools: null });
|
|
62
60
|
assert.deepEqual(result.config.lsp, { enabled: true, timeoutMs: 3000, maxFiles: 20, maxDiagnostics: 50 });
|
|
@@ -83,14 +81,14 @@ describe("watchdog settings", () => {
|
|
|
83
81
|
subagents: {
|
|
84
82
|
watchdog: {
|
|
85
83
|
enabled: true,
|
|
86
|
-
|
|
84
|
+
stalemateRepeats: 5,
|
|
87
85
|
children: {
|
|
88
|
-
|
|
86
|
+
cadence: { everyNTools: 10 },
|
|
89
87
|
overrides: {
|
|
90
|
-
worker: { enabled: true, model: "anthropic/claude-test" },
|
|
88
|
+
worker: { enabled: true, model: "anthropic/claude-test", cadence: { everyNTools: 5 } },
|
|
91
89
|
},
|
|
92
90
|
},
|
|
93
|
-
guidance: {
|
|
91
|
+
guidance: { watchdogMd: false },
|
|
94
92
|
scope: { enabled: false },
|
|
95
93
|
cadence: { everyNTools: 10 },
|
|
96
94
|
},
|
|
@@ -99,11 +97,10 @@ describe("watchdog settings", () => {
|
|
|
99
97
|
writeJson(projectSettingsPath(), {
|
|
100
98
|
subagents: {
|
|
101
99
|
watchdog: {
|
|
102
|
-
|
|
100
|
+
stalemateRepeats: 2,
|
|
103
101
|
lsp: { enabled: false, timeoutMs: 1500, maxFiles: 4, maxDiagnostics: 7 },
|
|
104
102
|
main: { model: "openai/gpt-test" },
|
|
105
103
|
children: {
|
|
106
|
-
autoFollow: { blockers: false },
|
|
107
104
|
overrides: {
|
|
108
105
|
worker: { thinking: "medium" },
|
|
109
106
|
reviewer: { enabled: false },
|
|
@@ -115,7 +112,6 @@ describe("watchdog settings", () => {
|
|
|
115
112
|
|
|
116
113
|
const result = resolveWatchdogConfig(tempProject, {
|
|
117
114
|
session: {
|
|
118
|
-
autoFollow: { maxAttempts: null },
|
|
119
115
|
children: {
|
|
120
116
|
overrides: {
|
|
121
117
|
worker: { enabled: false },
|
|
@@ -127,16 +123,17 @@ describe("watchdog settings", () => {
|
|
|
127
123
|
assert.equal(result.ok, true);
|
|
128
124
|
assert.equal(result.config.main.enabled, true);
|
|
129
125
|
assert.equal(result.config.main.model, "openai/gpt-test");
|
|
130
|
-
assert.
|
|
131
|
-
assert.equal(result.config.guidance.
|
|
126
|
+
assert.equal(result.config.stalemateRepeats, 2);
|
|
127
|
+
assert.equal(result.config.guidance.watchdogMd, false);
|
|
132
128
|
assert.deepEqual(result.config.scope, { enabled: false });
|
|
133
129
|
assert.deepEqual(result.config.cadence, { everyNTools: 10 });
|
|
130
|
+
assert.deepEqual(result.config.children.cadence, { everyNTools: 10 });
|
|
134
131
|
assert.deepEqual(result.config.lsp, { enabled: false, timeoutMs: 1500, maxFiles: 4, maxDiagnostics: 7 });
|
|
135
|
-
assert.deepEqual(result.config.children.autoFollow, { blockers: false, maxAttempts: 4, stalemateRepeats: 3 });
|
|
136
132
|
assert.deepEqual(result.config.children.overrides.worker, {
|
|
137
133
|
enabled: false,
|
|
138
134
|
model: "anthropic/claude-test",
|
|
139
135
|
thinking: "medium",
|
|
136
|
+
cadence: { everyNTools: 5 },
|
|
140
137
|
});
|
|
141
138
|
assert.deepEqual(result.config.children.overrides.reviewer, { enabled: false });
|
|
142
139
|
});
|
|
@@ -178,6 +175,29 @@ describe("watchdog settings", () => {
|
|
|
178
175
|
);
|
|
179
176
|
});
|
|
180
177
|
|
|
178
|
+
it("parses launch rules and rejects malformed rule fields", () => {
|
|
179
|
+
const rules = {
|
|
180
|
+
action: "block",
|
|
181
|
+
roleModels: { scout: { allow: ["openai-codex/gpt-5.6-luna:max"] }, oracle: { deny: ["*"], note: "ask first" } },
|
|
182
|
+
} as const;
|
|
183
|
+
writeJson(userSettingsPath(), {
|
|
184
|
+
subagents: {
|
|
185
|
+
watchdog: { rules },
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
const result = resolveWatchdogConfig(tempProject);
|
|
189
|
+
assert.equal(result.ok, true, result.errors[0]?.message);
|
|
190
|
+
assert.deepEqual(result.config.rules, rules);
|
|
191
|
+
assert.equal(resolveWatchdogConfig(tempProject, { session: { enabled: true } }).config.rules?.action, "block");
|
|
192
|
+
|
|
193
|
+
writeJson(userSettingsPath(), { subagents: { watchdog: { rules: { action: "shout" } } } });
|
|
194
|
+
assert.match(resolveWatchdogConfig(tempProject).errors[0]?.message ?? "", /invalid 'subagents\.watchdog\.rules\.action'; expected 'warn' or 'block'/);
|
|
195
|
+
writeJson(userSettingsPath(), { subagents: { watchdog: { rules: { roleModels: { scout: { allowed: ["x"] } } } } } });
|
|
196
|
+
assert.match(resolveWatchdogConfig(tempProject).errors[0]?.message ?? "", /unknown field 'subagents\.watchdog\.rules\.roleModels\.scout\.allowed'/);
|
|
197
|
+
writeJson(userSettingsPath(), { subagents: { watchdog: {} } });
|
|
198
|
+
assert.equal(resolveWatchdogConfig(tempProject).config.rules, undefined);
|
|
199
|
+
});
|
|
200
|
+
|
|
181
201
|
it("rejects invalid scope and cadence config at settings load", () => {
|
|
182
202
|
writeJson(userSettingsPath(), {
|
|
183
203
|
subagents: {
|
|
@@ -202,6 +222,9 @@ describe("watchdog settings", () => {
|
|
|
202
222
|
result = resolveWatchdogConfig(tempProject);
|
|
203
223
|
assert.equal(result.ok, false);
|
|
204
224
|
assert.match(result.errors[0]?.message ?? "", /invalid 'subagents\.watchdog\.cadence\.everyNTools'/);
|
|
225
|
+
|
|
226
|
+
writeJson(userSettingsPath(), { subagents: { watchdog: { children: { cadence: { everyNTools: 4 } } } } });
|
|
227
|
+
assert.match(resolveWatchdogConfig(tempProject).errors[0]?.message ?? "", /invalid 'subagents\.watchdog\.children\.cadence\.everyNTools'/);
|
|
205
228
|
});
|
|
206
229
|
|
|
207
230
|
it("rejects invalid LSP config at settings load", () => {
|
|
@@ -297,7 +297,7 @@ describe("workflow chat progress rendering", () => {
|
|
|
297
297
|
assert.doesNotMatch(text, /failed\s+detaches/);
|
|
298
298
|
});
|
|
299
299
|
|
|
300
|
-
it("
|
|
300
|
+
it("keeps advisory preflight lanes in plan metadata instead of runtime rows", () => {
|
|
301
301
|
const preflight = {
|
|
302
302
|
version: 1 as const,
|
|
303
303
|
coverage: "partial" as const,
|
|
@@ -306,40 +306,57 @@ describe("workflow chat progress rendering", () => {
|
|
|
306
306
|
{ key: "review", mode: "review" as const, expectedOutput: "review.md" },
|
|
307
307
|
],
|
|
308
308
|
};
|
|
309
|
-
assert.deepEqual(buildWorkflowChatProgressRows([], preflight)
|
|
310
|
-
|
|
311
|
-
{
|
|
312
|
-
]);
|
|
313
|
-
const text = componentText(renderSubagentResult({
|
|
314
|
-
content: [{ type: "text", text: "Workflow running." }],
|
|
309
|
+
assert.deepEqual(buildWorkflowChatProgressRows([], preflight), []);
|
|
310
|
+
const result = {
|
|
311
|
+
content: [{ type: "text" as const, text: "Workflow running." }],
|
|
315
312
|
details: {
|
|
316
|
-
mode: "workflow",
|
|
313
|
+
mode: "workflow" as const,
|
|
317
314
|
runId: "wf_planned",
|
|
318
315
|
results: [],
|
|
319
316
|
preflight,
|
|
320
|
-
chatProgress: { mode: "live-card", repoRelation: "same", repoLabel: "pi-subagents" },
|
|
317
|
+
chatProgress: { mode: "live-card" as const, repoRelation: "same" as const, repoLabel: "pi-subagents" },
|
|
321
318
|
workflow: { trace: [], emits: [], console: [] },
|
|
322
319
|
},
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
assert.match(text, /
|
|
320
|
+
};
|
|
321
|
+
const text = componentText(renderSubagentResult(result, { expanded: false }, theme as any));
|
|
322
|
+
assert.match(text, /Plan: 2 lanes · writer, review/);
|
|
323
|
+
assert.match(text, /waiting for workflow child launches/);
|
|
324
|
+
assert.doesNotMatch(text, /planned\s+writer/);
|
|
325
|
+
assert.doesNotMatch(text, /planned\s+review/);
|
|
326
326
|
assert.doesNotMatch(text, /mode:mutation/);
|
|
327
327
|
assert.doesNotMatch(text, /expected:review\.md/);
|
|
328
|
-
assert.doesNotMatch(text, /waiting for workflow child launches/);
|
|
329
328
|
|
|
330
|
-
const expanded = componentText(renderSubagentResult({
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
},
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
assert.
|
|
329
|
+
const expanded = componentText(renderSubagentResult(result, { expanded: true }, theme as any));
|
|
330
|
+
assert.match(expanded, /Plan: 2 lanes · writer, review/);
|
|
331
|
+
assert.doesNotMatch(expanded, /planned\s+writer/);
|
|
332
|
+
assert.doesNotMatch(expanded, /planned\s+review/);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("annotates an authoritative dotted child without adding its declared root", () => {
|
|
336
|
+
const preflight = { version: 1 as const, coverage: "partial" as const, lanes: [{ key: "pr14", mode: "review" as const }] };
|
|
337
|
+
const rows = buildWorkflowChatProgressRows([
|
|
338
|
+
{ operation: "run", key: "pr14.quality", generatedLaneKey: "pr14", state: "started", agent: "reviewer" },
|
|
339
|
+
], preflight);
|
|
340
|
+
|
|
341
|
+
assert.deepEqual(rows.map((row) => ({ key: row.key, state: row.state, mode: row.preflight?.mode })), [
|
|
342
|
+
{ key: "pr14.quality", state: "running", mode: "review" },
|
|
343
|
+
]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("prefers a specific preflight lane over an earlier broad generated alias", () => {
|
|
347
|
+
const preflight = {
|
|
348
|
+
version: 1 as const,
|
|
349
|
+
coverage: "partial" as const,
|
|
350
|
+
lanes: [
|
|
351
|
+
{ key: "writer", mode: "mutation" as const },
|
|
352
|
+
{ key: "writer.quality", mode: "review" as const },
|
|
353
|
+
],
|
|
354
|
+
};
|
|
355
|
+
const rows = buildWorkflowChatProgressRows([
|
|
356
|
+
{ operation: "run", key: "writer.quality.deep", generatedLaneKey: "writer", state: "started", agent: "reviewer" },
|
|
357
|
+
], preflight);
|
|
358
|
+
|
|
359
|
+
assert.equal(rows[0]?.preflight?.mode, "review");
|
|
343
360
|
});
|
|
344
361
|
|
|
345
362
|
it("uses the compact plan preview for collapsed workflow launch output", () => {
|
|
@@ -434,6 +451,7 @@ describe("workflow chat progress rendering", () => {
|
|
|
434
451
|
key: `step-${index}`,
|
|
435
452
|
state: "started" as const,
|
|
436
453
|
label: `review ${index}`,
|
|
454
|
+
phase: `phase-${index}`,
|
|
437
455
|
}));
|
|
438
456
|
const result = {
|
|
439
457
|
content: [{ type: "text" as const, text: "Workflow running." }],
|
|
@@ -454,6 +472,7 @@ describe("workflow chat progress rendering", () => {
|
|
|
454
472
|
const expanded = renderSubagentResult(result, { expanded: true }, theme as any, undefined, { horizontalSpacing: 0, compactResultMaxLines: 3 }).render(120);
|
|
455
473
|
assert.ok(expanded.length > 3);
|
|
456
474
|
assert.match(expanded[1]!, /^ Repo pi-subagents\s*$/);
|
|
475
|
+
assert.match(expanded.join("\n"), /phase-9 1 active/);
|
|
457
476
|
assert.doesNotMatch(expanded.join("\n"), /rows hidden · .* expands/);
|
|
458
477
|
});
|
|
459
478
|
|