@henryqw/pi-subagent 22.1.0 → 22.2.1
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 +13 -8
- package/dist/git-runtime.js +21 -18
- package/dist/herdr-runtime.d.ts +3 -0
- package/dist/herdr-runtime.js +18 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/integration-git.js +7 -2
- package/dist/launch-runtime.d.ts +1 -1
- package/dist/launch-runtime.js +9 -9
- package/dist/runner.d.ts +7 -0
- package/dist/runner.js +94 -13
- package/dist/store.d.ts +3 -0
- package/dist/store.js +6 -1
- package/dist/worktree.d.ts +2 -2
- package/dist/worktree.js +8 -8
- package/docs/worker-lifecycle.html +92 -0
- package/docs/worker-lifecycle.svg +80 -0
- package/extensions/isolated.ts +71 -7
- package/extensions/role-tools.ts +8 -2
- package/extensions/subagent-command.ts +15 -6
- package/extensions/subagent.ts +45 -1
- 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
|
@@ -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
|
@@ -65,18 +65,19 @@ export const createIsolatedComponents: CreateIsolatedComponents = ({
|
|
|
65
65
|
: await directRunProcess(command, args, options);
|
|
66
66
|
const resolveRoot = createCanonicalGitRootResolver({ runProcess });
|
|
67
67
|
const git = new CheckedGitRuntime({ runProcess, executeReview: createExactJudgmentExecutor(executor) });
|
|
68
|
-
const
|
|
68
|
+
const executionBudget = () => ({
|
|
69
|
+
maxTurns: policy.maxTurns,
|
|
70
|
+
maxMs: null,
|
|
71
|
+
...(policy.maxTokens === undefined ? {} : { maxTokens: policy.maxTokens }),
|
|
72
|
+
});
|
|
73
|
+
const host = new HerdrHostRuntime({ inspectInFlightTaskCandidate: git.inspectInFlightTaskCandidate.bind(git), runProcess, executionBudget });
|
|
69
74
|
const coordinator = new RoleLaunchRuntime({
|
|
70
75
|
pi,
|
|
71
76
|
context,
|
|
72
77
|
resolveRoot,
|
|
73
78
|
preflightHost: async ({ root }, operation) => await host.preflightHost({ root }, operation),
|
|
74
79
|
inspectMain: async (input, operation) => await git.inspectMain(input, operation),
|
|
75
|
-
executionBudget
|
|
76
|
-
maxTurns: policy.maxTurns,
|
|
77
|
-
maxMs: null,
|
|
78
|
-
...(policy.maxTokens === undefined ? {} : { maxTokens: policy.maxTokens }),
|
|
79
|
-
}),
|
|
80
|
+
executionBudget,
|
|
80
81
|
});
|
|
81
82
|
return {
|
|
82
83
|
resolveRoot,
|
|
@@ -400,6 +401,7 @@ function toolResult(response: RunResponse, ctx: ExtensionContext, rowsByRequest:
|
|
|
400
401
|
export interface IsolatedSurface {
|
|
401
402
|
execute(params: unknown, signal: AbortSignal | undefined, ctx: ExtensionContext): Promise<ReturnType<typeof toolResult>>;
|
|
402
403
|
inventory(cwd: string): Promise<IsolatedInventory>;
|
|
404
|
+
recover(cwd: string): Promise<string>;
|
|
403
405
|
inspect(root: string, requestId: string): Promise<readonly string[]>;
|
|
404
406
|
canFollowup(root: string, requestId: string, taskId: string): boolean;
|
|
405
407
|
enqueue(root: string, requestId: string, taskId: string, text: string, current: () => boolean): string;
|
|
@@ -417,7 +419,7 @@ export interface RegisterIsolatedOptions {
|
|
|
417
419
|
export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIsolatedOptions): IsolatedSurface {
|
|
418
420
|
if (process.argv.includes(`--${ROLE_TOOL_POLICY_FLAG}`)) {
|
|
419
421
|
const denied = (): never => { throw new Error("Child Roles cannot access isolated delegation."); };
|
|
420
|
-
return { execute: async () => denied(), inventory: async () => denied(), inspect: async () => denied(), canFollowup: denied, enqueue: denied, drain: denied };
|
|
422
|
+
return { execute: async () => denied(), inventory: async () => denied(), recover: async () => denied(), inspect: async () => denied(), canFollowup: denied, enqueue: denied, drain: denied };
|
|
421
423
|
}
|
|
422
424
|
const componentsFactory = options.componentsFactory ?? createIsolatedComponents;
|
|
423
425
|
let latestCtx: ExtensionContext | undefined;
|
|
@@ -498,8 +500,11 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
498
500
|
const controller = new AbortController();
|
|
499
501
|
activeJobs.add(controller);
|
|
500
502
|
jobOwners.set(key, canDeliver);
|
|
503
|
+
const notified = new Set<string>();
|
|
504
|
+
let candidateListener: ((state: RunState) => void) | undefined;
|
|
501
505
|
const finish = () => {
|
|
502
506
|
activeJobs.delete(controller);
|
|
507
|
+
if (candidateListener) stateListeners.delete(candidateListener);
|
|
503
508
|
if (jobOwners.get(key) === canDeliver) jobOwners.delete(key);
|
|
504
509
|
};
|
|
505
510
|
const abortBeforeAck = () => controller.abort(signal?.reason);
|
|
@@ -514,6 +519,7 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
514
519
|
if (state.root !== root || state.request.id !== id) return;
|
|
515
520
|
latestState = state;
|
|
516
521
|
if (!acknowledged(state)) return;
|
|
522
|
+
for (const candidate of state.integration.candidates) notified.add(`${candidate.taskId}\0${candidate.attempt}\0${candidate.tip.head}`);
|
|
517
523
|
stateListeners.delete(listener);
|
|
518
524
|
removeTurnAbort();
|
|
519
525
|
accept(state);
|
|
@@ -539,6 +545,17 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
539
545
|
if (ctx.hasUI) ctx.ui.notify(`Pi Subagent ${id} result delivery failed; use subagent_status to recover.`, "error");
|
|
540
546
|
}
|
|
541
547
|
};
|
|
548
|
+
candidateListener = (state) => {
|
|
549
|
+
if (state.root !== root || state.request.id !== id || state.status !== "running" || !canDeliver()) return;
|
|
550
|
+
latestState = state;
|
|
551
|
+
for (const candidate of state.integration.candidates) {
|
|
552
|
+
const identity = `${candidate.taskId}\0${candidate.attempt}\0${candidate.tip.head}`;
|
|
553
|
+
if (candidate.decision || notified.has(identity)) continue;
|
|
554
|
+
notified.add(identity);
|
|
555
|
+
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.`);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
stateListeners.add(candidateListener);
|
|
542
559
|
void Promise.resolve().then(() => start(controller.signal)).then(
|
|
543
560
|
(response) => {
|
|
544
561
|
finish();
|
|
@@ -614,6 +631,12 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
614
631
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
615
632
|
latestCtx = ctx;
|
|
616
633
|
const root = await lookupRoot(ctx.cwd, signal);
|
|
634
|
+
if (params.action === "advance") {
|
|
635
|
+
return await startInSession(params.id, root, signal, ctx,
|
|
636
|
+
(state) => state.status === "running" && state.waves.at(-1)?.status === "dispatching"
|
|
637
|
+
&& sameIdentity(state.waves.at(-1)!.base, params.expectedTip),
|
|
638
|
+
(runSignal) => getComponents().runner.integrate(params, root, runSignal));
|
|
639
|
+
}
|
|
617
640
|
return toolResult(await getComponents().runner.integrate(params, root, signal), ctx, workspaceRowsByRequest);
|
|
618
641
|
},
|
|
619
642
|
});
|
|
@@ -634,6 +657,47 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
634
657
|
const root = await lookupRoot(cwd);
|
|
635
658
|
return { root, ...await getComponents().runner.listRequests(root) };
|
|
636
659
|
},
|
|
660
|
+
async recover(cwd) {
|
|
661
|
+
const root = await lookupRoot(cwd);
|
|
662
|
+
const { requests, invalidIds, leaseBusy } = await getComponents().runner.recoverRepository(root);
|
|
663
|
+
const lines = [
|
|
664
|
+
`Pi Subagent recovery in ${root}: ${leaseBusy ? "productive lease is live; no state changed" : "orphaned interruptions classified; no work replayed"}.`,
|
|
665
|
+
...(requests.length ? requests.map((response) => {
|
|
666
|
+
const state = response.state;
|
|
667
|
+
const blockers = state.tasks.flatMap((task) => [
|
|
668
|
+
...(task.failure ? [`${task.taskId}: ${boundedPublicText(task.failure)}`] : []),
|
|
669
|
+
...(task.kind === "changeset" && task.attempts.some((attempt) => attempt.prompts.some((prompt) => prompt.status === "ambiguous"))
|
|
670
|
+
? [`${task.taskId}: ambiguous worker prompt; do not replay`] : []),
|
|
671
|
+
]);
|
|
672
|
+
const generation = state.integration.generations.at(-1);
|
|
673
|
+
const readyCandidates = state.integration.candidates.filter((candidate) => candidate.worker === "retained" && !candidate.decision);
|
|
674
|
+
return [
|
|
675
|
+
`${state.request.id}: ${state.status}; Main ${response.main?.status ?? "not checked"}.`,
|
|
676
|
+
...blockers.map((blocker) => ` Blocker: ${blocker}`),
|
|
677
|
+
...(state.final.failure ? [` Final blocker: ${boundedPublicText(state.final.failure)}`] : []),
|
|
678
|
+
...(generation?.failure ? [` Integration blocker: ${boundedPublicText(generation.failure)}`] : []),
|
|
679
|
+
...(state.status !== "completed" && readyCandidates.length ? [` ${readyCandidates.length} retained candidate(s); Main selects exact candidates with subagent_stage after subagent_status.`] : []),
|
|
680
|
+
...(state.status === "completed" ? [" Retained resources after completion; inspect subagent_status for guarded cleanup."] : []),
|
|
681
|
+
...(generation ? [` Generation ${generation.number}: ${generation.status}; inspect subagent_status before any subagent_integrate action.`] : []),
|
|
682
|
+
...(!leaseBusy && response.continuation ? [` Reported next action: subagent_resume ${JSON.stringify(response.continuation)}.`] : []),
|
|
683
|
+
].join("\n");
|
|
684
|
+
}) : ["No unfinished or retained isolated requests."]),
|
|
685
|
+
...(invalidIds.length ? [`Unreadable state IDs (preserved; inspect with their compatible owner): ${invalidIds.map((id) => JSON.stringify(id)).join(", ")}.`] : []),
|
|
686
|
+
leaseBusy ? "Wait for the active productive run; use subagent_status to inspect its current state. No work was taken over."
|
|
687
|
+
: "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.",
|
|
688
|
+
];
|
|
689
|
+
const report: string[] = [];
|
|
690
|
+
let bytes = 0;
|
|
691
|
+
let omitted = 0;
|
|
692
|
+
for (const line of lines.flatMap((entry) => entry.split("\n"))) {
|
|
693
|
+
const size = Buffer.byteLength(`${line}\n`, "utf8");
|
|
694
|
+
if (bytes + size > 7_900) { omitted++; continue; }
|
|
695
|
+
report.push(line);
|
|
696
|
+
bytes += size;
|
|
697
|
+
}
|
|
698
|
+
if (omitted) report.push(`[${omitted} recovery report lines omitted; use subagent_status for exact request evidence.]`);
|
|
699
|
+
return report.join("\n");
|
|
700
|
+
},
|
|
637
701
|
async inspect(root, requestId) {
|
|
638
702
|
const response = await getComponents().runner.status(requestId, root);
|
|
639
703
|
return inspectionNotices(response);
|
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;
|
package/extensions/subagent.ts
CHANGED
|
@@ -228,7 +228,51 @@ export default function subagentExtension(
|
|
|
228
228
|
return [...grouped.values()];
|
|
229
229
|
},
|
|
230
230
|
isolated: (cwd) => isolatedSurface.inventory(cwd),
|
|
231
|
-
|
|
231
|
+
recover: (cwd) => isolatedSurface.recover(cwd),
|
|
232
|
+
async inspectInTab(root, id, ctx, current) {
|
|
233
|
+
const notices = await isolatedSurface.inspect(root, id);
|
|
234
|
+
if (!current()) throw new Error("Session or branch changed before status inspection.");
|
|
235
|
+
const snapshot = notices.join("\n");
|
|
236
|
+
if (Buffer.byteLength(snapshot, "utf8") > 32_000) throw new Error("Status exceeds the 32 KiB inspection limit; use subagent_status for exact evidence.");
|
|
237
|
+
const role = loadRoles().find((candidate) => candidate.name === "scout");
|
|
238
|
+
if (!role || roleCanWrite(role)) throw new Error("Status inspection requires a configured read-only scout Role.");
|
|
239
|
+
const prepared = prepareRoleLaunch(pi, ctx, { role, task: DELEGATE_TASK, modelClass: "fast" });
|
|
240
|
+
const herdr = createDirectHerdr(pi, ctx.cwd, currentPolicy().childIdleMs);
|
|
241
|
+
const controller = new AbortController();
|
|
242
|
+
const taskId = `inspect-${id}-${randomUUID()}`;
|
|
243
|
+
const handles: DirectHandle[] = [];
|
|
244
|
+
const tabs: DirectTabRecord[] = [];
|
|
245
|
+
let resolveSettled!: () => void;
|
|
246
|
+
const settled = new Promise<void>((resolve) => { resolveSettled = resolve; });
|
|
247
|
+
directTasks.set(taskId, { controller, settled, handles, tabs });
|
|
248
|
+
try {
|
|
249
|
+
const transient = await materializeTransientLaunch({ launch: prepared, prompt: prepared.systemPrompt,
|
|
250
|
+
promptArgIndex: prepared.promptArgIndex }, controller.signal);
|
|
251
|
+
let handle: DirectHandle;
|
|
252
|
+
try {
|
|
253
|
+
handle = await herdr.start(
|
|
254
|
+
{ ...prepared, args: [...transient.launch.args] }, `i-${randomUUID().replaceAll("-", "").slice(0, 24)}`,
|
|
255
|
+
`Inspect ${id}`, `Analyze this saved, read-only status snapshot for request ${id}. Summarize blockers and safe next decisions; do not infer missing evidence or mutate resources. The subagent_status tool in Main remains authoritative for exact action identities.\n\n${snapshot}`,
|
|
256
|
+
controller.signal, (tab) => {
|
|
257
|
+
const record = { taskId, entryId: id, ...tab };
|
|
258
|
+
pi.appendEntry(DIRECT_TAB_TYPE, record);
|
|
259
|
+
tabs.push(record);
|
|
260
|
+
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}. Inspection Role prompt retained at ${transient.launch.args[prepared.promptArgIndex + 1]} after uncertain start.`, { cause: error });
|
|
263
|
+
}
|
|
264
|
+
handles.push(handle);
|
|
265
|
+
await transient.cleanup();
|
|
266
|
+
if (!current() || controller.signal.aborted) {
|
|
267
|
+
await handle.cancel();
|
|
268
|
+
throw new Error(`Session changed during inspection launch; inspect Herdr tab ${handle.tabId}.`);
|
|
269
|
+
}
|
|
270
|
+
return { tabId: handle.tabId, name: handle.name, sessionFile: handle.sessionFile };
|
|
271
|
+
} finally {
|
|
272
|
+
directTasks.delete(taskId);
|
|
273
|
+
resolveSettled();
|
|
274
|
+
}
|
|
275
|
+
},
|
|
232
276
|
canFollowup: (root, id, task) => isolatedSurface.canFollowup(root, id, task),
|
|
233
277
|
enqueue: (root, id, task, text, current) => isolatedSurface.enqueue(root, id, task, text, current),
|
|
234
278
|
drain: (root, id, task, current) => isolatedSurface.drain(root, id, task, current),
|
package/package.json
CHANGED
|
@@ -26,10 +26,10 @@ Use one compact `role`/`name`/`task` packet, `tasks` for independent packets, or
|
|
|
26
26
|
|
|
27
27
|
Give the request a unique ID and goal. Give each task a unique ID, kind, Role, model class, bounded requirements, deliverable, `dependsOn`, and `contextFrom`. Changesets require focused task checks, and a graph containing changesets requires final checks. The root must have a `package.json` test script; `pnpm test` is added to final checks if missing and runs against the combined tip before Main promotion. Text context may name only completed text tasks and preserves declared order.
|
|
28
28
|
|
|
29
|
-
Every changeset advances automatically after preliminary checks pass. While a locally owned changeset task is actively working, the user may open `/subagent` to queue bounded same-worker revisions or withdraw all still-pending instructions into the native editor and submit one replacement. Cancellation after withdrawal leaves the originals removed; already-claimed instructions cannot be recalled. Each revision must produce a new clean commit and rerun preliminary checks. When no queued revision remains, the runner seals and durably records the exact ready candidate without waiting for
|
|
29
|
+
Every changeset advances automatically after preliminary checks pass. While a locally owned changeset task is actively working, the user may open `/subagent` to queue bounded same-worker revisions or withdraw all still-pending instructions into the native editor and submit one replacement. Cancellation after withdrawal leaves the originals removed; already-claimed instructions cannot be recalled. Each revision must produce a new clean commit and rerun preliminary checks. When no queued revision remains, the runner seals and durably records the exact ready candidate without waiting for sibling workers. There is no guaranteed post-completion editing window, and late follow-ups fail visibly.
|
|
30
30
|
|
|
31
|
-
A ready candidate does not integrate automatically.
|
|
31
|
+
A ready candidate does not integrate automatically. Each checked worker triggers a follow-up while its siblings continue; inspect `subagent_status` and immediately use `subagent_stage` with the exact `generation`, `taskId`, `attempt`, candidate `tip`, and `expectedTip` to select and merge it into the owned integration checkout. Stage and resolve are available during the active wave; wait for the wave to settle before rejecting or revising. Resolve conflicts in the integration checkout and verify with `resolve`; never edit Main to resolve them. For dependents, `subagent_integrate advance` acknowledges a durable background wave, then reports checked candidates as they finish. When selected work is staged and the wave settles, use `subagent_integrate validate` on the exact `combinedTip` for full root checks and optional final judgment; `promote` requires passing evidence and unchanged Main. One Main-authored committed correction to a definitively failed combined validation can be recorded with `correct`, then revalidated. Only proven promotion triggers selected-worker termination and verified cleanup. Failures, ambiguity, conflicts, interruption, child-limit exhaustion, or Main drift retain work and never waive checks or identity guards. The extension never pushes, opens a pull request, publishes, or deploys.
|
|
32
32
|
|
|
33
|
-
Use `subagent_status` to inspect durable state without mutation, including candidate identities, generations, and retained resources. `subagent_resume` handles only the reported retry/verify/finalize continuation; it cannot replace Main's stage/integrate decisions or reset policy/corrections. `subagent_stage reject` or `revise` freezes a generation containing the candidate and invalidates dependent attempts; restage chosen work in a new generation. If Main advances cleanly before promotion, use `subagent_integrate refresh` with exact old/new Main and combined-tip identities; restage immutable candidates into the new generation and rerun combined validation. Dirty or divergent Main blocks refresh. At most two unreleased integration worktrees may coexist. Rejected candidates and superseded generations remain retained until Main explicitly calls `subagent_integrate release` with the exact generation and tip; for a rejected worker include its task ID and attempt after releasing all generations using it. Dirty/conflicted or unproved resources block release without force deletion. Impossible fresh dependent work after rejection needs a new request. `subagent_abort` cannot discard retained candidates or integration generations; it may finish once all are released. Old v4 state is rejected (current state is v5), not migrated. Status, abort, process I/O, termination, and verified post-promotion cleanup keep finite safety budgets.
|
|
33
|
+
After a coordinator interruption in the canonical repository, ask the user to run `/subagent recover` once: it acquires the productive lease, classifies orphaned interruptions, preserves invalid files and retained work, and sends Main one follow-up with blockers and next actions. A live productive lease is read-only and cannot be taken over. Recovery never replays prompts, merges, promotion, or cleanup. Use `subagent_status` to inspect exact durable state without mutation, including candidate identities, generations, and retained resources. `subagent_resume` handles only the reported retry/verify/finalize continuation; it cannot replace Main's stage/integrate decisions or reset policy/corrections. `subagent_stage reject` or `revise` freezes a generation containing the candidate and invalidates dependent attempts; restage chosen work in a new generation. If Main advances cleanly before promotion, use `subagent_integrate refresh` with exact old/new Main and combined-tip identities; restage immutable candidates into the new generation and rerun combined validation. Dirty or divergent Main blocks refresh. At most two unreleased integration worktrees may coexist. Rejected candidates and superseded generations remain retained until Main explicitly calls `subagent_integrate release` with the exact generation and tip; for a rejected worker include its task ID and attempt after releasing all generations using it. Dirty/conflicted or unproved resources block release without force deletion. Impossible fresh dependent work after rejection needs a new request. `subagent_abort` cannot discard retained candidates or integration generations; it may finish once all are released. Old v4 state is rejected (current state is v5), not migrated. Status, abort, process I/O, termination, and verified post-promotion cleanup keep finite safety budgets.
|
|
34
34
|
|
|
35
35
|
All launches use the global policy in `config/pi-subagent/config.json`: productive concurrency, per-child turns/tokens, and idle timeout. Ephemeral children also have a hard runtime limit; isolated requests have a correction cap. Productive requests have no whole-run wall-clock deadline. Request fields cannot replenish or override limits.
|
|
@@ -1,89 +0,0 @@
|
|
|
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>Delegation routing and checked task lifecycle</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>Delegation routing and checked task lifecycle</h1>
|
|
14
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="delegation-routing-title delegation-routing-desc">
|
|
15
|
-
<title id="delegation-routing-title">Delegation routing and checked task lifecycle</title>
|
|
16
|
-
<desc id="delegation-routing-desc">A read-only Role runs direct in the current workspace; implementation uses an isolated checked graph. In that graph workers produce a checked candidate, Main stages and validates the combined tip, then explicitly promotes it. Interruptions retain work for recovery and revalidation.</desc>
|
|
17
|
-
<defs>
|
|
18
|
-
<marker id="routing-arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#4c5665"/></marker>
|
|
19
|
-
<marker id="routing-arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#1d4ed8"/></marker>
|
|
20
|
-
<marker id="routing-arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#4c5665"/></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 / ROUTING + LIFECYCLE</text>
|
|
24
|
-
<text x="48" y="99" fill="#101828" font-family="'Instrument Serif',serif" font-size="28">One explicit mode; only checked work can be promoted</text>
|
|
25
|
-
<text x="48" y="126" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Direct work returns an observed result. Isolated changesets yield a candidate that Main must validate and promote.</text>
|
|
26
|
-
<!-- Routes and transitions are drawn before nodes. -->
|
|
27
|
-
<line x1="224" y1="212" x2="248" y2="212" stroke="#4c5665" marker-end="url(#routing-arrow)"/>
|
|
28
|
-
<path d="M336 268 V296 Q336 304 328 304 H192 Q184 304 184 312 V352" fill="none" stroke="#4c5665" marker-end="url(#routing-arrow)"/>
|
|
29
|
-
<path d="M424 212 H488 Q496 212 496 220 V352" fill="none" stroke="#1d4ed8" stroke-width="1.4" marker-end="url(#routing-arrow-accent)"/>
|
|
30
|
-
<line x1="560" y1="400" x2="624" y2="400" stroke="#4c5665" marker-end="url(#routing-arrow)"/>
|
|
31
|
-
<line x1="784" y1="400" x2="848" y2="400" stroke="#4c5665" marker-end="url(#routing-arrow)"/>
|
|
32
|
-
<line x1="1008" y1="400" x2="1072" y2="400" stroke="#1d4ed8" stroke-width="1.4" marker-end="url(#routing-arrow-accent)"/>
|
|
33
|
-
<!-- Interruption / re-entry paths use separate attach points. -->
|
|
34
|
-
<line x1="448" y1="448" x2="448" y2="512" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#routing-arrow)"/>
|
|
35
|
-
<line x1="512" y1="512" x2="512" y2="448" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#routing-arrow)"/>
|
|
36
|
-
<line x1="896" y1="448" x2="896" y2="512" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#routing-arrow)"/>
|
|
37
|
-
<line x1="960" y1="512" x2="960" y2="448" stroke="#4c5665" stroke-dasharray="5,4" marker-end="url(#routing-arrow)"/>
|
|
38
|
-
<!-- Arrow labels have an 8px gap to their strokes. -->
|
|
39
|
-
<g fill="#4c5665" font-family="'Geist Mono',monospace" font-size="8" text-anchor="middle">
|
|
40
|
-
<rect x="256" y="284" width="48" height="12" rx="2" fill="#f0eee9"/><text x="280" y="293">YES</text>
|
|
41
|
-
<rect x="432" y="192" width="72" height="12" rx="2" fill="#f0eee9"/><text x="468" y="201" fill="#1d4ed8">NO / GRAPH</text>
|
|
42
|
-
<rect x="568" y="380" width="48" height="12" rx="2" fill="#f0eee9"/><text x="592" y="389">CHECK</text>
|
|
43
|
-
<rect x="792" y="380" width="48" height="12" rx="2" fill="#f0eee9"/><text x="816" y="389">STAGE</text>
|
|
44
|
-
<rect x="1016" y="380" width="48" height="12" rx="2" fill="#f0eee9"/><text x="1040" y="389" fill="#1d4ed8">VERIFY</text>
|
|
45
|
-
<rect x="384" y="472" width="56" height="12" rx="2" fill="#f0eee9"/><text x="412" y="481">PAUSE</text>
|
|
46
|
-
<rect x="520" y="472" width="56" height="12" rx="2" fill="#f0eee9"/><text x="548" y="481">RESUME</text>
|
|
47
|
-
<rect x="816" y="472" width="72" height="12" rx="2" fill="#f0eee9"/><text x="852" y="481">MAIN DRIFT</text>
|
|
48
|
-
<rect x="968" y="472" width="72" height="12" rx="2" fill="#f0eee9"/><text x="1004" y="481">RECHECK</text>
|
|
49
|
-
</g>
|
|
50
|
-
<!-- Mode selection. -->
|
|
51
|
-
<rect x="80" y="180" width="144" height="64" rx="24" fill="#ffffff" stroke="#101828"/>
|
|
52
|
-
<text x="152" y="217" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Delegate task</text>
|
|
53
|
-
<polygon points="336,156 424,212 336,268 248,212" fill="#ffffff" stroke="#101828"/>
|
|
54
|
-
<text x="336" y="208" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Direct</text>
|
|
55
|
-
<text x="336" y="226" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">eligible?</text>
|
|
56
|
-
<!-- Direct is intentionally a single terminal branch: no worktree or candidate. -->
|
|
57
|
-
<text x="80" y="336" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">DIRECT</text>
|
|
58
|
-
<rect x="80" y="352" width="208" height="96" rx="8" fill="#ffffff" stroke="#101828"/>
|
|
59
|
-
<text x="184" y="384" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Herdr tabs</text>
|
|
60
|
-
<text x="184" y="406" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Current workspace</text>
|
|
61
|
-
<text x="184" y="428" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Observe result</text>
|
|
62
|
-
<text x="80" y="477" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Single · parallel · chain</text>
|
|
63
|
-
<!-- Isolated changeset rail. -->
|
|
64
|
-
<text x="400" y="336" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">ISOLATED / CHECKED CHANGESET</text>
|
|
65
|
-
<rect x="400" y="352" width="160" height="96" rx="8" fill="#ffffff" stroke="#101828"/>
|
|
66
|
-
<text x="480" y="387" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Saved + working</text>
|
|
67
|
-
<text x="480" y="411" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Durable ID</text>
|
|
68
|
-
<text x="480" y="429" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Owned worktree</text>
|
|
69
|
-
<rect x="624" y="352" width="160" height="96" rx="8" fill="#ffffff" stroke="#101828"/>
|
|
70
|
-
<text x="704" y="390" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Ready candidate</text>
|
|
71
|
-
<text x="704" y="416" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Clean commit + checks</text>
|
|
72
|
-
<rect x="848" y="352" width="160" height="96" rx="8" fill="#ffffff" stroke="#101828"/>
|
|
73
|
-
<text x="928" y="390" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Staged by Main</text>
|
|
74
|
-
<text x="928" y="416" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Exact candidate tip</text>
|
|
75
|
-
<rect x="1072" y="352" width="160" height="96" rx="8" fill="#1d4ed8" fill-opacity="0.08" stroke="#1d4ed8"/>
|
|
76
|
-
<text x="1152" y="390" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Promoted by Main</text>
|
|
77
|
-
<text x="1152" y="416" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">After combined checks</text>
|
|
78
|
-
<!-- Recovery states do not erase or bypass exact checks. -->
|
|
79
|
-
<text x="400" y="497" fill="#6a7282" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">WAIT / RECOVERY</text>
|
|
80
|
-
<rect x="400" y="512" width="160" height="72" rx="8" fill="#e6e4de" stroke="#4c5665" stroke-dasharray="5,4"/>
|
|
81
|
-
<text x="480" y="541" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Needs attention</text>
|
|
82
|
-
<text x="480" y="562" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Status / retry</text>
|
|
83
|
-
<rect x="848" y="512" width="160" height="72" rx="8" fill="#e6e4de" stroke="#4c5665" stroke-dasharray="5,4"/>
|
|
84
|
-
<text x="928" y="541" text-anchor="middle" fill="#101828" font-family="'Geist',sans-serif" font-size="12" font-weight="600">Revalidate</text>
|
|
85
|
-
<text x="928" y="562" text-anchor="middle" fill="#4c5665" font-family="'Geist',sans-serif" font-size="12">Restage if needed</text>
|
|
86
|
-
<line x1="48" y1="648" x2="1232" y2="648" stroke="#101828" stroke-opacity="0.12"/>
|
|
87
|
-
<text x="48" y="674" fill="#4c5665" font-family="'Geist Mono',monospace" font-size="8" letter-spacing="1">GUARD</text>
|
|
88
|
-
<text x="128" y="674" fill="#101828" font-family="'Geist',sans-serif" font-size="12">No fallback between modes. Failed validation retains evidence; abort requires no retained work.</text>
|
|
89
|
-
</svg></main></body></html>
|