pi-subagents 0.63.0 → 0.64.0
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 +24 -1
- package/docs/watchdog.md +92 -114
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +4 -3
- package/src/runs/background/async-execution.ts +3 -0
- package/src/runs/background/notify.ts +41 -2
- package/src/runs/background/subagent-runner.ts +23 -1
- package/src/runs/foreground/execution.ts +7 -1
- package/src/runs/foreground/subagent-executor.ts +21 -1
- package/src/runs/shared/acceptance.ts +10 -0
- package/src/runs/shared/async-status-projection.ts +11 -43
- package/src/runs/shared/pi-args.ts +1 -4
- package/src/runs/shared/subagent-control.ts +4 -2
- package/src/shared/types.ts +9 -2
- package/src/tui/render.ts +5 -0
- package/src/watchdog/child-status.ts +54 -33
- package/src/watchdog/diff-tool.ts +77 -0
- package/src/watchdog/emission-guard.ts +5 -3
- package/src/watchdog/guidance.ts +20 -0
- package/src/watchdog/register-child.ts +16 -13
- package/src/watchdog/register-main.ts +10 -9
- package/src/watchdog/render.ts +4 -5
- package/src/watchdog/review.ts +15 -4
- package/src/watchdog/rules.ts +70 -0
- package/src/watchdog/runtime.ts +75 -92
- package/src/watchdog/scope.ts +0 -11
- package/src/watchdog/settings.ts +48 -104
- package/src/watchdog/types.ts +18 -32
- package/src/watchdog/warning-format.ts +0 -1
- package/src/workflows/chat-progress.ts +3 -2
- package/src/workflows/workflow-checklist.ts +10 -12
- package/src/workflows/workflow-preflight.ts +28 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { captureWatchdogDiffBaseline, type WatchdogDiffBaseline } from "./diff-tool.ts";
|
|
2
3
|
import { MainWatchdogRuntime } from "./runtime.ts";
|
|
3
4
|
import { createMainWatchdogReview } from "./review.ts";
|
|
4
5
|
import { DEFAULT_WATCHDOG_CONFIG } from "./settings.ts";
|
|
@@ -23,11 +24,8 @@ export function childResolvedConfig(config: ChildWatchdogConfig): ResolvedWatchd
|
|
|
23
24
|
...(config.model ? { model: config.model } : {}),
|
|
24
25
|
...(config.thinking !== undefined ? { thinking: config.thinking } : {}),
|
|
25
26
|
},
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
maxAttempts: config.autoFollowMaxAttempts,
|
|
29
|
-
stalemateRepeats: config.stalemateRepeats,
|
|
30
|
-
},
|
|
27
|
+
stalemateRepeats: config.stalemateRepeats,
|
|
28
|
+
cadence: { ...config.cadence },
|
|
31
29
|
children: {
|
|
32
30
|
...DEFAULT_WATCHDOG_CONFIG.children,
|
|
33
31
|
watchdogTailTimeoutMs: config.watchdogTailTimeoutMs,
|
|
@@ -55,10 +53,11 @@ function writeStatus(event: unknown): void {
|
|
|
55
53
|
|
|
56
54
|
export function registerChildWatchdog(pi: ExtensionAPI, rawConfig = process.env[CHILD_WATCHDOG_CONFIG_ENV]): MainWatchdogRuntime | undefined {
|
|
57
55
|
const childConfig = decodeChildWatchdogConfig(rawConfig);
|
|
58
|
-
if (!childConfig
|
|
56
|
+
if (!childConfig) return undefined;
|
|
59
57
|
let currentContext: ExtensionContext | undefined;
|
|
58
|
+
let diffBaseline: WatchdogDiffBaseline | undefined;
|
|
60
59
|
let seq = 0;
|
|
61
|
-
const emitStatus = (phase: ChildWatchdogPhase,
|
|
60
|
+
const emitStatus = (phase: ChildWatchdogPhase, reason?: string): void => {
|
|
62
61
|
writeStatus({
|
|
63
62
|
type: CHILD_WATCHDOG_STATUS_EVENT,
|
|
64
63
|
...(childConfig.runId ? { runId: childConfig.runId } : {}),
|
|
@@ -67,19 +66,18 @@ export function registerChildWatchdog(pi: ExtensionAPI, rawConfig = process.env[
|
|
|
67
66
|
seq: ++seq,
|
|
68
67
|
phase,
|
|
69
68
|
ts: Date.now(),
|
|
70
|
-
followUpPending,
|
|
71
69
|
...(reason ? { reason } : {}),
|
|
72
70
|
});
|
|
73
71
|
};
|
|
74
72
|
const resolved = childResolvedConfig(childConfig);
|
|
75
73
|
const runtime = new MainWatchdogRuntime({
|
|
76
74
|
resolveConfig: () => ({ ok: true, config: resolved, errors: [], sources: [{ scope: "session", exists: true }] }),
|
|
77
|
-
review: createMainWatchdogReview(() => currentContext, { getThinkingLevel: () => pi.getThinkingLevel() }),
|
|
75
|
+
review: createMainWatchdogReview(() => currentContext, { getThinkingLevel: () => pi.getThinkingLevel(), diffBaseline: () => diffBaseline }),
|
|
78
76
|
reviewDescription: "child model review",
|
|
79
77
|
reviewChangesOnly: true,
|
|
80
|
-
displayWarning: (details) => {
|
|
78
|
+
displayWarning: (details, options) => {
|
|
81
79
|
const childDetails = childWarningDetails(details, childConfig);
|
|
82
|
-
pi.sendMessage(createWatchdogWarningMessage(childDetails, { display: true, details: childDetails }));
|
|
80
|
+
pi.sendMessage(createWatchdogWarningMessage(childDetails, { display: true, details: childDetails }), options);
|
|
83
81
|
},
|
|
84
82
|
});
|
|
85
83
|
const rememberContext = (ctx: ExtensionContext) => {
|
|
@@ -88,6 +86,7 @@ export function registerChildWatchdog(pi: ExtensionAPI, rawConfig = process.env[
|
|
|
88
86
|
const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: unknown, ctx: ExtensionContext) => unknown) => void;
|
|
89
87
|
onRuntimeEvent("session_start", (_event, ctx) => {
|
|
90
88
|
rememberContext(ctx);
|
|
89
|
+
diffBaseline = captureWatchdogDiffBaseline(ctx.cwd);
|
|
91
90
|
runtime.bindSession(ctx);
|
|
92
91
|
emitStatus("idle");
|
|
93
92
|
});
|
|
@@ -99,13 +98,17 @@ export function registerChildWatchdog(pi: ExtensionAPI, rawConfig = process.env[
|
|
|
99
98
|
rememberContext(ctx);
|
|
100
99
|
runtime.handleTurnEnd(event, ctx);
|
|
101
100
|
});
|
|
101
|
+
onRuntimeEvent("tool_result", (_event, ctx) => {
|
|
102
|
+
rememberContext(ctx);
|
|
103
|
+
runtime.handleToolResult(ctx);
|
|
104
|
+
});
|
|
102
105
|
onRuntimeEvent("agent_end", async (event, ctx) => {
|
|
103
106
|
rememberContext(ctx);
|
|
104
107
|
emitStatus("reviewing");
|
|
105
108
|
await runtime.handleAgentEnd(event, ctx);
|
|
106
109
|
const snapshot = runtime.getSnapshot(ctx.cwd);
|
|
107
|
-
if (snapshot.status === "failed") emitStatus("failed",
|
|
108
|
-
else if (snapshot.status === "stale") emitStatus("stale",
|
|
110
|
+
if (snapshot.status === "failed") emitStatus("failed", snapshot.lastError);
|
|
111
|
+
else if (snapshot.status === "stale") emitStatus("stale", "review stale");
|
|
109
112
|
else emitStatus("idle");
|
|
110
113
|
});
|
|
111
114
|
onRuntimeEvent("session_shutdown", () => {
|
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@e
|
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, type ThinkingLevel } from "../shared/model-info.ts";
|
|
4
4
|
import { SLASH_TEXT_RESULT_TYPE } from "../shared/types.ts";
|
|
5
|
+
import { captureWatchdogDiffBaseline, type WatchdogDiffBaseline } from "./diff-tool.ts";
|
|
5
6
|
import { recommendStrongWatchdogModel, resolveWatchdogModelInput, parseWatchdogThinkingInput } from "./model-selection.ts";
|
|
6
7
|
import { renderWatchdogWarning } from "./render.ts";
|
|
7
8
|
import { createMainWatchdogReview } from "./review.ts";
|
|
@@ -120,7 +121,8 @@ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["ge
|
|
|
120
121
|
childrenLine(snapshot),
|
|
121
122
|
recommendationLine(ctx),
|
|
122
123
|
`Agent-end timeout: ${snapshot.config.agentEndTimeoutMs}ms`,
|
|
123
|
-
`
|
|
124
|
+
`Stalemate: ${snapshot.boundaryRepeats}/${snapshot.config.stalemateRepeats}${snapshot.stalemate ? " · stopped" : ""}`,
|
|
125
|
+
`Rules: ${snapshot.config.rules ? `${Object.keys(snapshot.config.rules.roleModels).length} role models · ${snapshot.config.rules.action}` : "none"}`,
|
|
124
126
|
`Review model call: ${snapshot.reviewDescription}`,
|
|
125
127
|
];
|
|
126
128
|
if (snapshot.failedReviews > 0) lines.push(`Failed reviews: ${snapshot.failedReviews}`);
|
|
@@ -238,7 +240,7 @@ function createTestWarning(severity: "concern" | "blocker", text: string): Watch
|
|
|
238
240
|
summary: text,
|
|
239
241
|
evidence: `Manual /subagents-watchdog test ${severity} message from the main session.`,
|
|
240
242
|
recommendedAction: severity === "blocker"
|
|
241
|
-
? "Verify the renderer
|
|
243
|
+
? "Verify the renderer and transcript delivery."
|
|
242
244
|
: "Verify the renderer and transcript delivery; decide manually whether any action is needed.",
|
|
243
245
|
};
|
|
244
246
|
}
|
|
@@ -376,17 +378,15 @@ async function handleWatchdogCommand(
|
|
|
376
378
|
|
|
377
379
|
export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatchdogOptions = {}): MainWatchdogRuntime {
|
|
378
380
|
let currentContext: ExtensionContext | undefined;
|
|
381
|
+
let diffBaseline: WatchdogDiffBaseline | undefined;
|
|
379
382
|
const rememberContext = (ctx: ExtensionContext) => {
|
|
380
383
|
currentContext = ctx;
|
|
381
384
|
};
|
|
382
385
|
const runtime = options.runtime ?? new MainWatchdogRuntime({
|
|
383
|
-
review: options.review ?? createMainWatchdogReview(() => currentContext, { getThinkingLevel: () => pi.getThinkingLevel() }),
|
|
386
|
+
review: options.review ?? createMainWatchdogReview(() => currentContext, { getThinkingLevel: () => pi.getThinkingLevel(), diffBaseline: () => diffBaseline }),
|
|
384
387
|
reviewDescription: options.review ? "injected seam" : "real model review",
|
|
385
388
|
reviewChangesOnly: true,
|
|
386
|
-
displayWarning: (details,
|
|
387
|
-
pi.sendMessage(createWatchdogWarningMessage(details, { display: true, details }), delivery?.deliverAs === "steer" ? { deliverAs: "steer" } : undefined);
|
|
388
|
-
},
|
|
389
|
-
sendUserMessage: (message) => pi.sendUserMessage(message),
|
|
389
|
+
displayWarning: (details, options) => pi.sendMessage(createWatchdogWarningMessage(details, { display: true, details }), options),
|
|
390
390
|
});
|
|
391
391
|
|
|
392
392
|
pi.registerMessageRenderer<WatchdogWarningDetails>(SUBAGENT_WATCHDOG_WARNING_TYPE, (message, renderOptions, theme) => {
|
|
@@ -410,6 +410,7 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
|
|
|
410
410
|
|
|
411
411
|
pi.on("session_start", (_event, ctx) => {
|
|
412
412
|
rememberContext(ctx);
|
|
413
|
+
diffBaseline = captureWatchdogDiffBaseline(ctx.cwd);
|
|
413
414
|
runtime.bindSession(ctx);
|
|
414
415
|
});
|
|
415
416
|
pi.on("before_agent_start", (event, ctx) => {
|
|
@@ -428,8 +429,8 @@ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatc
|
|
|
428
429
|
rememberContext(ctx);
|
|
429
430
|
return runtime.handleAgentEnd(event, ctx);
|
|
430
431
|
});
|
|
431
|
-
pi.on("session_before_switch", () => runtime.reset("session switch", { clearReviewInputSignature: true, clearLspLedger: true, clearScope: true
|
|
432
|
-
pi.on("session_before_fork", () => runtime.reset("session fork", { clearReviewInputSignature: true, clearLspLedger: true, clearScope: true
|
|
432
|
+
pi.on("session_before_switch", () => runtime.reset("session switch", { clearReviewInputSignature: true, clearLspLedger: true, clearScope: true }));
|
|
433
|
+
pi.on("session_before_fork", () => runtime.reset("session fork", { clearReviewInputSignature: true, clearLspLedger: true, clearScope: true }));
|
|
433
434
|
pi.on("session_compact", () => runtime.reset("session compact", { clearScope: true }));
|
|
434
435
|
pi.on("session_shutdown", () => {
|
|
435
436
|
currentContext = undefined;
|
package/src/watchdog/render.ts
CHANGED
|
@@ -13,10 +13,9 @@ function titleCase(value: string): string {
|
|
|
13
13
|
function stateLabels(warning: WatchdogWarningDetails): string[] {
|
|
14
14
|
const labels: string[] = [];
|
|
15
15
|
if (warning.state === "displayed") labels.push("displayed");
|
|
16
|
-
if (warning.stale || warning.state === "stale") labels.push("stale
|
|
16
|
+
if (warning.stale || warning.state === "stale") labels.push("stale");
|
|
17
17
|
if (warning.state === "failed") labels.push("failed review");
|
|
18
|
-
if (warning.state === "stalemate") labels.push("stalemate
|
|
19
|
-
if (warning.autoFollowAttempt !== undefined) labels.push(`auto-follow attempt ${warning.autoFollowAttempt}`);
|
|
18
|
+
if (warning.state === "stalemate") labels.push("stalemate");
|
|
20
19
|
return labels;
|
|
21
20
|
}
|
|
22
21
|
|
|
@@ -31,9 +30,9 @@ export function formatWatchdogWarningRenderText(warning: WatchdogWarningDetails)
|
|
|
31
30
|
];
|
|
32
31
|
if (warning.state === "failed" && warning.error) lines.push(`Failure: ${warning.error}`);
|
|
33
32
|
if (warning.state === "stalemate" && warning.stalemateRepeats !== undefined) {
|
|
34
|
-
lines.push(`
|
|
33
|
+
lines.push(`Same warning ${warning.stalemateRepeats} time${warning.stalemateRepeats === 1 ? "" : "s"} in a row; the watchdog stopped continuing the run.`);
|
|
35
34
|
}
|
|
36
|
-
if (warning.stale || warning.state === "stale") lines.push("This warning arrived after the watchdog catch-up timeout
|
|
35
|
+
if (warning.stale || warning.state === "stale") lines.push("This warning arrived after the watchdog catch-up timeout.");
|
|
37
36
|
return lines.join("\n");
|
|
38
37
|
}
|
|
39
38
|
|
package/src/watchdog/review.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { Type, type Static } from "typebox";
|
|
|
6
6
|
import { resolveModelCandidate } from "../runs/shared/model-fallback.ts";
|
|
7
7
|
import { agentStreamOptions } from "../shared/agent-stream-options.ts";
|
|
8
8
|
import { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, toModelInfo } from "../shared/model-info.ts";
|
|
9
|
+
import { createWatchdogDiffTool, WATCHDOG_DIFF_TOOL_NAME, type WatchdogDiffBaseline } from "./diff-tool.ts";
|
|
10
|
+
import { loadWatchdogGuidance } from "./guidance.ts";
|
|
9
11
|
import type { WatchdogReviewFunction, WatchdogReviewRequest } from "./runtime.ts";
|
|
10
12
|
import {
|
|
11
13
|
WATCHDOG_WARNING_CATEGORIES,
|
|
@@ -18,7 +20,7 @@ import {
|
|
|
18
20
|
type WatchdogWarning,
|
|
19
21
|
} from "./types.ts";
|
|
20
22
|
|
|
21
|
-
const WATCHDOG_ALLOWED_TOOL_NAMES = new Set(["read", "grep", "find", "ls", "watchdog_warn"]);
|
|
23
|
+
const WATCHDOG_ALLOWED_TOOL_NAMES = new Set(["read", "grep", "find", "ls", "watchdog_warn", WATCHDOG_DIFF_TOOL_NAME]);
|
|
22
24
|
|
|
23
25
|
const WatchdogWarnParams = Type.Object({
|
|
24
26
|
severity: Type.String({ enum: WATCHDOG_WARNING_SEVERITIES, description: "concern for actionable risk, blocker for a likely wrong or unsafe outcome" }),
|
|
@@ -52,6 +54,7 @@ export interface CreateMainWatchdogReviewOptions {
|
|
|
52
54
|
streamFn?: StreamFn;
|
|
53
55
|
createReadOnlyTools?: (cwd: string) => AgentTool[];
|
|
54
56
|
getThinkingLevel?: () => ThinkingLevel | undefined;
|
|
57
|
+
diffBaseline?: () => WatchdogDiffBaseline | undefined;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function fullModelId(model: Pick<RegistryModel, "provider" | "id">): string {
|
|
@@ -205,18 +208,20 @@ function createWatchdogWarnTool(request: WatchdogReviewRequest): AgentTool<typeo
|
|
|
205
208
|
};
|
|
206
209
|
}
|
|
207
210
|
|
|
208
|
-
function buildWatchdogSystemPrompt(ctx: ExtensionContext, options: { hasScope?: boolean } = {}): string {
|
|
211
|
+
export function buildWatchdogSystemPrompt(ctx: Pick<ExtensionContext, "cwd">, options: { hasScope?: boolean; guidance?: string; hasDiff?: boolean } = {}): string {
|
|
212
|
+
const guidance = options.guidance?.trim();
|
|
209
213
|
return [
|
|
210
214
|
"You are the main-session subagent watchdog for Pi.",
|
|
211
215
|
`Working directory: ${ctx.cwd}`,
|
|
212
216
|
"Review only the supplied parent turn delta. Inspect repository files only when needed to verify a concrete concern.",
|
|
213
217
|
options.hasScope ? "When the review input includes a Current scope block, treat newer scope prompts as superseding/mutating older prompts and use category='scope-drift' for work that serves no current scope item." : undefined,
|
|
214
|
-
|
|
218
|
+
`You are read-only. You may use ${options.hasDiff ? "read, grep, find, ls, and watchdog_diff (the full repo diff since the session baseline; pass a path to narrow it)" : "read, grep, find, and ls"}. Do not edit files, run shell commands, spawn agents, or mutate state.`,
|
|
215
219
|
"Emit warnings only by calling watchdog_warn. Freeform assistant text is ignored and must not be used to report warnings.",
|
|
216
220
|
"Emit only medium/high confidence actionable concerns or blockers: missed user constraints, correctness risks, test gaps that matter, unsafe changes, stale facts, loop risks, or scope drift.",
|
|
217
221
|
"Do not emit nits, style preferences, low-confidence guesses, informational notes, praise, or summaries.",
|
|
218
222
|
"If the turn is clean, call no tools and end normally.",
|
|
219
223
|
"Use severity='blocker' only when the issue should stop acceptance until addressed; otherwise use severity='concern'.",
|
|
224
|
+
guidance ? `\nStanding instructions from WATCHDOG.md (project first, then user):\n${guidance}` : undefined,
|
|
220
225
|
].filter((line): line is string => Boolean(line)).join("\n");
|
|
221
226
|
}
|
|
222
227
|
|
|
@@ -269,13 +274,19 @@ export function createMainWatchdogReview(provider: WatchdogContextProvider, opti
|
|
|
269
274
|
env: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,
|
|
270
275
|
headers: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
|
|
271
276
|
});
|
|
277
|
+
const diffBaseline = options.diffBaseline?.();
|
|
272
278
|
const tools = [
|
|
273
279
|
...(options.createReadOnlyTools ?? createReadOnlyTools)(ctx.cwd).filter((tool) => WATCHDOG_ALLOWED_TOOL_NAMES.has(tool.name) && tool.name !== "watchdog_warn"),
|
|
274
280
|
createWatchdogWarnTool(request),
|
|
281
|
+
...(diffBaseline ? [createWatchdogDiffTool(diffBaseline)] : []),
|
|
275
282
|
];
|
|
276
283
|
const agent = new Agent({
|
|
277
284
|
initialState: {
|
|
278
|
-
systemPrompt: buildWatchdogSystemPrompt(ctx, {
|
|
285
|
+
systemPrompt: buildWatchdogSystemPrompt(ctx, {
|
|
286
|
+
hasScope: request.hasScope,
|
|
287
|
+
guidance: loadWatchdogGuidance(ctx.cwd, request.config.guidance.watchdogMd),
|
|
288
|
+
hasDiff: diffBaseline !== undefined,
|
|
289
|
+
}),
|
|
279
290
|
model: selection.model,
|
|
280
291
|
thinkingLevel: selection.thinkingLevel,
|
|
281
292
|
tools,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { splitKnownThinkingSuffix } from "../shared/model-info.ts";
|
|
3
|
+
import { resolveWatchdogConfig } from "./settings.ts";
|
|
4
|
+
import type { WatchdogRulesConfig, WatchdogWarning } from "./types.ts";
|
|
5
|
+
import { createWatchdogWarningMessage } from "./warning-format.ts";
|
|
6
|
+
|
|
7
|
+
export interface WatchdogRuleViolation {
|
|
8
|
+
agent: string;
|
|
9
|
+
summary: string;
|
|
10
|
+
evidence: string;
|
|
11
|
+
recommendedAction: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function loadWatchdogLaunchRules(cwd: string): WatchdogRulesConfig | undefined {
|
|
15
|
+
const result = resolveWatchdogConfig(cwd);
|
|
16
|
+
return result.ok ? result.config.rules : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `*` matches any run of characters, `?` one character; anchored, case-sensitive. */
|
|
20
|
+
export function watchdogGlobMatch(pattern: string, value: string): boolean {
|
|
21
|
+
const source = pattern.split("").map((char) => char === "*" ? ".*" : char === "?" ? "." : char.replace(/[.+^${}()|[\]\\]/g, "\\$&")).join("");
|
|
22
|
+
return new RegExp(`^${source}$`).test(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function modelMatches(patterns: string[], model: string): string | undefined {
|
|
26
|
+
const base = splitKnownThinkingSuffix(model).baseModel;
|
|
27
|
+
return patterns.find((pattern) => watchdogGlobMatch(pattern, model) || watchdogGlobMatch(pattern, base));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Deny wins over allow; an unknown model cannot be judged. */
|
|
31
|
+
export function evaluateLaunchRule(rules: WatchdogRulesConfig | undefined, agent: string, model: string | undefined): WatchdogRuleViolation | undefined {
|
|
32
|
+
const roleRule = rules?.roleModels[agent];
|
|
33
|
+
if (!roleRule || !model) return undefined;
|
|
34
|
+
const note = roleRule.note ? ` ${roleRule.note}` : "";
|
|
35
|
+
const denied = roleRule.deny?.length ? modelMatches(roleRule.deny, model) : undefined;
|
|
36
|
+
if (denied !== undefined) {
|
|
37
|
+
return {
|
|
38
|
+
agent,
|
|
39
|
+
summary: `Agent '${agent}' was launched with denied model '${model}'.`,
|
|
40
|
+
evidence: `subagents.watchdog.rules.roleModels.${agent}.deny matches '${denied}'.${note}`,
|
|
41
|
+
recommendedAction: roleRule.allow?.length ? `Use one of: ${roleRule.allow.join(", ")}.` : "Choose a different model for this role.",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (!roleRule.allow?.length || modelMatches(roleRule.allow, model) !== undefined) return undefined;
|
|
45
|
+
return {
|
|
46
|
+
agent,
|
|
47
|
+
summary: `Agent '${agent}' was launched with model '${model}', which is not in its allowed list.`,
|
|
48
|
+
evidence: `subagents.watchdog.rules.roleModels.${agent}.allow is [${roleRule.allow.join(", ")}].${note}`,
|
|
49
|
+
recommendedAction: `Use one of: ${roleRule.allow.join(", ")}.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function ruleViolationWarning(violation: WatchdogRuleViolation): WatchdogWarning {
|
|
54
|
+
return { severity: "concern", category: "missed-constraint", confidence: "high", source: "main", ...violation };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function applyWatchdogLaunchRules(input: { cwd: string; agent: string; model?: string; warn?: (violation: WatchdogRuleViolation) => void }): string | undefined {
|
|
58
|
+
const rules = loadWatchdogLaunchRules(input.cwd);
|
|
59
|
+
const violation = evaluateLaunchRule(rules, input.agent, input.model);
|
|
60
|
+
if (!violation) return undefined;
|
|
61
|
+
if (rules?.action === "block") return `Launch blocked by subagents.watchdog.rules: ${violation.summary}`;
|
|
62
|
+
input.warn?.(violation);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** For launch paths without a main watchdog runtime (background chain steps). */
|
|
67
|
+
export function sendRuleViolationWarning(pi: Pick<ExtensionAPI, "sendMessage">, violation: WatchdogRuleViolation): void {
|
|
68
|
+
const warning = ruleViolationWarning(violation);
|
|
69
|
+
pi.sendMessage(createWatchdogWarningMessage(warning, { display: true, details: { state: "displayed", displayedAt: new Date().toISOString() } }), { deliverAs: "steer" });
|
|
70
|
+
}
|