@henryqw/pi-subagent 22.0.1 → 22.2.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/README.md +15 -8
- package/dist/direct-herdr.d.ts +2 -0
- package/dist/direct-herdr.js +49 -0
- package/dist/git-runtime.js +23 -22
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/integration-git.js +4 -2
- package/dist/launch-runtime.d.ts +1 -1
- package/dist/launch-runtime.js +10 -10
- package/dist/runner.d.ts +7 -0
- package/dist/runner.js +87 -13
- package/dist/store.d.ts +3 -0
- package/dist/store.js +8 -3
- package/dist/worktree.d.ts +1 -1
- package/dist/worktree.js +6 -6
- package/docs/worker-lifecycle.html +92 -0
- package/docs/worker-lifecycle.svg +80 -0
- package/extensions/isolated.ts +95 -9
- package/extensions/result-transport.ts +1 -5
- package/extensions/role-tools.ts +8 -2
- package/extensions/subagent-command.ts +15 -6
- package/extensions/subagent.ts +94 -43
- package/extensions/workflow.ts +18 -6
- package/package.json +1 -1
- package/skills/pi-subagent/SKILL.md +3 -3
- package/docs/delegation-routing.html +0 -89
- package/docs/delegation-routing.svg +0 -78
- package/extensions/task-name.ts +0 -28
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
+
<title>Checked subagent lifecycle and coordinator recovery</title>
|
|
6
|
+
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; } body { margin: 0; padding: 32px; background: #f0eee9; color: #101828; font-family: 'Geist', sans-serif; }
|
|
9
|
+
main { max-width: 1280px; margin: auto; } h1 { font: 400 28px 'Instrument Serif', serif; margin: 0 0 16px; }
|
|
10
|
+
svg { display: block; width: 100%; height: auto; }
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body><main><h1>Checked subagent lifecycle and coordinator recovery</h1>
|
|
14
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="worker-lifecycle-title worker-lifecycle-desc">
|
|
15
|
+
<title id="worker-lifecycle-title">Checked subagent lifecycle and coordinator recovery</title>
|
|
16
|
+
<desc id="worker-lifecycle-desc">Main delegates to the coordinator extension in its own Pi process. The coordinator saves state and launches a Herdr subagent in an isolated worktree. After the subagent commits, the coordinator verifies and saves a checked candidate; Main explicitly stages and promotes only after combined validation. A worker failure waits for the wave to settle before an attention follow-up. If the coordinator process stops, no follow-up is guaranteed; the last durable state remains for status inspection and deliberate recovery without automatic replay.</desc>
|
|
17
|
+
<defs>
|
|
18
|
+
<style>@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap');</style>
|
|
19
|
+
<marker id="worker-arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#4c5665"/></marker>
|
|
20
|
+
<marker id="worker-arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#1d4ed8"/></marker>
|
|
21
|
+
</defs>
|
|
22
|
+
<rect width="1280" height="720" fill="#f0eee9"/>
|
|
23
|
+
<text x="48" y="60" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1.5">PI SUBAGENT / ONE ISOLATED WORKER</text>
|
|
24
|
+
<text x="48" y="99" fill="#101828" font-family="'Instrument Serif',serif" font-size="28">A worker commits. Main decides what is integrated.</text>
|
|
25
|
+
<text x="48" y="126" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">The coordinator is extension code inside Main's Pi process, not another agent.</text>
|
|
26
|
+
<rect x="64" y="158" width="1138" height="58" rx="8" fill="#e6e4de"/>
|
|
27
|
+
<text x="84" y="181" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">One worker shown; siblings can keep working after this candidate is ready.</text>
|
|
28
|
+
<text x="84" y="200" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">State is persisted by the coordinator. Herdr agent status alone does not prove a clean, checked candidate.</text>
|
|
29
|
+
<!-- Connectors before nodes. Primary rail is the checked path; dashed paths retain uncertainty. -->
|
|
30
|
+
<line x1="234" y1="350" x2="306" y2="350" stroke="#4c5665" marker-end="url(#worker-arrow)"/>
|
|
31
|
+
<line x1="476" y1="350" x2="548" y2="350" stroke="#4c5665" marker-end="url(#worker-arrow)"/>
|
|
32
|
+
<line x1="718" y1="350" x2="790" y2="350" stroke="#4c5665" marker-end="url(#worker-arrow)"/>
|
|
33
|
+
<line x1="960" y1="350" x2="1032" y2="350" stroke="#1d4ed8" stroke-width="1.4" marker-end="url(#worker-arrow-accent)"/>
|
|
34
|
+
<path d="M633 405 V434 Q633 442 625 442 H408 Q400 442 400 450 V512" fill="none" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#worker-arrow)"/>
|
|
35
|
+
<line x1="500" y1="546" x2="526" y2="546" stroke="#4c5665" marker-end="url(#worker-arrow)"/>
|
|
36
|
+
<line x1="875" y1="405" x2="875" y2="512" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#worker-arrow)"/>
|
|
37
|
+
<line x1="968" y1="546" x2="994" y2="546" stroke="#4c5665" marker-end="url(#worker-arrow)"/>
|
|
38
|
+
<!-- Transition labels sit above, not on, their strokes. -->
|
|
39
|
+
<g fill="#4c5665" font-family="'Geist Mono',monospace" font-size="8" text-anchor="middle">
|
|
40
|
+
<rect x="242" y="324" width="56" height="12" rx="2" fill="#f0eee9"/><text x="270" y="333">START</text>
|
|
41
|
+
<rect x="484" y="324" width="56" height="12" rx="2" fill="#f0eee9"/><text x="512" y="333">LAUNCH</text>
|
|
42
|
+
<rect x="726" y="324" width="56" height="12" rx="2" fill="#f0eee9"/><text x="754" y="333">CHECK</text>
|
|
43
|
+
<rect x="968" y="324" width="56" height="12" rx="2" fill="#f0eee9"/><text x="996" y="333" fill="#1d4ed8">READY</text>
|
|
44
|
+
</g>
|
|
45
|
+
<!-- Five phases, with ownership above each. -->
|
|
46
|
+
<g fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1" text-anchor="middle">
|
|
47
|
+
<text x="149" y="272">MAIN AGENT</text><text x="391" y="272">COORDINATOR</text>
|
|
48
|
+
<text x="633" y="272">HERDR SUBAGENT</text><text x="875" y="272">COORDINATOR</text>
|
|
49
|
+
<text x="1117" y="272">MAIN AGENT</text>
|
|
50
|
+
</g>
|
|
51
|
+
<g fill="#ffffff" stroke="#101828">
|
|
52
|
+
<rect x="64" y="295" width="170" height="110" rx="8"/>
|
|
53
|
+
<rect x="306" y="295" width="170" height="110" rx="8"/>
|
|
54
|
+
<rect x="548" y="295" width="170" height="110" rx="8"/>
|
|
55
|
+
<rect x="790" y="295" width="170" height="110" rx="8" fill="#1d4ed8" fill-opacity="0.08" stroke="#1d4ed8"/>
|
|
56
|
+
<rect x="1032" y="295" width="170" height="110" rx="8"/>
|
|
57
|
+
</g>
|
|
58
|
+
<g fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600" text-anchor="middle">
|
|
59
|
+
<text x="149" y="339">Delegate task</text><text x="391" y="339">Save + launch</text>
|
|
60
|
+
<text x="633" y="339">Implement + commit</text><text x="875" y="339">Check + seal</text>
|
|
61
|
+
<text x="1117" y="339">Stage + promote</text>
|
|
62
|
+
</g>
|
|
63
|
+
<g fill="#4c5665" font-family="'Geist',sans-serif" font-size="11" text-anchor="middle">
|
|
64
|
+
<text x="149" y="370">Bounded request</text><text x="391" y="370">Durable ID + lease</text>
|
|
65
|
+
<text x="633" y="370">Isolated worktree</text><text x="875" y="370">Clean tip + task checks</text>
|
|
66
|
+
<text x="1117" y="370">Combined validation first</text>
|
|
67
|
+
</g>
|
|
68
|
+
<!-- Recovery bands: an incomplete worker and a stopped coordinator are distinct. -->
|
|
69
|
+
<rect x="286" y="468" width="448" height="146" rx="8" fill="#e6e4de"/>
|
|
70
|
+
<rect x="756" y="468" width="446" height="146" rx="8" fill="#e6e4de"/>
|
|
71
|
+
<g fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">
|
|
72
|
+
<text x="310" y="492">WORKER FAILURE</text><text x="778" y="492">COORDINATOR STOPS / ANY PHASE</text>
|
|
73
|
+
</g>
|
|
74
|
+
<g fill="#ffffff" stroke="#4c5665" stroke-dasharray="5,4">
|
|
75
|
+
<rect x="310" y="512" width="190" height="68" rx="8"/>
|
|
76
|
+
<rect x="526" y="512" width="192" height="68" rx="8"/>
|
|
77
|
+
<rect x="778" y="512" width="190" height="68" rx="8"/>
|
|
78
|
+
<rect x="994" y="512" width="190" height="68" rx="8"/>
|
|
79
|
+
</g>
|
|
80
|
+
<g fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600" text-anchor="middle">
|
|
81
|
+
<text x="405" y="540">No checked candidate</text><text x="622" y="540">Wave needs attention</text>
|
|
82
|
+
<text x="873" y="540">No guaranteed message</text><text x="1089" y="540">Inspect saved state</text>
|
|
83
|
+
</g>
|
|
84
|
+
<g fill="#4c5665" font-family="'Geist',sans-serif" font-size="10" text-anchor="middle">
|
|
85
|
+
<text x="405" y="560">Failure retained</text><text x="622" y="560">Follow-up when settled</text>
|
|
86
|
+
<text x="873" y="560">Last write survives</text><text x="1089" y="560">Restart; recover deliberately</text>
|
|
87
|
+
</g>
|
|
88
|
+
<line x1="48" y1="648" x2="1232" y2="648" stroke="#101828" stroke-opacity="0.12"/>
|
|
89
|
+
<text x="48" y="675" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">GUARD</text>
|
|
90
|
+
<text x="128" y="675" fill="#101828" font-family="'Geist',sans-serif" font-size="12">Status reports exact saved evidence; uncertain steps are never replayed automatically.</text>
|
|
91
|
+
<text x="128" y="696" fill="#4c5665" font-family="'Geist',sans-serif" font-size="11">A ready candidate may be staged while siblings run. Promotion still requires full combined validation.</text>
|
|
92
|
+
</svg></main></body></html>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="worker-lifecycle-title worker-lifecycle-desc">
|
|
3
|
+
<title id="worker-lifecycle-title">Checked subagent lifecycle and coordinator recovery</title>
|
|
4
|
+
<desc id="worker-lifecycle-desc">Main delegates to the coordinator extension in its own Pi process. The coordinator saves state and launches a Herdr subagent in an isolated worktree. After the subagent commits, the coordinator verifies and saves a checked candidate; Main explicitly stages and promotes only after combined validation. A worker failure waits for the wave to settle before an attention follow-up. If the coordinator process stops, no follow-up is guaranteed; the last durable state remains for status inspection and deliberate recovery without automatic replay.</desc>
|
|
5
|
+
<defs>
|
|
6
|
+
<style>@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap');</style>
|
|
7
|
+
<marker id="worker-arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#4c5665"/></marker>
|
|
8
|
+
<marker id="worker-arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#1d4ed8"/></marker>
|
|
9
|
+
</defs>
|
|
10
|
+
<rect width="1280" height="720" fill="#f0eee9"/>
|
|
11
|
+
<text x="48" y="60" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1.5">PI SUBAGENT / ONE ISOLATED WORKER</text>
|
|
12
|
+
<text x="48" y="99" fill="#101828" font-family="'Instrument Serif',serif" font-size="28">A worker commits. Main decides what is integrated.</text>
|
|
13
|
+
<text x="48" y="126" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">The coordinator is extension code inside Main's Pi process, not another agent.</text>
|
|
14
|
+
<rect x="64" y="158" width="1138" height="58" rx="8" fill="#e6e4de"/>
|
|
15
|
+
<text x="84" y="181" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">One worker shown; siblings can keep working after this candidate is ready.</text>
|
|
16
|
+
<text x="84" y="200" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">State is persisted by the coordinator. Herdr agent status alone does not prove a clean, checked candidate.</text>
|
|
17
|
+
<!-- Connectors before nodes. Primary rail is the checked path; dashed paths retain uncertainty. -->
|
|
18
|
+
<line x1="234" y1="350" x2="306" y2="350" stroke="#4c5665" marker-end="url(#worker-arrow)"/>
|
|
19
|
+
<line x1="476" y1="350" x2="548" y2="350" stroke="#4c5665" marker-end="url(#worker-arrow)"/>
|
|
20
|
+
<line x1="718" y1="350" x2="790" y2="350" stroke="#4c5665" marker-end="url(#worker-arrow)"/>
|
|
21
|
+
<line x1="960" y1="350" x2="1032" y2="350" stroke="#1d4ed8" stroke-width="1.4" marker-end="url(#worker-arrow-accent)"/>
|
|
22
|
+
<path d="M633 405 V434 Q633 442 625 442 H408 Q400 442 400 450 V512" fill="none" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#worker-arrow)"/>
|
|
23
|
+
<line x1="500" y1="546" x2="526" y2="546" stroke="#4c5665" marker-end="url(#worker-arrow)"/>
|
|
24
|
+
<line x1="875" y1="405" x2="875" y2="512" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#worker-arrow)"/>
|
|
25
|
+
<line x1="968" y1="546" x2="994" y2="546" stroke="#4c5665" marker-end="url(#worker-arrow)"/>
|
|
26
|
+
<!-- Transition labels sit above, not on, their strokes. -->
|
|
27
|
+
<g fill="#4c5665" font-family="'Geist Mono',monospace" font-size="8" text-anchor="middle">
|
|
28
|
+
<rect x="242" y="324" width="56" height="12" rx="2" fill="#f0eee9"/><text x="270" y="333">START</text>
|
|
29
|
+
<rect x="484" y="324" width="56" height="12" rx="2" fill="#f0eee9"/><text x="512" y="333">LAUNCH</text>
|
|
30
|
+
<rect x="726" y="324" width="56" height="12" rx="2" fill="#f0eee9"/><text x="754" y="333">CHECK</text>
|
|
31
|
+
<rect x="968" y="324" width="56" height="12" rx="2" fill="#f0eee9"/><text x="996" y="333" fill="#1d4ed8">READY</text>
|
|
32
|
+
</g>
|
|
33
|
+
<!-- Five phases, with ownership above each. -->
|
|
34
|
+
<g fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1" text-anchor="middle">
|
|
35
|
+
<text x="149" y="272">MAIN AGENT</text><text x="391" y="272">COORDINATOR</text>
|
|
36
|
+
<text x="633" y="272">HERDR SUBAGENT</text><text x="875" y="272">COORDINATOR</text>
|
|
37
|
+
<text x="1117" y="272">MAIN AGENT</text>
|
|
38
|
+
</g>
|
|
39
|
+
<g fill="#ffffff" stroke="#101828">
|
|
40
|
+
<rect x="64" y="295" width="170" height="110" rx="8"/>
|
|
41
|
+
<rect x="306" y="295" width="170" height="110" rx="8"/>
|
|
42
|
+
<rect x="548" y="295" width="170" height="110" rx="8"/>
|
|
43
|
+
<rect x="790" y="295" width="170" height="110" rx="8" fill="#1d4ed8" fill-opacity="0.08" stroke="#1d4ed8"/>
|
|
44
|
+
<rect x="1032" y="295" width="170" height="110" rx="8"/>
|
|
45
|
+
</g>
|
|
46
|
+
<g fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600" text-anchor="middle">
|
|
47
|
+
<text x="149" y="339">Delegate task</text><text x="391" y="339">Save + launch</text>
|
|
48
|
+
<text x="633" y="339">Implement + commit</text><text x="875" y="339">Check + seal</text>
|
|
49
|
+
<text x="1117" y="339">Stage + promote</text>
|
|
50
|
+
</g>
|
|
51
|
+
<g fill="#4c5665" font-family="'Geist',sans-serif" font-size="11" text-anchor="middle">
|
|
52
|
+
<text x="149" y="370">Bounded request</text><text x="391" y="370">Durable ID + lease</text>
|
|
53
|
+
<text x="633" y="370">Isolated worktree</text><text x="875" y="370">Clean tip + task checks</text>
|
|
54
|
+
<text x="1117" y="370">Combined validation first</text>
|
|
55
|
+
</g>
|
|
56
|
+
<!-- Recovery bands: an incomplete worker and a stopped coordinator are distinct. -->
|
|
57
|
+
<rect x="286" y="468" width="448" height="146" rx="8" fill="#e6e4de"/>
|
|
58
|
+
<rect x="756" y="468" width="446" height="146" rx="8" fill="#e6e4de"/>
|
|
59
|
+
<g fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">
|
|
60
|
+
<text x="310" y="492">WORKER FAILURE</text><text x="778" y="492">COORDINATOR STOPS / ANY PHASE</text>
|
|
61
|
+
</g>
|
|
62
|
+
<g fill="#ffffff" stroke="#4c5665" stroke-dasharray="5,4">
|
|
63
|
+
<rect x="310" y="512" width="190" height="68" rx="8"/>
|
|
64
|
+
<rect x="526" y="512" width="192" height="68" rx="8"/>
|
|
65
|
+
<rect x="778" y="512" width="190" height="68" rx="8"/>
|
|
66
|
+
<rect x="994" y="512" width="190" height="68" rx="8"/>
|
|
67
|
+
</g>
|
|
68
|
+
<g fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600" text-anchor="middle">
|
|
69
|
+
<text x="405" y="540">No checked candidate</text><text x="622" y="540">Wave needs attention</text>
|
|
70
|
+
<text x="873" y="540">No guaranteed message</text><text x="1089" y="540">Inspect saved state</text>
|
|
71
|
+
</g>
|
|
72
|
+
<g fill="#4c5665" font-family="'Geist',sans-serif" font-size="10" text-anchor="middle">
|
|
73
|
+
<text x="405" y="560">Failure retained</text><text x="622" y="560">Follow-up when settled</text>
|
|
74
|
+
<text x="873" y="560">Last write survives</text><text x="1089" y="560">Restart; recover deliberately</text>
|
|
75
|
+
</g>
|
|
76
|
+
<line x1="48" y1="648" x2="1232" y2="648" stroke="#101828" stroke-opacity="0.12"/>
|
|
77
|
+
<text x="48" y="675" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">GUARD</text>
|
|
78
|
+
<text x="128" y="675" fill="#101828" font-family="'Geist',sans-serif" font-size="12">Status reports exact saved evidence; uncertain steps are never replayed automatically.</text>
|
|
79
|
+
<text x="128" y="696" fill="#4c5665" font-family="'Geist',sans-serif" font-size="11">A ready candidate may be staged while siblings run. Promotion still requires full combined validation.</text>
|
|
80
|
+
</svg>
|
package/extensions/isolated.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
3
|
import type { EphemeralSubagentExecutor } from "../dist/ephemeral.js";
|
|
3
4
|
import { ROLE_TOOL_POLICY_FLAG } from "../dist/index.js";
|
|
4
5
|
import type { EffectiveExecutionPolicy } from "./config.ts";
|
|
@@ -39,6 +40,7 @@ const LOOKUP_ROOT_TIMEOUT_MS = 5_000;
|
|
|
39
40
|
const PUBLIC_EVIDENCE_MAX_BYTES = 512;
|
|
40
41
|
const WORKSPACE_WIDGET_KEY = "pi-subagent-workspaces";
|
|
41
42
|
const WIDGET_FIELD_MAX_CHARS = 32;
|
|
43
|
+
const MAX_WORKSPACE_WIDGET_LINES = 6;
|
|
42
44
|
|
|
43
45
|
export interface IsolatedExtensionComponents {
|
|
44
46
|
runner: IsolatedRunner;
|
|
@@ -112,18 +114,26 @@ function workspaceStatus(status: RunState["tasks"][number]["status"]): string {
|
|
|
112
114
|
|
|
113
115
|
export function workspaceWidgetLines(state: RunState): string[] | undefined {
|
|
114
116
|
const rows = state.tasks.flatMap((taskState) => {
|
|
115
|
-
|
|
117
|
+
const task = state.request.tasks.find((candidate) => candidate.id === taskState.taskId);
|
|
118
|
+
if (!task) return [];
|
|
119
|
+
const status = state.status === "aborted" ? "aborted" : workspaceStatus(taskState.status);
|
|
120
|
+
if (taskState.kind === "text") {
|
|
121
|
+
return taskState.status === "running" || taskState.status === "needs_attention"
|
|
122
|
+
? [`${status === "attention" ? "!" : status === "aborted" ? "■" : "◌"} I ${workspaceBadge(task.role, task.modelClass)} ${compactWidgetField(task.id)} · ${status === "attention" ? `attention · ${compactWidgetField(taskState.failure ?? status)}` : status}`]
|
|
123
|
+
: [];
|
|
124
|
+
}
|
|
116
125
|
const attempt = taskState.attempts.at(-1);
|
|
117
126
|
const allocation = [...(attempt?.allocations ?? [])].reverse().find(
|
|
118
127
|
(candidate): candidate is WorkspaceAllocationIntent =>
|
|
119
128
|
candidate.kind === "workspace" && candidate.status !== "absent",
|
|
120
129
|
);
|
|
121
130
|
const workspaceCleanup = attempt?.cleanup.find((step) => step.kind === "workspace");
|
|
122
|
-
if (
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
131
|
+
if (workspaceCleanup?.status === "completed" || (!allocation && !["allocating", "working", "attention"].includes(status))) return [];
|
|
132
|
+
const symbol = status === "attention" ? "!" : status === "ready" || status === "completed" ? "✓"
|
|
133
|
+
: ["working", "allocating", "integrating", "cleanup"].includes(status) ? "◌" : "■";
|
|
134
|
+
const detail = status === "ready" ? "candidate ready · Main not promoted"
|
|
135
|
+
: status === "attention" ? `attention · ${compactWidgetField(taskState.failure ?? "needs attention")}` : status;
|
|
136
|
+
return `${symbol} I ${workspaceBadge(task.role, task.modelClass)} ${compactWidgetField(task.id)} · ${detail}${allocation ? ` · ${compactWidgetField(allocation.label)}` : ""}`;
|
|
127
137
|
});
|
|
128
138
|
return rows.length ? rows : undefined;
|
|
129
139
|
}
|
|
@@ -134,8 +144,21 @@ function updateWorkspaceWidget(ctx: ExtensionContext, state: RunState, rowsByReq
|
|
|
134
144
|
if (rows) rowsByRequest.set(key, rows);
|
|
135
145
|
else rowsByRequest.delete(key);
|
|
136
146
|
if (!ctx.hasUI) return;
|
|
137
|
-
const allRows = [...rowsByRequest.values()].flat()
|
|
138
|
-
|
|
147
|
+
const allRows = [...rowsByRequest.values()].flat().sort((a, b) =>
|
|
148
|
+
(a.startsWith("!") ? 0 : a.startsWith("◌") ? 1 : 2) - (b.startsWith("!") ? 0 : b.startsWith("◌") ? 1 : 2));
|
|
149
|
+
const shown = allRows.length > MAX_WORKSPACE_WIDGET_LINES
|
|
150
|
+
? [...allRows.slice(0, MAX_WORKSPACE_WIDGET_LINES - 1), `+${allRows.length - MAX_WORKSPACE_WIDGET_LINES + 1} more · /subagent`]
|
|
151
|
+
: allRows;
|
|
152
|
+
ctx.ui.setWidget(WORKSPACE_WIDGET_KEY, shown.length ? (_tui, theme) => ({
|
|
153
|
+
invalidate() {},
|
|
154
|
+
render(width) {
|
|
155
|
+
return shown.map((line) => {
|
|
156
|
+
const color = line.startsWith("!") ? "error" : line.startsWith("◌") ? "accent"
|
|
157
|
+
: line.startsWith("✓") ? "success" : line.startsWith("■") ? "warning" : undefined;
|
|
158
|
+
return truncateToWidth(color ? theme.fg(color, line[0]!) + line.slice(1) : line, width);
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
}) : undefined);
|
|
139
162
|
}
|
|
140
163
|
|
|
141
164
|
function updateWorkspaceWidgetSafely(ctx: ExtensionContext, state: RunState, rowsByRequest: Map<string, string[]>): void {
|
|
@@ -377,6 +400,7 @@ function toolResult(response: RunResponse, ctx: ExtensionContext, rowsByRequest:
|
|
|
377
400
|
export interface IsolatedSurface {
|
|
378
401
|
execute(params: unknown, signal: AbortSignal | undefined, ctx: ExtensionContext): Promise<ReturnType<typeof toolResult>>;
|
|
379
402
|
inventory(cwd: string): Promise<IsolatedInventory>;
|
|
403
|
+
recover(cwd: string): Promise<string>;
|
|
380
404
|
inspect(root: string, requestId: string): Promise<readonly string[]>;
|
|
381
405
|
canFollowup(root: string, requestId: string, taskId: string): boolean;
|
|
382
406
|
enqueue(root: string, requestId: string, taskId: string, text: string, current: () => boolean): string;
|
|
@@ -394,7 +418,7 @@ export interface RegisterIsolatedOptions {
|
|
|
394
418
|
export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIsolatedOptions): IsolatedSurface {
|
|
395
419
|
if (process.argv.includes(`--${ROLE_TOOL_POLICY_FLAG}`)) {
|
|
396
420
|
const denied = (): never => { throw new Error("Child Roles cannot access isolated delegation."); };
|
|
397
|
-
return { execute: async () => denied(), inventory: async () => denied(), inspect: async () => denied(), canFollowup: denied, enqueue: denied, drain: denied };
|
|
421
|
+
return { execute: async () => denied(), inventory: async () => denied(), recover: async () => denied(), inspect: async () => denied(), canFollowup: denied, enqueue: denied, drain: denied };
|
|
398
422
|
}
|
|
399
423
|
const componentsFactory = options.componentsFactory ?? createIsolatedComponents;
|
|
400
424
|
let latestCtx: ExtensionContext | undefined;
|
|
@@ -475,8 +499,11 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
475
499
|
const controller = new AbortController();
|
|
476
500
|
activeJobs.add(controller);
|
|
477
501
|
jobOwners.set(key, canDeliver);
|
|
502
|
+
const notified = new Set<string>();
|
|
503
|
+
let candidateListener: ((state: RunState) => void) | undefined;
|
|
478
504
|
const finish = () => {
|
|
479
505
|
activeJobs.delete(controller);
|
|
506
|
+
if (candidateListener) stateListeners.delete(candidateListener);
|
|
480
507
|
if (jobOwners.get(key) === canDeliver) jobOwners.delete(key);
|
|
481
508
|
};
|
|
482
509
|
const abortBeforeAck = () => controller.abort(signal?.reason);
|
|
@@ -491,6 +518,7 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
491
518
|
if (state.root !== root || state.request.id !== id) return;
|
|
492
519
|
latestState = state;
|
|
493
520
|
if (!acknowledged(state)) return;
|
|
521
|
+
for (const candidate of state.integration.candidates) notified.add(`${candidate.taskId}\0${candidate.attempt}\0${candidate.tip.head}`);
|
|
494
522
|
stateListeners.delete(listener);
|
|
495
523
|
removeTurnAbort();
|
|
496
524
|
accept(state);
|
|
@@ -516,6 +544,17 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
516
544
|
if (ctx.hasUI) ctx.ui.notify(`Pi Subagent ${id} result delivery failed; use subagent_status to recover.`, "error");
|
|
517
545
|
}
|
|
518
546
|
};
|
|
547
|
+
candidateListener = (state) => {
|
|
548
|
+
if (state.root !== root || state.request.id !== id || state.status !== "running" || !canDeliver()) return;
|
|
549
|
+
latestState = state;
|
|
550
|
+
for (const candidate of state.integration.candidates) {
|
|
551
|
+
const identity = `${candidate.taskId}\0${candidate.attempt}\0${candidate.tip.head}`;
|
|
552
|
+
if (candidate.decision || notified.has(identity)) continue;
|
|
553
|
+
notified.add(identity);
|
|
554
|
+
deliver(`Pi Subagent ${id}: ${candidate.taskId} ready to integrate. Inspect the exact candidate with subagent_status and stage it now; other workers may still be running.`);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
stateListeners.add(candidateListener);
|
|
519
558
|
void Promise.resolve().then(() => start(controller.signal)).then(
|
|
520
559
|
(response) => {
|
|
521
560
|
finish();
|
|
@@ -591,6 +630,12 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
591
630
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
592
631
|
latestCtx = ctx;
|
|
593
632
|
const root = await lookupRoot(ctx.cwd, signal);
|
|
633
|
+
if (params.action === "advance") {
|
|
634
|
+
return await startInSession(params.id, root, signal, ctx,
|
|
635
|
+
(state) => state.status === "running" && state.waves.at(-1)?.status === "dispatching"
|
|
636
|
+
&& sameIdentity(state.waves.at(-1)!.base, params.expectedTip),
|
|
637
|
+
(runSignal) => getComponents().runner.integrate(params, root, runSignal));
|
|
638
|
+
}
|
|
594
639
|
return toolResult(await getComponents().runner.integrate(params, root, signal), ctx, workspaceRowsByRequest);
|
|
595
640
|
},
|
|
596
641
|
});
|
|
@@ -611,6 +656,47 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
611
656
|
const root = await lookupRoot(cwd);
|
|
612
657
|
return { root, ...await getComponents().runner.listRequests(root) };
|
|
613
658
|
},
|
|
659
|
+
async recover(cwd) {
|
|
660
|
+
const root = await lookupRoot(cwd);
|
|
661
|
+
const { requests, invalidIds, leaseBusy } = await getComponents().runner.recoverRepository(root);
|
|
662
|
+
const lines = [
|
|
663
|
+
`Pi Subagent recovery in ${root}: ${leaseBusy ? "productive lease is live; no state changed" : "orphaned interruptions classified; no work replayed"}.`,
|
|
664
|
+
...(requests.length ? requests.map((response) => {
|
|
665
|
+
const state = response.state;
|
|
666
|
+
const blockers = state.tasks.flatMap((task) => [
|
|
667
|
+
...(task.failure ? [`${task.taskId}: ${boundedPublicText(task.failure)}`] : []),
|
|
668
|
+
...(task.kind === "changeset" && task.attempts.some((attempt) => attempt.prompts.some((prompt) => prompt.status === "ambiguous"))
|
|
669
|
+
? [`${task.taskId}: ambiguous worker prompt; do not replay`] : []),
|
|
670
|
+
]);
|
|
671
|
+
const generation = state.integration.generations.at(-1);
|
|
672
|
+
const readyCandidates = state.integration.candidates.filter((candidate) => candidate.worker === "retained" && !candidate.decision);
|
|
673
|
+
return [
|
|
674
|
+
`${state.request.id}: ${state.status}; Main ${response.main?.status ?? "not checked"}.`,
|
|
675
|
+
...blockers.map((blocker) => ` Blocker: ${blocker}`),
|
|
676
|
+
...(state.final.failure ? [` Final blocker: ${boundedPublicText(state.final.failure)}`] : []),
|
|
677
|
+
...(generation?.failure ? [` Integration blocker: ${boundedPublicText(generation.failure)}`] : []),
|
|
678
|
+
...(state.status !== "completed" && readyCandidates.length ? [` ${readyCandidates.length} retained candidate(s); Main selects exact candidates with subagent_stage after subagent_status.`] : []),
|
|
679
|
+
...(state.status === "completed" ? [" Retained resources after completion; inspect subagent_status for guarded cleanup."] : []),
|
|
680
|
+
...(generation ? [` Generation ${generation.number}: ${generation.status}; inspect subagent_status before any subagent_integrate action.`] : []),
|
|
681
|
+
...(!leaseBusy && response.continuation ? [` Reported next action: subagent_resume ${JSON.stringify(response.continuation)}.`] : []),
|
|
682
|
+
].join("\n");
|
|
683
|
+
}) : ["No unfinished or retained isolated requests."]),
|
|
684
|
+
...(invalidIds.length ? [`Unreadable state IDs (preserved; inspect with their compatible owner): ${invalidIds.map((id) => JSON.stringify(id)).join(", ")}.`] : []),
|
|
685
|
+
leaseBusy ? "Wait for the active productive run; use subagent_status to inspect its current state. No work was taken over."
|
|
686
|
+
: "Main: inspect subagent_status for exact identities and blockers before deciding stage, promote, resume, or guarded release. No worker prompt, merge, promotion, or cleanup was replayed.",
|
|
687
|
+
];
|
|
688
|
+
const report: string[] = [];
|
|
689
|
+
let bytes = 0;
|
|
690
|
+
let omitted = 0;
|
|
691
|
+
for (const line of lines.flatMap((entry) => entry.split("\n"))) {
|
|
692
|
+
const size = Buffer.byteLength(`${line}\n`, "utf8");
|
|
693
|
+
if (bytes + size > 7_900) { omitted++; continue; }
|
|
694
|
+
report.push(line);
|
|
695
|
+
bytes += size;
|
|
696
|
+
}
|
|
697
|
+
if (omitted) report.push(`[${omitted} recovery report lines omitted; use subagent_status for exact request evidence.]`);
|
|
698
|
+
return report.join("\n");
|
|
699
|
+
},
|
|
614
700
|
async inspect(root, requestId) {
|
|
615
701
|
const response = await getComponents().runner.status(requestId, root);
|
|
616
702
|
return inspectionNotices(response);
|
|
@@ -49,10 +49,6 @@ export type WorkflowTransport = {
|
|
|
49
49
|
|
|
50
50
|
type Evidence = { heading: string; preview: string; remainder: string };
|
|
51
51
|
|
|
52
|
-
function compareEntries(left: WorkflowTransportEntry, right: WorkflowTransportEntry): number {
|
|
53
|
-
return left.index - right.index || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
52
|
function splitEvidence(text: string): [string, string] {
|
|
57
53
|
const preview = Array.from(text).slice(0, EVIDENCE_PREVIEW_CODE_POINTS).join("");
|
|
58
54
|
return [preview, text.slice(preview.length)];
|
|
@@ -114,7 +110,7 @@ function evidenceFor(entry: WorkflowTransportEntry, position: number, total: num
|
|
|
114
110
|
}
|
|
115
111
|
|
|
116
112
|
export function formatWorkflowResult(mode: WorkflowMode, entries: readonly WorkflowTransportEntry[]): WorkflowTransport {
|
|
117
|
-
const ordered = [...entries].sort(
|
|
113
|
+
const ordered = [...entries].sort((left, right) => left.index - right.index);
|
|
118
114
|
if (ordered.some(({ status }) => status === "pending" || status === "running")) {
|
|
119
115
|
throw new TypeError("Final workflow transport requires terminal entry states.");
|
|
120
116
|
}
|
package/extensions/role-tools.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { isAbsolute } from "node:path";
|
|
|
3
3
|
import { isToolCallEventType, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import {
|
|
5
5
|
CHILD_EXCLUDED_TOOL_NAMES,
|
|
6
|
+
EXECUTION_BUDGET_FLAG,
|
|
6
7
|
EXECUTION_BUDGET_ENV,
|
|
7
8
|
PI_SUBAGENT_PROCESS_LEASE,
|
|
8
9
|
ROLE_TOOL_POLICY_FLAG,
|
|
@@ -47,8 +48,9 @@ function configuredTools(value: unknown): string[] {
|
|
|
47
48
|
return [...new Set(parsed.map((name) => name.trim()))];
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function executionBudget(value: string | undefined): EphemeralSubagentExecutionBudget | undefined {
|
|
51
|
+
function executionBudget(value: string | boolean | undefined): EphemeralSubagentExecutionBudget | undefined {
|
|
51
52
|
if (value === undefined) return;
|
|
53
|
+
if (typeof value !== "string") throw new Error(`${EXECUTION_BUDGET_ENV} must be a JSON execution budget.`);
|
|
52
54
|
let parsed: unknown;
|
|
53
55
|
try {
|
|
54
56
|
parsed = JSON.parse(value);
|
|
@@ -104,7 +106,11 @@ export default function roleTools(pi: ExtensionAPI): void {
|
|
|
104
106
|
description: "Internal Pi Subagent Role tool policy",
|
|
105
107
|
type: "string",
|
|
106
108
|
});
|
|
107
|
-
|
|
109
|
+
pi.registerFlag(EXECUTION_BUDGET_FLAG, {
|
|
110
|
+
description: "Internal Pi Subagent execution budget",
|
|
111
|
+
type: "string",
|
|
112
|
+
});
|
|
113
|
+
const budget = executionBudget(pi.getFlag(EXECUTION_BUDGET_FLAG) ?? process.env[EXECUTION_BUDGET_ENV]);
|
|
108
114
|
let handoffSent = false;
|
|
109
115
|
pi.on("session_start", () => {
|
|
110
116
|
const selected = configuredTools(pi.getFlag(ROLE_TOOL_POLICY_FLAG));
|
|
@@ -31,7 +31,8 @@ export interface IsolatedInventory {
|
|
|
31
31
|
export interface SubagentCommandAdapter {
|
|
32
32
|
direct(ctx: ExtensionContext): readonly DirectTask[];
|
|
33
33
|
isolated(cwd: string): Promise<IsolatedInventory>;
|
|
34
|
-
|
|
34
|
+
recover(cwd: string): Promise<string>;
|
|
35
|
+
inspectInTab(root: string, requestId: string, ctx: ExtensionContext, current: () => boolean): Promise<{ tabId: string; name: string; sessionFile: string }>;
|
|
35
36
|
canFollowup(root: string, requestId: string, taskId: string): boolean;
|
|
36
37
|
enqueue(root: string, requestId: string, taskId: string, text: string, current: () => boolean): string;
|
|
37
38
|
drain(root: string, requestId: string, taskId: string, current: () => boolean): readonly string[];
|
|
@@ -55,10 +56,11 @@ async function choose<T>(ctx: ExtensionContext, title: string, items: readonly P
|
|
|
55
56
|
|
|
56
57
|
export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentCommandAdapter): void {
|
|
57
58
|
pi.registerCommand("subagent", {
|
|
58
|
-
description: "Inspect direct and isolated work
|
|
59
|
+
description: "Inspect direct and isolated work, or recover orphaned isolated requests with /subagent recover",
|
|
59
60
|
handler: async (args, ctx) => {
|
|
60
|
-
|
|
61
|
-
if (
|
|
61
|
+
const command = args.trim();
|
|
62
|
+
if (command && command !== "recover") throw new Error("Usage: /subagent [recover]");
|
|
63
|
+
if (!command && !ctx.hasUI) throw new Error("/subagent requires interactive UI (TUI or RPC); use the subagent agent tools in non-interactive mode.");
|
|
62
64
|
const manager = ctx.sessionManager;
|
|
63
65
|
const session = manager.getSessionId();
|
|
64
66
|
const file = manager.getSessionFile();
|
|
@@ -73,6 +75,13 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
|
|
|
73
75
|
return now.length >= branch.length && branch.every((id, index) => now[index]?.id === id)
|
|
74
76
|
&& now.slice(branch.length).every((entry) => !existing.has(entry.id));
|
|
75
77
|
};
|
|
78
|
+
if (command === "recover") {
|
|
79
|
+
const report = await adapter.recover(ctx.cwd);
|
|
80
|
+
if (!current()) return;
|
|
81
|
+
pi.sendMessage({ customType: "pi-subagent-recovery", content: report, display: true },
|
|
82
|
+
{ triggerTurn: true, deliverAs: "followUp" });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
76
85
|
const valid = () => {
|
|
77
86
|
if (current()) return true;
|
|
78
87
|
ctx.ui.notify("Session or branch changed; reopen /subagent in the current context.", "warning");
|
|
@@ -130,8 +139,8 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
|
|
|
130
139
|
if (action === "back") break;
|
|
131
140
|
if (action === "inspect") {
|
|
132
141
|
try {
|
|
133
|
-
const
|
|
134
|
-
if (valid())
|
|
142
|
+
const tab = await adapter.inspectInTab(root, request.id, ctx, current);
|
|
143
|
+
if (valid()) ctx.ui.notify(`Status inspection started in Herdr tab ${tab.tabId} · agent ${tab.name} · session ${tab.sessionFile}.`, "info");
|
|
135
144
|
}
|
|
136
145
|
catch (error) { if (valid()) ctx.ui.notify(`Inspection failed: ${errorText(error)}`, "error"); }
|
|
137
146
|
continue;
|