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 CHANGED
@@ -1,8 +1,31 @@
1
1
  # Changelog
2
2
 
3
-
4
3
  ## [Unreleased]
5
4
 
5
+ ## [0.64.0] - 2026-09-02
6
+
7
+ ### Highlights
8
+ - Watchdog can now warn or block child launches before they start, based on role and model rules.
9
+ - Watchdog reviews are easier to guide with safe diff access, reusable `WATCHDOG.md` instructions, and configurable child review cadence.
10
+ - Watchdog findings are easier to see in parent results, completion notices, acceptance evidence, and Fleet.
11
+ - Workflow status and async results are less noisy and more accurate.
12
+
13
+ ### Added
14
+ - Add watchdog launch rules under `subagents.watchdog.rules`, with per-role model allow and deny globs that warn or block before a child starts.
15
+ - Give watchdog reviewers a read-only `watchdog_diff` tool for session-start diffs, untracked paths, path narrowing, and stat summaries.
16
+ - Run child watchdog reviews on a configurable cadence with `children.cadence` and `children.overrides.<agent>.cadence`.
17
+ - Show child watchdog warnings in parent results, acceptance evidence, completion notices, and Fleet `wd:<n>` chips.
18
+ - Load watchdog reviewer instructions from project and agent `WATCHDOG.md` files.
19
+
20
+ ### Changed
21
+ - Reject unsupported watchdog settings that never took effect: `delivery`, `showDuringRun`, `syncBacklog`, `lateWarningPolicy`, `compactAtPercent`, `reviewRetryDelayMs`, `maxReviewFailures`, `asyncCompletion`, and `guidance.systemPromptPath`.
22
+ - Remove watchdog auto-follow. Pi 0.84+ already continues after displayed boundary warnings, and repeated identical warnings now stop after `subagents.watchdog.stalemateRepeats`. The `autoFollow` settings block is now unknown.
23
+
24
+ ### Fixed
25
+ - Keep advisory preflight checks out of runtime workflow rows and queued checklist counts (#1821). Thanks [@stekman08](https://github.com/stekman08).
26
+ - Preserve effective thinking in completed async step results. Thanks to [@Nickonomic](https://github.com/Nickonomic) for #1823.
27
+ - Forward workflow child control overrides through new and retained launches, and suppress idle needs-attention notices before the first assistant turn (#1817). Thanks [@rrocxela](https://github.com/rrocxela).
28
+
6
29
  ## [0.63.0] - 2026-09-01
7
30
 
8
31
  ### Highlights
package/docs/watchdog.md CHANGED
@@ -1,145 +1,142 @@
1
1
  # Watchdog and child permissions
2
2
 
3
- The watchdog is an opt-in adversarial reviewer for repo edits. This page covers what it reviews, how to pick its model, scope monitoring, LSP diagnostics, and the native child tool permission gate that uses the child watchdog as its arbiter.
3
+ The watchdog is an opt-in second model that reviews what the agent just did and pushes findings back into the transcript. It looks for missed constraints, correctness risks, test gaps, unsafe changes, loop risks, and scope drift, and says nothing when the turn is clean. It is not the `reviewer` subagent; `subagents.defaultModel` and `agentOverrides.reviewer` do not configure it.
4
4
 
5
- ## What the watchdog reviews
5
+ ## When it runs
6
6
 
7
- The watchdog is not the `reviewer` subagent. `subagents.defaultModel` and `subagents.agentOverrides.reviewer` do not configure it.
7
+ | Timing | Trigger | Gate | Delivery |
8
+ |---|---|---|---|
9
+ | Boundary review | `agent_end` of every main or child turn | Repo changed | Steered into the transcript; the agent gets one continuation, then that turn is reviewed again |
10
+ | Cadence review | Every `cadence.everyNTools` tool results, minimum 5 | Opt-in | Steered after the current tool, before the next step |
11
+ | LSP pre-pass | Before boundary review | Changed TypeScript/JavaScript files | Diagnostics become watchdog findings without a model call |
8
12
 
9
- It reviews repo edits, not ordinary conversation:
13
+ Boundary reviews coalesce a turn's edits into one final-state review. Unchanged or reverted diffs are skipped, as are `.pi/subagents/` and `tmp/` artifacts. In orchestrated runs, each writing child reviews its own worktree and the parent reviews the aggregate diff after child changes land. There is no timer or "every turn regardless of edits" mode; the closest is a low cadence such as `everyNTools: 5`. Cadence monitoring is inspired by [Scopey](https://github.com/ArchAstro/scopey).
10
14
 
11
- - It runs at the safe `agent_end` boundary, only when the current agent or child writer changed the final repo state since the start of that turn.
12
- - Multiple edits in one turn are coalesced into one review of the final changed state.
13
- - Unchanged/reverted diffs are skipped.
14
- - Generated `.pi/subagents/` or `tmp/` artifacts do not trigger review.
15
- - In orchestrated runs, each writing child can review its own edited worktree, and the parent can still review the aggregate repo diff after child changes are applied.
15
+ Children get the same boundary, cadence, and LSP behavior. Child cadence resolves from `children.overrides.<agent>.cadence`, then `children.cadence`, then top-level `cadence`:
16
16
 
17
- ## Choosing a model
17
+ ```json
18
+ {
19
+ "subagents": {
20
+ "watchdog": {
21
+ "enabled": true,
22
+ "cadence": { "everyNTools": 10 },
23
+ "children": {
24
+ "enabled": true,
25
+ "cadence": { "everyNTools": 20 },
26
+ "overrides": {
27
+ "worker": { "cadence": { "everyNTools": 5 } },
28
+ "reviewer": { "enabled": false }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+ ```
18
35
 
19
- Because the watchdog is an adversarial change reviewer, it should usually use a strong complementary model rather than a cheap/light one.
36
+ That means: main every 10 tools, worker every 5, other children every 20, reviewer never.
20
37
 
21
- Ask pi-subagents for the current strong pairing:
38
+ ## What you see
22
39
 
23
- ```text
24
- /subagents-watchdog recommend-model
25
- /subagents-watchdog session model recommended
26
- /subagents-watchdog model recommended
40
+ Every finding is an ordinary transcript message: expandable, scrollable, and persisted in session JSONL. A clean review shows nothing.
41
+
42
+ ```
43
+ you ─▶ agent turn ─▶ edits repo ─▶ agent_end ─▶ watchdog review
44
+ ├─ clean: turn ends
45
+ └─ warning: steered in; agent continues once
27
46
  ```
28
47
 
29
- The current recommendation policy is Opus 4.8 with thinking high or GPT 5.5 with thinking high. If your main session is using one, the watchdog should use the other when that model is authenticated.
48
+ Collapsed warnings show the title and evidence line. Expanded warnings show evidence, recommended action, category, and source:
30
49
 
31
- - `session model recommended` changes only the current Pi session.
32
- - `model recommended` saves the recommendation to `~/.pi/agent/settings.json`. It does not turn the watchdog on; enable it separately with `/subagents-watchdog on`.
50
+ ```
51
+ Subagent watchdog Blocker (displayed): Claims tests passed without running them
52
+ Evidence: The transcript claims `npm test` passed but no test command appears in the tool log.
53
+ Recommended action: Run the focused test before finishing.
54
+ Category: Test Gap · Source: main
55
+ ```
33
56
 
34
- Or set the model explicitly:
57
+ When consecutive boundary reviews raise the same warning, the agent is not making progress. After `stalemateRepeats` identical warnings in a row (default 3), the warning is shown as `stalemate`, no continuation is triggered, and the turn ends. Your next prompt resets the count.
35
58
 
36
- ```text
37
- /subagents-watchdog model anthropic/claude-opus-4-8:high
38
- /subagents-watchdog model openai-codex/gpt-5.5:high
39
- /subagents-watchdog model inherit
40
- /subagents-watchdog check
41
- ```
59
+ Child watchdog findings are lifted into the parent in three ways:
42
60
 
43
- In settings files, use `subagents.watchdog.main.model` and `subagents.watchdog.main.thinking` for the main watchdog:
61
+ - The result envelope contains `watchdog.warnings` with severity, category, summary, evidence, recommended action, `addressed`, and `stalemate`, bounded to the last 20.
62
+ - The acceptance runtime check `watchdog-blocker` fails on blockers that are unaddressed or stalemate.
63
+ - Completion notices include `Watchdog blockers:` lines, and Fleet/status views show `wd:<n>` plus `resolve watchdog blockers`.
44
64
 
45
- - If `main.model` is omitted, the main watchdog uses the current session model and thinking level.
46
- - If `main.model` is set without a thinking suffix or `main.thinking`, it runs with thinking off. Prefer `:high` or `"thinking": "high"` for the strong-watchdog pairing.
65
+ `/subagents-watchdog status` shows setting sources, enabled state, runtime state, review trigger, scope, cadence, LSP status, selected model/thinking, child overrides, timeout, stalemate count, launch-rule count, review backend, last warning, changed paths, and config errors when present.
47
66
 
48
- Default strong-reviewer profile:
67
+ ## What the reviewer is given
49
68
 
50
- ```json
51
- {
52
- "subagents": {
53
- "watchdog": {
54
- "enabled": true,
55
- "main": {
56
- "model": "anthropic/claude-opus-4-8",
57
- "thinking": "high"
58
- }
59
- }
60
- }
61
- }
62
- ```
69
+ - **Turn delta** with changed repo paths. Over-long input keeps the first 6,000 characters and the tail.
70
+ - **Current scope** (`scope.enabled`, default on): bounded real user prompts, with newer prompts superseding older ones.
71
+ - **`watchdog_diff`** when inside git: diff since the session-start commit, including later commits, plus untracked paths to inspect with `read`; accepts `path` and `stat:true`.
72
+ - **`WATCHDOG.md`** standing instructions, read fresh on every review: `<project>/.pi/WATCHDOG.md` first, then `~/.pi/agent/WATCHDOG.md`, capped at 8,000 characters. Set `guidance.watchdogMd: false` to ignore them.
73
+ - **LSP diagnostics** from `typescript-language-server`, auto-detected in `node_modules/.bin` or `PATH`; it is never installed and never run over the whole workspace. Errors become blockers, warnings concerns, and info/hints stay in status.
63
74
 
64
- ## Scope monitoring
75
+ ## Choosing a model
65
76
 
66
- When enabled, the watchdog keeps a bounded in-memory current-scope artifact from real user prompts and prepends it to review input by default (`subagents.watchdog.scope.enabled`). Newer prompts supersede and mutate older prompts, so the reviewer can flag work that no longer serves the current scope as `scope-drift`. Watchdog auto-follow prompts are not recorded as scope.
77
+ One model setting serves both boundary and cadence reviews per endpoint. Use a strong complementary model for rare adversarial boundary reviews, or a cheap one for frequent cadence monitoring.
67
78
 
68
- You can opt into Scopey-style scope monitoring, inspired by [Scopey](https://github.com/ArchAstro/scopey), by setting `subagents.watchdog.cadence.everyNTools` to run additional non-blocking reviews every N tool results. Cadence warnings are transcript-visible and delivered with Pi's `steer` mode after the current tool boundary; they are never hidden. The same configured watchdog model is used for all checks, so choose a cheap model for frequent monitoring or a strong model for rarer adversarial review.
79
+ ```text
80
+ /subagents-watchdog recommend-model
81
+ /subagents-watchdog session model recommended
82
+ /subagents-watchdog model recommended
83
+ /subagents-watchdog model anthropic/claude-opus-4-8:high
84
+ /subagents-watchdog model openai-codex/gpt-5.5:high
85
+ /subagents-watchdog model inherit
86
+ /subagents-watchdog check
87
+ /subagents-watchdog on
88
+ ```
69
89
 
70
- Scopey-style profile:
90
+ The recommendation is Opus 4.8 or GPT 5.5 at thinking high, whichever your main session is not using and is authenticated. Saving a model does not enable the watchdog; use `on` separately.
71
91
 
72
92
  ```json
73
93
  {
74
94
  "subagents": {
75
95
  "watchdog": {
76
96
  "enabled": true,
77
- "main": {
78
- "model": "anthropic/claude-haiku-4-5",
79
- "thinking": "medium"
80
- },
97
+ "main": { "model": "anthropic/claude-opus-4-8", "thinking": "high" },
81
98
  "scope": { "enabled": true },
82
99
  "cadence": { "everyNTools": 10 },
83
- "autoFollow": {
84
- "blockers": true,
85
- "maxAttempts": 3,
86
- "stalemateRepeats": 3
87
- }
100
+ "stalemateRepeats": 3
88
101
  }
89
102
  }
90
103
  }
91
104
  ```
92
105
 
93
- ## Auto-follow
94
-
95
- When the watchdog displays a blocker at `agent_end`, the `subagents.watchdog.autoFollow` policy can queue a visible follow-up user message asking the agent to address it. Auto-follow only runs while the watchdog is enabled, respects `maxAttempts`, and stops on repeated identical blockers using `stalemateRepeats`.
96
-
97
- ## LSP diagnostics
98
-
99
- When the watchdog is enabled, it also checks changed TypeScript and JavaScript files for fresh language-server diagnostics before the model review.
106
+ Omit `main.model` to inherit the session model and thinking level. A `main.model` without a thinking suffix or `main.thinking` runs with thinking off, so prefer `:high` for the strong pairing.
100
107
 
101
- - It auto-detects `typescript-language-server` from the project `node_modules/.bin` or `PATH`. It never installs tools or scans the whole workspace.
102
- - LSP errors surface as watchdog blockers, warnings as concerns, and info/hints stay in status details.
103
- - Slow or missing servers are reported in `/subagents-watchdog status` without blocking the turn or emitting late mid-turn warnings.
104
- - Configure the bounds with `subagents.watchdog.lsp.enabled`, `timeoutMs`, `maxFiles`, and `maxDiagnostics`.
108
+ Agents can call `subagent({ action: "watchdog.recommend-model" })` and `subagent({ action: "watchdog.configure", model: "recommended", scope: "session" | "user" | "project" })`. They should use `scope: "session"` unless you ask for a lasting default.
105
109
 
106
110
  ## Child watchdogs
107
111
 
108
- For child subagent watchdogs, use `subagents.watchdog.children.model` as the default child watchdog model, or `subagents.watchdog.children.overrides.<agent>.model` for a specific child role.
109
-
110
- Child watchdogs are opt-in and follow the same edit-gated rule: read-only children do not trigger watchdog reviews, while writer children are reviewed at their own `agent_end` if their worktree changed.
111
-
112
- ## Agent-driven configuration
113
-
114
- Agents can configure the same values through the tool when you ask them to set up the watchdog:
115
-
116
- ```ts
117
- subagent({ action: "watchdog.recommend-model" })
118
- subagent({ action: "watchdog.configure", model: "recommended", scope: "session" })
119
- subagent({ action: "watchdog.configure", model: "recommended", scope: "project" })
120
- ```
121
-
122
- Persistent scopes (`user` or `project`) should only be used when you ask for a lasting default. Otherwise the agent should use `scope: "session"`.
112
+ Opt in under `subagents.watchdog.children`. `model` and `thinking` set the default child watchdog; `overrides.<agent>` can set `model`, `thinking`, `enabled`, or `cadence` per role.
123
113
 
124
- ## Native child tool permissions
114
+ ## Launch rules
125
115
 
126
- Native permissions are opt-in and apply only to Pi child runtimes. With no rules configured, every tool call passes through unchanged.
127
-
128
- Configure explicit non-bash rules globally in `~/.pi/agent/extensions/subagent/config.json`:
116
+ `subagents.watchdog.rules` pins which models each role may run on. It runs before a child starts, needs no model call, and applies even when model review is off.
129
117
 
130
118
  ```json
131
119
  {
132
- "permissions": {
133
- "rules": {
134
- "read": "allow",
135
- "write": "ask",
136
- "edit": "deny"
120
+ "subagents": {
121
+ "watchdog": {
122
+ "rules": {
123
+ "action": "warn",
124
+ "roleModels": {
125
+ "scout": { "allow": ["openai-codex/gpt-5.6-luna:max"] },
126
+ "oracle": { "deny": ["*"], "note": "oracle is for hard questions only; ask before launching" },
127
+ "worker": { "deny": ["openai-codex/gpt-5.6-sol:high"] }
128
+ }
129
+ }
137
130
  }
138
131
  }
139
132
  }
140
133
  ```
141
134
 
142
- Custom agents can override matching global rules with a `permission:` or `permissions:` frontmatter block:
135
+ `action: "warn"` steers a concern into the orchestrator transcript and lets the launch proceed. `action: "block"` returns a tool error and starts nothing. `allow` and `deny` are anchored, case-sensitive globs (`*`, `?`) matched against `provider/id[:thinking]` and bare `provider/id`; `deny` wins. Rules apply to direct launches, workflow children, and chain/parallel steps using settings visible at the launch cwd.
136
+
137
+ ## Native child tool permissions
138
+
139
+ Opt-in, Pi child runtimes only. With no rules, every tool call passes through. Global non-bash rules live in `~/.pi/agent/extensions/subagent/config.json`; agents override matching rules in `permission:` or `permissions:` frontmatter:
143
140
 
144
141
  ```yaml
145
142
  ---
@@ -150,27 +147,8 @@ permission:
150
147
  ---
151
148
  ```
152
149
 
153
- Rules support `allow`, `ask`, and `deny`:
154
-
155
- - Agent rules override matching global rules.
156
- - Omitted and unknown tools default to `allow`.
157
- - Explicit `allow` removes an inherited restriction.
158
- - The gate is not registered when the resolved policy has no `ask` or `deny` rules.
159
-
160
- ### How `ask` works
161
-
162
- An explicit `ask` pauses that exact tool call and sends a bounded, redacted preview to a one-call permission arbiter owned by the built-in child watchdog. The arbiter uses the configured child-watchdog model and returns only `approve` or `deny`; it does not notify the parent agent.
163
-
164
- Enable and configure `subagents.watchdog.children` before using `ask` rules. A disabled watchdog, missing model/auth, timeout, malformed response, or runtime error denies the call with a clear error.
165
-
166
- Asked requests and decisions are written to bounded audit JSONL, including `decisionSource: "watchdog"` and bounded failure reasons. Ordinary direction and clarification through `contact_supervisor` or the optional `pi-intercom` extension remain separate and are never permission-gated.
167
-
168
- ### Bash is out of scope
169
-
170
- `bash` is always passed through by pi-subagents. Bash rules are rejected rather than parsed, gated, denied, or audited. Install and configure `pi-guard` when command-level bash policy is needed.
171
-
172
- A pi-subagents child is headless, so a pi-guard rule that resolves to `ask` cannot request approval from the parent Pi UI. Native permissions do not forward pi-guard decisions; they only apply to the separate non-bash child permission gate. For child-specific policy, use `PI_GUARD` through a `PI_SUBAGENT_PI_BINARY` wrapper or an equivalent launch wrapper, and configure explicit `allow` or `deny` rules. An `allow` rule grants execution; it is not approval forwarding, so retain explicit denies for commands the child must not run.
150
+ Values are `allow`, `ask`, and `deny`. Agent rules override global ones, omitted and unknown tools default to `allow`, an explicit `allow` removes an inherited restriction, and the gate is not registered when the resolved policy has no `ask` or `deny`.
173
151
 
174
- ### External CLI profiles
152
+ `ask` pauses that exact tool call and sends a bounded, redacted preview to a one-call arbiter owned by the child watchdog, using the configured child-watchdog model. The arbiter returns only `approve` or `deny` and does not notify the parent. A disabled watchdog, missing model/auth, timeout, malformed response, or runtime error denies the call with a clear error. Requests and decisions are written to bounded audit JSONL. `contact_supervisor` and the optional `pi-intercom` extension are never permission-gated.
175
153
 
176
- External CLI profiles are opaque processes, so native permissions cannot intercept their tools. A launch with effective `ask` or `deny` rules is rejected for an external CLI agent instead of claiming enforcement.
154
+ Bash is always passed through; bash rules are rejected. Use `pi-guard` for command-level policy. For child-specific command policy, run `PI_GUARD` through a `PI_SUBAGENT_PI_BINARY` wrapper with explicit `allow` or `deny` rules. External CLI profiles are opaque processes, so native permissions cannot intercept their tools; launches with effective `ask` or `deny` rules are rejected for external CLI agents.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.63.0",
3
+ "version": "0.64.0",
4
4
  "description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -337,9 +337,10 @@ child changes land. Enabled watchdogs also run changed-file TypeScript/JavaScrip
337
337
  LSP diagnostics before the model pass when `typescript-language-server` is available.
338
338
  They keep bounded current-scope context from real user prompts (`watchdog.scope.enabled`)
339
339
  and can optionally run non-blocking Scopey-style cadence reviews every N tool results
340
- (`watchdog.cadence.everyNTools`). Cadence corrections and blocker auto-follow prompts
341
- are always transcript-visible; choose the watchdog model that matches the desired
342
- cheap-monitor vs strong-reviewer policy.
340
+ (`watchdog.cadence.everyNTools`). Cadence corrections and boundary warnings are always
341
+ transcript-visible; a boundary warning continues the run so the agent can act on it, and
342
+ repeated identical warnings stop after `watchdog.stalemateRepeats`. Choose the watchdog
343
+ model that matches the desired cheap-monitor vs strong-reviewer policy.
343
344
 
344
345
  Prefer a strong complementary model (for example Opus 4.8 high paired against a
345
346
  GPT 5.5 main session, or the reverse). Recommendation and configuration:
@@ -16,6 +16,7 @@ import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
16
16
  import { planChildLaunch, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
17
17
  import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
18
18
  import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
19
+ import { applyWatchdogLaunchRules, sendRuleViolationWarning } from "../../watchdog/rules.ts";
19
20
  import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadInstructionPaths, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
20
21
  import type { RunnerStep } from "../shared/parallel-utils.ts";
21
22
  import type { ContextMode } from "../shared/context-mode.ts";
@@ -939,6 +940,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
939
940
  throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
940
941
  }
941
942
  }
943
+ const launchRuleError = applyWatchdogLaunchRules({ cwd: stepCwd, agent: a.name, model: modelCandidates[0] ?? model, warn: (violation) => sendRuleViolationWarning(ctx.pi, violation) });
944
+ if (launchRuleError) throw new AsyncStartValidationError(launchRuleError);
942
945
  const fast = s.fast ?? params.fast ?? a.fast;
943
946
  const toolPlan = resolvePiLaunchToolPlan({
944
947
  tools: a.tools,
@@ -14,7 +14,7 @@ import {
14
14
  createCompletionBatcher,
15
15
  resolveCompletionBatchConfig,
16
16
  } from "./completion-batcher.ts";
17
- import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, type ParallelHandoffReference, type ScheduleOrigin, type SubagentState } from "../../shared/types.ts";
17
+ import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, type ChildWatchdogProgress, type ChildWatchdogWarningSummary, type ParallelHandoffReference, type ScheduleOrigin, type SubagentState } from "../../shared/types.ts";
18
18
  import { safeTerminalText } from "../../shared/display-text.ts";
19
19
  import { resolveSubagentResultStatus } from "../../intercom/result-intercom.ts";
20
20
  import { isUnexplainedProcessSignal } from "../shared/process-signal.ts";
@@ -31,6 +31,8 @@ export interface SubagentNotifyChildOutput {
31
31
  previewUnavailableReason?: string;
32
32
  }
33
33
 
34
+ export type SubagentNotifyWatchdogBlocker = Pick<ChildWatchdogWarningSummary, "summary" | "addressed" | "stalemate"> & { agent: string };
35
+
34
36
  export interface SubagentNotifyDetails {
35
37
  agent: string;
36
38
  status: "completed" | "failed" | "paused" | "stopped";
@@ -47,6 +49,7 @@ export interface SubagentNotifyDetails {
47
49
  handoffPath?: string;
48
50
  /** Present when a durable schedule launched the run. */
49
51
  scheduleOrigin?: ScheduleOrigin;
52
+ watchdogBlockers?: SubagentNotifyWatchdogBlocker[];
50
53
  }
51
54
 
52
55
  export interface CompletionNotification {
@@ -85,7 +88,9 @@ export interface CompletionNotification {
85
88
  timedOut?: boolean;
86
89
  stopped?: boolean;
87
90
  turnBudgetExceeded?: boolean;
91
+ watchdog?: ChildWatchdogProgress;
88
92
  }>;
93
+ watchdog?: ChildWatchdogProgress;
89
94
  timestamp?: number;
90
95
  durationMs?: number;
91
96
  cwd?: string;
@@ -249,9 +254,28 @@ function formatCorrelationLines(details: SubagentNotifyDetails): string[] {
249
254
  ].filter((line): line is string => line !== undefined);
250
255
  }
251
256
 
257
+ const WATCHDOG_BLOCKERS_HEADING = "Watchdog blockers:";
258
+
259
+ function formatWatchdogBlockerLines(details: SubagentNotifyDetails): string[] {
260
+ if (!details.watchdogBlockers?.length) return [];
261
+ return [WATCHDOG_BLOCKERS_HEADING, ...details.watchdogBlockers.map((blocker) => `- ${blocker.agent}: ${blocker.summary} (${blocker.stalemate ? "stalemate" : blocker.addressed ? "addressed" : "unaddressed"})`)];
262
+ }
263
+
264
+ // A stalemate blocker parses back as unaddressed; acceptance treats both as unresolved.
265
+ function parseWatchdogBlockerLines(lines: string[]): SubagentNotifyWatchdogBlocker[] {
266
+ const blockers: SubagentNotifyWatchdogBlocker[] = [];
267
+ for (const line of lines) {
268
+ const match = line.match(/^- (.+?): (.+) \((addressed|unaddressed|stalemate)\)$/);
269
+ if (!match) break;
270
+ blockers.push({ agent: match[1]!, summary: match[2]!, addressed: match[3] === "addressed", stalemate: match[3] === "stalemate" });
271
+ }
272
+ return blockers;
273
+ }
274
+
252
275
  export function formatSingleCompletion(details: SubagentNotifyDetails): string {
253
276
  const sessionLine = formatSessionLine(details);
254
277
  const correlationLines = formatCorrelationLines(details);
278
+ const watchdogLines = formatWatchdogBlockerLines(details);
255
279
  const taskKind = details.source === "foreground" ? "Detached foreground task" : "Background task";
256
280
  const scheduleLine = details.scheduleOrigin
257
281
  ? `Scheduled run from **${details.scheduleOrigin.name ?? details.scheduleOrigin.id}** (schedule ${details.scheduleOrigin.id}).`
@@ -262,6 +286,7 @@ export function formatSingleCompletion(details: SubagentNotifyDetails): string {
262
286
  scheduleLine,
263
287
  scheduleLine ? "" : undefined,
264
288
  formatResultPreview(details),
289
+ ...(watchdogLines.length ? ["", ...watchdogLines] : []),
265
290
  details.handoffPath ? "" : undefined,
266
291
  details.handoffPath ? `Parallel handoff: ${details.handoffPath}` : undefined,
267
292
  correlationLines.length && !details.handoffPath ? "" : undefined,
@@ -300,7 +325,9 @@ export function parseSubagentNotifyContent(content: string): SubagentNotifyDetai
300
325
  const workflowRunIndex = body.findIndex((line) => line.startsWith("Workflow run: "));
301
326
  const childRunsIndex = body.findIndex((line) => line.startsWith("Child runs: "));
302
327
  const reconciledIndex = body.findIndex((line) => line.startsWith("Reconciled detached child: "));
303
- const metadataIndexes = [sessionIndex, handoffIndex, workflowRunIndex, childRunsIndex, reconciledIndex].filter((index) => index >= 0);
328
+ const watchdogIndex = body.findIndex((line) => line === WATCHDOG_BLOCKERS_HEADING);
329
+ const watchdogBlockers = watchdogIndex >= 0 ? parseWatchdogBlockerLines(body.slice(watchdogIndex + 1)) : [];
330
+ const metadataIndexes = [sessionIndex, handoffIndex, workflowRunIndex, childRunsIndex, reconciledIndex, watchdogIndex].filter((index) => index >= 0);
304
331
  const firstMetadataIndex = metadataIndexes.length ? Math.min(...metadataIndexes) : body.length;
305
332
  const resultEnd = firstMetadataIndex > 0 && body[firstMetadataIndex - 1]?.trim() === "" ? firstMetadataIndex - 1 : firstMetadataIndex;
306
333
  const resultPreview = body.slice(0, resultEnd).join("\n").trim() || "(no output)";
@@ -336,6 +363,7 @@ export function parseSubagentNotifyContent(content: string): SubagentNotifyDetai
336
363
  ...(workflowRunId ? { workflowRunId } : {}),
337
364
  ...(childRuns?.length ? { childRuns } : {}),
338
365
  ...(reconciledFromDetachedChild ? { reconciledFromDetachedChild } : {}),
366
+ ...(watchdogBlockers.length ? { watchdogBlockers } : {}),
339
367
  ...(sessionLabel && sessionValue ? { sessionLabel, sessionValue } : {}),
340
368
  };
341
369
  }
@@ -349,6 +377,7 @@ export function formatGroupedCompletion(details: SubagentNotifyDetails[]): strin
349
377
  const sessionLine = formatSessionLine(detail);
350
378
  blocks.push(`${index + 1}. ${detail.agent}${detail.taskInfo ?? ""}${detail.scheduleOrigin ? ` — scheduled run from ${detail.scheduleOrigin.name ?? detail.scheduleOrigin.id} (schedule ${detail.scheduleOrigin.id})` : ""}`);
351
379
  blocks.push(formatResultPreview(detail));
380
+ blocks.push(...formatWatchdogBlockerLines(detail));
352
381
  if (detail.handoffPath) blocks.push(`Parallel handoff: ${detail.handoffPath}`);
353
382
  blocks.push(...formatCorrelationLines(detail));
354
383
  if (sessionLine) blocks.push(sessionLine);
@@ -462,6 +491,15 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
462
491
  })
463
492
  : undefined;
464
493
  const reconciledFromDetachedChild = typeof result.reconciledFromDetachedChild === "string" ? result.reconciledFromDetachedChild : undefined;
494
+ const watchdogBlockers: SubagentNotifyWatchdogBlocker[] = [];
495
+ const collectWatchdogBlockers = (owner: string, progress: ChildWatchdogProgress | undefined) => {
496
+ for (const warning of progress?.warnings ?? []) {
497
+ if (warning.severity !== "blocker") continue;
498
+ watchdogBlockers.push({ agent: owner, summary: warning.summary, addressed: warning.addressed, stalemate: warning.stalemate });
499
+ }
500
+ };
501
+ collectWatchdogBlockers(agent, result.watchdog);
502
+ for (const child of result.results ?? []) collectWatchdogBlockers(typeof child.agent === "string" ? child.agent : agent, child.watchdog);
465
503
  const session =
466
504
  result.shareUrl
467
505
  ? { label: "Session", value: result.shareUrl }
@@ -487,6 +525,7 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
487
525
  ...(childRuns.length ? { childRuns } : {}),
488
526
  ...(childOutputs?.length ? { childOutputs } : {}),
489
527
  ...(reconciledFromDetachedChild ? { reconciledFromDetachedChild } : {}),
528
+ ...(watchdogBlockers.length ? { watchdogBlockers } : {}),
490
529
  ...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),
491
530
  };
492
531
  }
@@ -156,6 +156,7 @@ import { decodeSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type
156
156
  import {
157
157
  CHILD_WATCHDOG_CONFIG_ENV,
158
158
  acceptChildWatchdogEvent,
159
+ applyChildWatchdogMessage,
159
160
  childWatchdogIsActive,
160
161
  decodeChildWatchdogConfig,
161
162
  isChildWatchdogStatusEvent,
@@ -248,6 +249,7 @@ interface StepResult {
248
249
  sessionFile?: string;
249
250
  intercomTarget?: string;
250
251
  model?: string;
252
+ thinking?: string;
251
253
  attemptedModels?: string[];
252
254
  modelAttempts?: ModelAttempt[];
253
255
  /** True when the dispatch failed because the input exceeded the model's context window. */
@@ -853,6 +855,10 @@ function runPiStreaming(
853
855
  const text = extractTextFromContent(event.message.content);
854
856
  if (text) writeOutputText(text);
855
857
 
858
+ if (childWatchdogConfig && event.type === "message_end") {
859
+ const next = applyChildWatchdogMessage(childWatchdogState, event.message);
860
+ if (next) updateChildWatchdogState(next);
861
+ }
856
862
  if (event.type !== "message_end" || event.message.role !== "assistant") return;
857
863
  const hasToolCall = assistantStartsToolCall(event.message);
858
864
  if (event.message.model) {
@@ -1069,7 +1075,6 @@ function runPiStreaming(
1069
1075
  phase: "stale",
1070
1076
  seq: (childWatchdogState?.seq ?? 0) + 1,
1071
1077
  lastUpdate: Date.now(),
1072
- followUpPending: false,
1073
1078
  reason: "child watchdog tail timeout",
1074
1079
  timedOut: true,
1075
1080
  });
@@ -2167,6 +2172,7 @@ async function runSingleStepInner(
2167
2172
  reportOptional: isAgentContractV1(step.agentContract),
2168
2173
  artifactsDir: ctx.artifactsDir,
2169
2174
  runId: ctx.id,
2175
+ watchdog: finalResult?.watchdog,
2170
2176
  }))
2171
2177
  : undefined;
2172
2178
  const stoppedAfterAcceptance = finalResult?.stopped === true || ctx.stopSignal?.aborted === true;
@@ -2244,6 +2250,7 @@ async function runSingleStepInner(
2244
2250
  sessionFile: step.sessionFile,
2245
2251
  intercomTarget: ctx.childIntercomTarget,
2246
2252
  model: finalResult?.model,
2253
+ thinking: resolveEffectiveThinking(finalResult?.model, step.thinking),
2247
2254
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
2248
2255
  modelAttempts,
2249
2256
  contextOverflow: contextOverflow || undefined,
@@ -2905,6 +2912,7 @@ async function runSubagent(
2905
2912
  success: statusResultSuccess(state, step),
2906
2913
  sessionFile: step.sessionFile,
2907
2914
  model: step.model,
2915
+ thinking: step.thinking,
2908
2916
  attemptedModels: step.attemptedModels,
2909
2917
  modelAttempts: step.modelAttempts,
2910
2918
  usage: usageFromAttempts(step.modelAttempts),
@@ -3584,6 +3592,15 @@ async function runSubagent(
3584
3592
  writeStatusPayload(false);
3585
3593
  return;
3586
3594
  }
3595
+ if (event.type === "message_end") {
3596
+ const next = applyChildWatchdogMessage(step.watchdog, event.message, now);
3597
+ if (next) step.watchdog = next;
3598
+ if (next && (event.message as { role?: unknown } | undefined)?.role === "custom") {
3599
+ statusPayload.lastUpdate = now;
3600
+ writeStatusPayload(false);
3601
+ return;
3602
+ }
3603
+ }
3587
3604
  if (event.type === "tool_execution_start" && event.toolName) {
3588
3605
  const mutates = isMutatingTool(event.toolName, event.args, flatSteps[flatIndex]?.mutationTools);
3589
3606
  const currentPath = resolveCurrentPath(event.toolName, event.args);
@@ -3728,6 +3745,7 @@ async function runSubagent(
3728
3745
  config: controlConfig,
3729
3746
  startedAt: step.startedAt ?? overallStartTime,
3730
3747
  lastActivityAt,
3748
+ turnCount: step.turnCount,
3731
3749
  currentTool: step.currentTool,
3732
3750
  thinking: step.thinking,
3733
3751
  now,
@@ -4349,6 +4367,7 @@ async function runSubagent(
4349
4367
  sessionFile: pr.sessionFile,
4350
4368
  intercomTarget: pr.intercomTarget,
4351
4369
  model: pr.model,
4370
+ thinking: pr.thinking,
4352
4371
  attemptedModels: pr.attemptedModels,
4353
4372
  modelAttempts: pr.modelAttempts,
4354
4373
  contextOverflow: pr.contextOverflow,
@@ -4794,6 +4813,7 @@ async function runSubagent(
4794
4813
  sessionFile: pr.sessionFile,
4795
4814
  intercomTarget: pr.intercomTarget,
4796
4815
  model: pr.model,
4816
+ thinking: pr.thinking,
4797
4817
  attemptedModels: pr.attemptedModels,
4798
4818
  modelAttempts: pr.modelAttempts,
4799
4819
  contextOverflow: pr.contextOverflow,
@@ -5053,6 +5073,7 @@ async function runSubagent(
5053
5073
  sessionFile: singleResult.sessionFile,
5054
5074
  intercomTarget: singleResult.intercomTarget,
5055
5075
  model: singleResult.model,
5076
+ thinking: singleResult.thinking,
5056
5077
  attemptedModels: singleResult.attemptedModels,
5057
5078
  modelAttempts: singleResult.modelAttempts,
5058
5079
  contextOverflow: singleResult.contextOverflow,
@@ -5416,6 +5437,7 @@ async function runSubagent(
5416
5437
  sessionFile: r.sessionFile,
5417
5438
  intercomTarget: r.intercomTarget,
5418
5439
  model: r.model,
5440
+ thinking: r.thinking,
5419
5441
  attemptedModels: r.attemptedModels,
5420
5442
  modelAttempts: r.modelAttempts,
5421
5443
  contextOverflow: r.contextOverflow,
@@ -115,6 +115,7 @@ import { consumeWorkflowChildPermit } from "../../shared/workflow-child-permit.t
115
115
  import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, PI_AGGREGATE_EVENT_PROJECTOR, projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleState, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
116
116
  import {
117
117
  acceptChildWatchdogEvent,
118
+ applyChildWatchdogMessage,
118
119
  childWatchdogIsActive,
119
120
  isChildWatchdogStatusEvent,
120
121
  resolveChildWatchdogConfig,
@@ -779,7 +780,6 @@ async function runSingleAttempt(
779
780
  phase: "stale",
780
781
  seq: (childWatchdogState?.seq ?? 0) + 1,
781
782
  lastUpdate: Date.now(),
782
- followUpPending: false,
783
783
  reason: "child watchdog tail timeout",
784
784
  timedOut: true,
785
785
  });
@@ -965,6 +965,7 @@ async function runSingleAttempt(
965
965
  config: controlConfig,
966
966
  startedAt: startTime,
967
967
  lastActivityAt: progress.lastActivityAt,
968
+ turnCount: progress.turnCount,
968
969
  currentTool: progress.currentTool,
969
970
  thinking: resolvedThinking,
970
971
  now,
@@ -1119,6 +1120,10 @@ async function runSingleAttempt(
1119
1120
 
1120
1121
  if (evt.type === "message_end" && evt.message) {
1121
1122
  result.messages!.push(evt.message);
1123
+ if (childWatchdog) {
1124
+ const next = applyChildWatchdogMessage(childWatchdogState, evt.message);
1125
+ if (next) updateChildWatchdogState(next);
1126
+ }
1122
1127
  if (evt.message.role === "assistant") {
1123
1128
  result.usage.turns++;
1124
1129
  progress.turnCount = result.usage.turns;
@@ -2231,6 +2236,7 @@ async function runSyncCompletionInner(
2231
2236
  reportOptional: isAgentContractV1(options.agentContract),
2232
2237
  artifactsDir: options.artifactsDir,
2233
2238
  runId: options.runId,
2239
+ watchdog: result.watchdog,
2234
2240
  });
2235
2241
  }
2236
2242
  } catch (error) {