pi-subagents 0.34.0 → 0.35.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +78 -9
  2. package/README.md +213 -32
  3. package/index.ts +1 -0
  4. package/install.mjs +1 -1
  5. package/package.json +23 -8
  6. package/prompts/review-loop.md +3 -1
  7. package/skills/pi-subagents/SKILL.md +87 -25
  8. package/src/agents/agent-management.ts +82 -15
  9. package/src/agents/agent-serializer.ts +19 -0
  10. package/src/agents/agents.ts +91 -49
  11. package/src/agents/frontmatter.ts +67 -13
  12. package/src/agents/skills.ts +25 -12
  13. package/src/api/background-work.ts +197 -0
  14. package/src/api/delegation.ts +158 -0
  15. package/src/extension/chain-validation.ts +165 -0
  16. package/src/extension/doctor.ts +15 -0
  17. package/src/extension/fanout-child.ts +3 -1
  18. package/src/extension/index.ts +65 -124
  19. package/src/extension/rpc.ts +10 -2
  20. package/src/extension/schemas.ts +18 -14
  21. package/src/extension/steering-notices.ts +35 -0
  22. package/src/extension/tool-description.ts +20 -9
  23. package/src/intercom/intercom-bridge.ts +3 -2
  24. package/src/intercom/native-supervisor-channel.ts +9 -1
  25. package/src/intercom/result-intercom.ts +4 -0
  26. package/src/runs/background/async-execution.ts +293 -44
  27. package/src/runs/background/async-job-tracker.ts +56 -9
  28. package/src/runs/background/async-resume.ts +159 -52
  29. package/src/runs/background/async-status.ts +25 -18
  30. package/src/runs/background/auto-drain.ts +67 -0
  31. package/src/runs/background/chain-root-attachment.ts +16 -8
  32. package/src/runs/background/control-channel.ts +260 -13
  33. package/src/runs/background/fleet-view.ts +23 -2
  34. package/src/runs/background/notify.ts +79 -10
  35. package/src/runs/background/result-watcher.ts +12 -9
  36. package/src/runs/background/run-id-resolver.ts +14 -2
  37. package/src/runs/background/run-status.ts +23 -15
  38. package/src/runs/background/scheduled-runs.ts +3 -0
  39. package/src/runs/background/stale-run-reconciler.ts +32 -10
  40. package/src/runs/background/steering.ts +237 -0
  41. package/src/runs/background/subagent-runner.ts +898 -236
  42. package/src/runs/background/subagent-wait.ts +484 -0
  43. package/src/runs/background/top-level-async.ts +2 -1
  44. package/src/runs/background/wait-config.ts +36 -0
  45. package/src/runs/background/wait-tool.ts +26 -0
  46. package/src/runs/foreground/async-steering-action.ts +230 -0
  47. package/src/runs/foreground/chain-clarify.ts +22 -6
  48. package/src/runs/foreground/chain-execution.ts +50 -32
  49. package/src/runs/foreground/execution.ts +308 -94
  50. package/src/runs/foreground/subagent-executor.ts +592 -268
  51. package/src/runs/shared/acceptance.ts +355 -97
  52. package/src/runs/shared/child-protocol.ts +121 -0
  53. package/src/runs/shared/completion-guard.ts +8 -127
  54. package/src/runs/shared/dynamic-fanout.ts +6 -4
  55. package/src/runs/shared/model-fallback.ts +36 -0
  56. package/src/runs/shared/nested-events.ts +9 -4
  57. package/src/runs/shared/nested-render.ts +4 -1
  58. package/src/runs/shared/parallel-utils.ts +7 -0
  59. package/src/runs/shared/pi-args.ts +34 -7
  60. package/src/runs/shared/pi-spawn.ts +18 -12
  61. package/src/runs/shared/session-lease.ts +279 -0
  62. package/src/runs/shared/single-output.ts +61 -6
  63. package/src/runs/shared/spawn-budget.ts +128 -0
  64. package/src/runs/shared/subagent-control.ts +10 -6
  65. package/src/runs/shared/subagent-prompt-runtime.ts +127 -26
  66. package/src/runs/shared/task-intent.ts +176 -0
  67. package/src/runs/shared/tool-availability.ts +65 -0
  68. package/src/runs/shared/turn-budget.ts +49 -4
  69. package/src/shared/atomic-json.ts +4 -1
  70. package/src/shared/fork-context.ts +28 -3
  71. package/src/shared/model-info.ts +7 -4
  72. package/src/shared/status-format.ts +7 -1
  73. package/src/shared/types.ts +203 -25
  74. package/src/shared/utils.ts +35 -7
  75. package/src/slash/delegation-adapters.ts +457 -0
  76. package/src/slash/delegation-request.ts +103 -0
  77. package/src/slash/prompt-template-bridge.ts +167 -344
  78. package/src/slash/slash-commands.ts +239 -6
  79. package/src/slash/subagents-admin.ts +428 -0
  80. package/src/slash/subagents-editor.ts +86 -0
  81. package/src/tui/fleet.ts +405 -0
  82. package/src/tui/render.ts +90 -16
  83. package/src/watchdog/change-signature.ts +127 -0
  84. package/src/watchdog/child-status.ts +205 -0
  85. package/src/watchdog/emission-guard.ts +123 -0
  86. package/src/watchdog/lsp-diagnostics.ts +532 -0
  87. package/src/watchdog/model-selection.ts +167 -0
  88. package/src/watchdog/register-child.ts +117 -0
  89. package/src/watchdog/register-main.ts +433 -0
  90. package/src/watchdog/render.ts +54 -0
  91. package/src/watchdog/review.ts +293 -0
  92. package/src/watchdog/runtime.ts +712 -0
  93. package/src/watchdog/settings.ts +528 -0
  94. package/src/watchdog/tool-actions.ts +155 -0
  95. package/src/watchdog/turn-delta.ts +161 -0
  96. package/src/watchdog/types.ts +188 -0
  97. package/src/watchdog/warning-format.ts +73 -0
  98. package/src/runs/background/wait.ts +0 -394
@@ -0,0 +1,433 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+ import { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, type ThinkingLevel } from "../shared/model-info.ts";
4
+ import { SLASH_TEXT_RESULT_TYPE } from "../shared/types.ts";
5
+ import { recommendStrongWatchdogModel, resolveWatchdogModelInput, parseWatchdogThinkingInput } from "./model-selection.ts";
6
+ import { renderWatchdogWarning } from "./render.ts";
7
+ import { createMainWatchdogReview } from "./review.ts";
8
+ import { MainWatchdogRuntime, type WatchdogReviewFunction } from "./runtime.ts";
9
+ import { getWatchdogUserSettingsPath, writeUserWatchdogEnabled, writeWatchdogModelSettings } from "./settings.ts";
10
+ import {
11
+ SUBAGENT_WATCHDOG_WARNING_TYPE,
12
+ type WatchdogRuntimeStatus,
13
+ type WatchdogWarning,
14
+ type WatchdogWarningDetails,
15
+ } from "./types.ts";
16
+ import { createWatchdogWarningMessage } from "./warning-format.ts";
17
+
18
+ interface RegisterMainWatchdogOptions {
19
+ runtime?: MainWatchdogRuntime;
20
+ review?: WatchdogReviewFunction;
21
+ }
22
+
23
+ function sendSlashText(pi: ExtensionAPI, text: string): void {
24
+ pi.sendMessage({ customType: SLASH_TEXT_RESULT_TYPE, content: text, display: true });
25
+ }
26
+
27
+ function messageFromError(error: unknown): string {
28
+ return error instanceof Error ? error.message : String(error);
29
+ }
30
+
31
+ function boolLabel(value: boolean): string {
32
+ return value ? "on" : "off";
33
+ }
34
+
35
+ function statusLabel(status: WatchdogRuntimeStatus): string {
36
+ return status.replaceAll("-", " ");
37
+ }
38
+
39
+ function sourceLine(source: { scope: string; path?: string; exists: boolean }): string {
40
+ const location = source.path ? ` ${source.path}` : "";
41
+ return `- ${source.scope}${location}: ${source.exists ? "found" : "not found"}`;
42
+ }
43
+
44
+ function currentSessionModelLine(ctx: ExtensionContext): string {
45
+ const model = ctx.model as { provider?: unknown; id?: unknown } | undefined;
46
+ if (model && typeof model.provider === "string" && typeof model.id === "string") return `current session (${model.provider}/${model.id})`;
47
+ return "current session (not configured)";
48
+ }
49
+
50
+ function mainThinkingLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>, ctx: ExtensionContext): string {
51
+ const configuredModel = snapshot.config.main.model;
52
+ const configuredThinking = snapshot.config.main.thinking;
53
+ if (configuredModel) {
54
+ const effective = resolveEffectiveThinking(configuredModel, configuredThinking);
55
+ if (effective) return effective;
56
+ return "off (default for explicit watchdog model)";
57
+ }
58
+ if (configuredThinking === false) return "off";
59
+ if (configuredThinking !== undefined) return configuredThinking;
60
+ const currentThinking = (ctx as { thinkingLevel?: unknown }).thinkingLevel;
61
+ return typeof currentThinking === "string" ? `current session (${currentThinking})` : "current session";
62
+ }
63
+
64
+ function mainModelLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>, ctx: ExtensionContext): string {
65
+ if (snapshot.config.main.model) {
66
+ const source = snapshot.sessionModelOverride?.model ? "session override" : "configured";
67
+ return `Main model: ${splitKnownThinkingSuffix(snapshot.config.main.model).baseModel} (${source})`;
68
+ }
69
+ return `Main model: ${currentSessionModelLine(ctx)}`;
70
+ }
71
+
72
+ function childrenLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>): string {
73
+ const children = snapshot.config.children;
74
+ const model = children.model ? splitKnownThinkingSuffix(children.model).baseModel : "current child session";
75
+ const thinking = children.thinking === undefined ? "current child session" : children.thinking === false ? "off" : children.thinking;
76
+ const overrides = Object.entries(children.overrides);
77
+ const overrideText = overrides.length
78
+ ? ` · overrides ${overrides.map(([agent, override]) => {
79
+ const bits = [agent];
80
+ if (override.enabled !== undefined) bits.push(boolLabel(override.enabled));
81
+ if (override.model) bits.push(splitKnownThinkingSuffix(override.model).baseModel);
82
+ if (override.thinking !== undefined) bits.push(`thinking ${override.thinking === false ? "off" : override.thinking}`);
83
+ return bits.join(" ");
84
+ }).join("; ")}`
85
+ : "";
86
+ return `Children: ${boolLabel(snapshot.config.enabled && children.enabled)} · model ${model} · thinking ${thinking}${overrideText}`;
87
+ }
88
+
89
+ function recommendationLine(ctx: ExtensionContext): string {
90
+ try {
91
+ const recommendation = recommendStrongWatchdogModel(ctx);
92
+ return `Recommended strong watchdog: ${recommendation.model}:${recommendation.thinking} (${recommendation.label}, complementary reviewer)`;
93
+ } catch (error) {
94
+ return `Recommended strong watchdog: unavailable (${messageFromError(error)})`;
95
+ }
96
+ }
97
+
98
+ function lspLine(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>): string {
99
+ const lsp = snapshot.lsp;
100
+ const provider = lsp.provider ? ` · ${lsp.provider}` : "";
101
+ const counts = lsp.diagnosticCount > 0 || lsp.freshDiagnosticCount > 0
102
+ ? ` · ${lsp.freshDiagnosticCount} new/${lsp.diagnosticCount} total`
103
+ : "";
104
+ const message = lsp.message ? ` · ${lsp.message}` : "";
105
+ return `LSP diagnostics: ${lsp.enabled ? "on" : "off"} · ${lsp.status}${provider}${counts}${message}`;
106
+ }
107
+
108
+ export function buildWatchdogStatus(snapshot: ReturnType<MainWatchdogRuntime["getSnapshot"]>, ctx: ExtensionContext): string {
109
+ const lines = [
110
+ "Subagent watchdog",
111
+ `Main: ${boolLabel(snapshot.enabled)}${!snapshot.config.enabled && snapshot.sessionOverride === undefined ? " (default off)" : ""}`,
112
+ `Runtime: ${statusLabel(snapshot.status)}${snapshot.bufferedDeltas > 0 ? ` · buffered deltas ${snapshot.bufferedDeltas}` : ""}`,
113
+ `Review trigger: ${snapshot.reviewTrigger === "repo-edits" ? "repo edits only" : "every non-empty turn delta"}`,
114
+ lspLine(snapshot),
115
+ `Session override: ${snapshot.sessionOverride === undefined ? "none" : boolLabel(snapshot.sessionOverride)}`,
116
+ mainModelLine(snapshot, ctx),
117
+ `Main thinking: ${mainThinkingLine(snapshot, ctx)}`,
118
+ childrenLine(snapshot),
119
+ recommendationLine(ctx),
120
+ `Agent-end timeout: ${snapshot.config.agentEndTimeoutMs}ms`,
121
+ `Auto-follow: not implemented${snapshot.autoFollowQueued ? " (queued)" : ""}`,
122
+ `Review model call: ${snapshot.reviewDescription}`,
123
+ ];
124
+ if (snapshot.failedReviews > 0) lines.push(`Failed reviews: ${snapshot.failedReviews}`);
125
+ if (snapshot.staleReviews > 0) lines.push(`Stale reviews: ${snapshot.staleReviews}`);
126
+ if (snapshot.changedPaths?.length) {
127
+ lines.push(`Changed paths: ${snapshot.changedPaths.slice(0, 8).join(", ")}${snapshot.changedPaths.length > 8 ? `, +${snapshot.changedPaths.length - 8} more` : ""}`);
128
+ }
129
+ if (snapshot.lastWarning) {
130
+ lines.push(`Last warning: ${snapshot.lastWarning.severity} · ${snapshot.lastWarning.state ?? "candidate"} · ${snapshot.lastWarning.summary}`);
131
+ }
132
+ if (snapshot.lastError) lines.push(`Last error: ${snapshot.lastError}`);
133
+ if (!snapshot.configOk) {
134
+ lines.push("", "Config errors:", ...snapshot.errors.map((error) => `- ${error.message}`), "Watchdog is disabled until the config is fixed.");
135
+ } else {
136
+ lines.push("", "Config: ok");
137
+ }
138
+ lines.push(
139
+ "Sources:",
140
+ ...snapshot.sources.map(sourceLine),
141
+ "",
142
+ "Model commands:",
143
+ "- /subagents-watchdog recommend-model",
144
+ "- /subagents-watchdog model recommended",
145
+ "- /subagents-watchdog model <provider/model[:thinking]>",
146
+ "- /subagents-watchdog model inherit",
147
+ "- /subagents-watchdog session model recommended",
148
+ "Agent action: subagent({ action: \"watchdog.configure\", model: \"recommended\", scope: \"session\" })",
149
+ );
150
+ return lines.join("\n");
151
+ }
152
+
153
+ function parseTestCommand(input: string): { severity: "concern" | "blocker"; text: string } | undefined {
154
+ const match = input.match(/^test\s+(concern|blocker)\s+([\s\S]+)$/);
155
+ if (!match) return undefined;
156
+ return { severity: match[1] as "concern" | "blocker", text: match[2]!.trim() };
157
+ }
158
+
159
+ function formatThinking(value: ThinkingLevel | false | undefined): string {
160
+ if (value === undefined) return "inherit";
161
+ return value === false ? "off" : value;
162
+ }
163
+
164
+ function parseThinkingCommand(raw: string): ThinkingLevel | false | null {
165
+ const value = raw.trim();
166
+ if (value === "inherit") return null;
167
+ return parseWatchdogThinkingInput(value, "/subagents-watchdog thinking") ?? null;
168
+ }
169
+
170
+ function resolveModelCommandValue(ctx: ExtensionCommandContext, raw: string): { model: string | null; thinking: ThinkingLevel | false | null; description: string } {
171
+ const value = raw.trim();
172
+ if (!value) throw new Error("Expected a model, 'recommended', or 'inherit'.");
173
+ if (value === "inherit") return { model: null, thinking: null, description: "current session model and thinking" };
174
+ if (value === "recommended") {
175
+ const recommendation = recommendStrongWatchdogModel(ctx as ExtensionContext);
176
+ return {
177
+ model: recommendation.model,
178
+ thinking: recommendation.thinking,
179
+ description: `${recommendation.model}:${recommendation.thinking} (${recommendation.label})`,
180
+ };
181
+ }
182
+ const resolved = resolveWatchdogModelInput(ctx as ExtensionContext, value);
183
+ return {
184
+ model: resolved.model,
185
+ thinking: resolved.thinking,
186
+ description: `${resolved.model}${resolved.thinking ? `:${resolved.thinking}` : ""}`,
187
+ };
188
+ }
189
+
190
+ function buildRecommendationText(ctx: ExtensionCommandContext): string {
191
+ const recommendation = recommendStrongWatchdogModel(ctx as ExtensionContext);
192
+ return [
193
+ "Subagent watchdog recommended model",
194
+ `Current session: ${currentSessionModelLine(ctx as ExtensionContext)}`,
195
+ `Recommended: ${recommendation.model}:${recommendation.thinking}`,
196
+ `Reason: ${recommendation.reason}`,
197
+ "",
198
+ "Apply for this session:",
199
+ "/subagents-watchdog session model recommended",
200
+ "",
201
+ "Save as your user default:",
202
+ "/subagents-watchdog model recommended",
203
+ ].join("\n");
204
+ }
205
+
206
+ function buildCheckText(runtime: MainWatchdogRuntime, ctx: ExtensionCommandContext): string {
207
+ const snapshot = runtime.getSnapshot(ctx.cwd);
208
+ if (!snapshot.configOk) {
209
+ return ["Subagent watchdog config check", "", "Config errors:", ...snapshot.errors.map((error) => `- ${error.message}`)].join("\n");
210
+ }
211
+ const lines = ["Subagent watchdog config check", "", "Config: ok"];
212
+ if (snapshot.config.main.model) {
213
+ const resolved = resolveWatchdogModelInput(ctx as ExtensionContext, snapshot.config.main.model);
214
+ lines.push(`Main model: ${resolved.model} auth ok`);
215
+ } else {
216
+ lines.push(`Main model: ${currentSessionModelLine(ctx as ExtensionContext)}`);
217
+ }
218
+ lines.push(`Main thinking: ${mainThinkingLine(snapshot, ctx as ExtensionContext)}`);
219
+ lines.push(lspLine(snapshot));
220
+ try {
221
+ const recommendation = recommendStrongWatchdogModel(ctx as ExtensionContext);
222
+ lines.push(`Recommended strong watchdog: ${recommendation.model}:${recommendation.thinking}`);
223
+ } catch (error) {
224
+ lines.push(`Recommended strong watchdog: unavailable (${messageFromError(error)})`);
225
+ }
226
+ return lines.join("\n");
227
+ }
228
+
229
+ function createTestWarning(severity: "concern" | "blocker", text: string): WatchdogWarning {
230
+ return {
231
+ severity,
232
+ category: "other",
233
+ confidence: "high",
234
+ source: "main",
235
+ state: "displayed",
236
+ summary: text,
237
+ evidence: `Manual /subagents-watchdog test ${severity} message from the main session.`,
238
+ recommendedAction: severity === "blocker"
239
+ ? "Verify the renderer and transcript delivery; no automatic follow-up is queued in Gate 1B."
240
+ : "Verify the renderer and transcript delivery; decide manually whether any action is needed.",
241
+ };
242
+ }
243
+
244
+ async function handleWatchdogCommand(
245
+ pi: ExtensionAPI,
246
+ runtime: MainWatchdogRuntime,
247
+ args: string,
248
+ ctx: ExtensionCommandContext,
249
+ ): Promise<void> {
250
+ const input = args.trim();
251
+ if (!input || input === "status") {
252
+ sendSlashText(pi, buildWatchdogStatus(runtime.getSnapshot(ctx.cwd), ctx));
253
+ return;
254
+ }
255
+ if (input === "recommend-model") {
256
+ try {
257
+ sendSlashText(pi, buildRecommendationText(ctx));
258
+ } catch (error) {
259
+ sendSlashText(pi, `Subagent watchdog recommended model\n\n${messageFromError(error)}`);
260
+ }
261
+ return;
262
+ }
263
+ if (input === "check") {
264
+ try {
265
+ sendSlashText(pi, buildCheckText(runtime, ctx));
266
+ } catch (error) {
267
+ sendSlashText(pi, `Subagent watchdog config check\n\n${messageFromError(error)}`);
268
+ }
269
+ return;
270
+ }
271
+ if (input === "on" || input === "off") {
272
+ const enabled = input === "on";
273
+ try {
274
+ const settingsPath = writeUserWatchdogEnabled(enabled);
275
+ const snapshot = runtime.getSnapshot(ctx.cwd);
276
+ sendSlashText(pi, [
277
+ `Subagent watchdog ${boolLabel(enabled)} saved to user settings.`,
278
+ `Updated: ${settingsPath}`,
279
+ `Main now: ${boolLabel(snapshot.enabled)}${snapshot.sessionOverride !== undefined ? ` (session override ${boolLabel(snapshot.sessionOverride)})` : ""}`,
280
+ ].join("\n"));
281
+ } catch (error) {
282
+ sendSlashText(pi, `Subagent watchdog\n\nCould not update ${getWatchdogUserSettingsPath()}: ${messageFromError(error)}`);
283
+ }
284
+ return;
285
+ }
286
+ if (input === "session on" || input === "session off") {
287
+ const enabled = input.endsWith("on");
288
+ const snapshot = runtime.setSessionEnabled(enabled, ctx.cwd);
289
+ sendSlashText(pi, [
290
+ `Subagent watchdog session override: ${boolLabel(enabled)}.`,
291
+ "No settings files were changed.",
292
+ "",
293
+ buildWatchdogStatus(snapshot, ctx),
294
+ ].join("\n"));
295
+ return;
296
+ }
297
+ if (input.startsWith("session model ")) {
298
+ const rawModel = input.slice("session model ".length);
299
+ try {
300
+ const value = resolveModelCommandValue(ctx, rawModel);
301
+ const snapshot = value.model === null
302
+ ? runtime.clearSessionModel(ctx.cwd)
303
+ : runtime.setSessionModel({ model: value.model, thinking: value.thinking ?? null }, ctx.cwd);
304
+ sendSlashText(pi, [
305
+ `Subagent watchdog session model: ${value.description}.`,
306
+ "No settings files were changed.",
307
+ "",
308
+ buildWatchdogStatus(snapshot, ctx),
309
+ ].join("\n"));
310
+ } catch (error) {
311
+ sendSlashText(pi, `Subagent watchdog session model\n\n${messageFromError(error)}`);
312
+ }
313
+ return;
314
+ }
315
+ if (input.startsWith("model ")) {
316
+ const rawModel = input.slice("model ".length);
317
+ try {
318
+ const value = resolveModelCommandValue(ctx, rawModel);
319
+ const settingsPath = writeWatchdogModelSettings({
320
+ scope: "user",
321
+ target: { kind: "main" },
322
+ model: value.model,
323
+ thinking: value.thinking,
324
+ });
325
+ runtime.refreshConfig(ctx.cwd);
326
+ const snapshot = runtime.getSnapshot(ctx.cwd);
327
+ sendSlashText(pi, [
328
+ `Subagent watchdog model saved: ${value.description}.`,
329
+ `Updated: ${settingsPath}`,
330
+ `Main now: ${boolLabel(snapshot.enabled)}`,
331
+ value.model === null ? "The watchdog now inherits the current session model and thinking." : "Run /subagents-watchdog on if the watchdog is still off.",
332
+ "",
333
+ buildWatchdogStatus(snapshot, ctx),
334
+ ].join("\n"));
335
+ } catch (error) {
336
+ sendSlashText(pi, `Subagent watchdog model\n\n${messageFromError(error)}\nNo settings files were changed.`);
337
+ }
338
+ return;
339
+ }
340
+ if (input.startsWith("thinking ")) {
341
+ const rawThinking = input.slice("thinking ".length);
342
+ try {
343
+ const thinking = parseThinkingCommand(rawThinking);
344
+ const settingsPath = writeWatchdogModelSettings({
345
+ scope: "user",
346
+ target: { kind: "main" },
347
+ thinking,
348
+ });
349
+ runtime.refreshConfig(ctx.cwd);
350
+ sendSlashText(pi, [
351
+ `Subagent watchdog thinking saved: ${formatThinking(thinking ?? undefined)}.`,
352
+ `Updated: ${settingsPath}`,
353
+ "",
354
+ buildWatchdogStatus(runtime.getSnapshot(ctx.cwd), ctx),
355
+ ].join("\n"));
356
+ } catch (error) {
357
+ sendSlashText(pi, `Subagent watchdog thinking\n\n${messageFromError(error)}\nNo settings files were changed.`);
358
+ }
359
+ return;
360
+ }
361
+ const test = parseTestCommand(input);
362
+ if (test) {
363
+ if (!test.text) {
364
+ ctx.ui.notify("Usage: /subagents-watchdog test concern|blocker <text>", "error");
365
+ return;
366
+ }
367
+ const warning = createTestWarning(test.severity, test.text);
368
+ const details = runtime.recordDisplayedWarning(warning);
369
+ pi.sendMessage(createWatchdogWarningMessage(details, { display: true, details }));
370
+ return;
371
+ }
372
+ ctx.ui.notify(`Usage: /subagents-watchdog [status|on|off|session on|session off|recommend-model|model recommended|model <provider/model[:thinking]>|model inherit|thinking ${THINKING_LEVELS.join("|")}|thinking inherit|session model recommended|check|test concern <text>|test blocker <text>]`, "error");
373
+ }
374
+
375
+ export function registerMainWatchdog(pi: ExtensionAPI, options: RegisterMainWatchdogOptions = {}): MainWatchdogRuntime {
376
+ let currentContext: ExtensionContext | undefined;
377
+ const rememberContext = (ctx: ExtensionContext) => {
378
+ currentContext = ctx;
379
+ };
380
+ const runtime = options.runtime ?? new MainWatchdogRuntime({
381
+ review: options.review ?? createMainWatchdogReview(() => currentContext, { getThinkingLevel: () => pi.getThinkingLevel() }),
382
+ reviewDescription: options.review ? "injected seam" : "real model review",
383
+ reviewChangesOnly: true,
384
+ displayWarning: (details) => {
385
+ pi.sendMessage(createWatchdogWarningMessage(details, { display: true, details }));
386
+ },
387
+ });
388
+
389
+ pi.registerMessageRenderer<WatchdogWarningDetails>(SUBAGENT_WATCHDOG_WARNING_TYPE, (message, renderOptions, theme) => {
390
+ const details = message.details as WatchdogWarningDetails | undefined;
391
+ if (!details?.summary || !details.evidence || !details.recommendedAction) {
392
+ const content = typeof message.content === "string"
393
+ ? message.content
394
+ : message.content.filter((entry) => entry.type === "text").map((entry) => entry.text).join("\n");
395
+ return new Text(content, 0, 0);
396
+ }
397
+ return renderWatchdogWarning(details, renderOptions, theme);
398
+ });
399
+
400
+ pi.registerCommand("subagents-watchdog", {
401
+ description: "Show or toggle the default-off subagent watchdog",
402
+ handler: (args, ctx) => {
403
+ rememberContext(ctx);
404
+ return handleWatchdogCommand(pi, runtime, args, ctx);
405
+ },
406
+ });
407
+
408
+ pi.on("session_start", (_event, ctx) => {
409
+ rememberContext(ctx);
410
+ runtime.bindSession(ctx);
411
+ });
412
+ pi.on("before_agent_start", (event, ctx) => {
413
+ rememberContext(ctx);
414
+ runtime.handleBeforeAgentStart(event, ctx);
415
+ });
416
+ pi.on("turn_end", (event, ctx) => {
417
+ rememberContext(ctx);
418
+ runtime.handleTurnEnd(event, ctx);
419
+ });
420
+ pi.on("agent_end", (event, ctx) => {
421
+ rememberContext(ctx);
422
+ return runtime.handleAgentEnd(event, ctx);
423
+ });
424
+ pi.on("session_before_switch", () => runtime.reset("session switch", { clearReviewInputSignature: true, clearLspLedger: true }));
425
+ pi.on("session_before_fork", () => runtime.reset("session fork", { clearReviewInputSignature: true, clearLspLedger: true }));
426
+ pi.on("session_compact", () => runtime.reset("session compact"));
427
+ pi.on("session_shutdown", () => {
428
+ currentContext = undefined;
429
+ runtime.dispose();
430
+ });
431
+
432
+ return runtime;
433
+ }
@@ -0,0 +1,54 @@
1
+ import { Container, Spacer, Text, type Component } from "@earendil-works/pi-tui";
2
+ import type { WatchdogWarningDetails } from "./types.ts";
3
+
4
+ type WatchdogTheme = {
5
+ fg(name: string, value: string): string;
6
+ bold?(value: string): string;
7
+ };
8
+
9
+ function titleCase(value: string): string {
10
+ return value.split("-").map((part) => part ? `${part[0]?.toUpperCase()}${part.slice(1)}` : part).join(" ");
11
+ }
12
+
13
+ function stateLabels(warning: WatchdogWarningDetails): string[] {
14
+ const labels: string[] = [];
15
+ if (warning.state === "displayed") labels.push("displayed");
16
+ if (warning.stale || warning.state === "stale") labels.push("stale · no auto-follow");
17
+ if (warning.state === "failed") labels.push("failed review");
18
+ if (warning.state === "stalemate") labels.push("stalemate · auto-follow stopped");
19
+ if (warning.autoFollowAttempt !== undefined) labels.push(`auto-follow attempt ${warning.autoFollowAttempt}`);
20
+ return labels;
21
+ }
22
+
23
+ export function formatWatchdogWarningRenderText(warning: WatchdogWarningDetails): string {
24
+ const labels = stateLabels(warning);
25
+ const subject = warning.severity === "blocker" ? "Blocker" : "Concern";
26
+ const lines = [
27
+ `Subagent watchdog ${subject}${labels.length ? ` (${labels.join(", ")})` : ""}: ${warning.summary}`,
28
+ `Evidence: ${warning.evidence}`,
29
+ `Recommended action: ${warning.recommendedAction}`,
30
+ `Category: ${titleCase(warning.category)} · Source: ${warning.source}${warning.agent ? ` · Agent: ${warning.agent}` : ""}${warning.runId ? ` · Run: ${warning.runId}` : ""}`,
31
+ ];
32
+ if (warning.state === "failed" && warning.error) lines.push(`Failure: ${warning.error}`);
33
+ if (warning.state === "stalemate" && warning.stalemateRepeats !== undefined) {
34
+ lines.push(`Auto-follow stopped after ${warning.stalemateRepeats} repeated blocker warning${warning.stalemateRepeats === 1 ? "" : "s"}.`);
35
+ }
36
+ if (warning.stale || warning.state === "stale") lines.push("This warning arrived after the watchdog catch-up timeout and must not auto-follow.");
37
+ return lines.join("\n");
38
+ }
39
+
40
+ export function renderWatchdogWarning(warning: WatchdogWarningDetails, options: { expanded: boolean }, theme: WatchdogTheme): Component {
41
+ const text = formatWatchdogWarningRenderText(warning);
42
+ const lines = text.split("\n");
43
+ const container = new Container();
44
+ const color = warning.severity === "blocker" ? "error" : "warning";
45
+ const bold = theme.bold ?? ((value: string) => value);
46
+ container.addChild(new Text(theme.fg(color, bold(lines[0] ?? "Subagent watchdog warning")), 0, 0));
47
+ if (options.expanded) {
48
+ container.addChild(new Spacer(1));
49
+ for (const line of lines.slice(1)) container.addChild(new Text(theme.fg("dim", line), 0, 0));
50
+ } else if (lines[1]) {
51
+ container.addChild(new Text(theme.fg("dim", ` ⎿ ${lines[1]}`), 0, 0));
52
+ }
53
+ return container;
54
+ }