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
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
|
|
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
|
-
##
|
|
5
|
+
## When it runs
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
That means: main every 10 tools, worker every 5, other children every 20, reviewer never.
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
## What you see
|
|
22
39
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
48
|
+
Collapsed warnings show the title and evidence line. Expanded warnings show evidence, recommended action, category, and source:
|
|
30
49
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
67
|
+
## What the reviewer is given
|
|
49
68
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
##
|
|
75
|
+
## Choosing a model
|
|
65
76
|
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
84
|
-
"blockers": true,
|
|
85
|
-
"maxAttempts": 3,
|
|
86
|
-
"stalemateRepeats": 3
|
|
87
|
-
}
|
|
100
|
+
"stalemateRepeats": 3
|
|
88
101
|
}
|
|
89
102
|
}
|
|
90
103
|
}
|
|
91
104
|
```
|
|
92
105
|
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
114
|
+
## Launch rules
|
|
125
115
|
|
|
126
|
-
|
|
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
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
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) {
|