@henryqw/pi-subagent 27.0.0 → 27.1.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/CONTEXT.md +1 -1
- package/README.md +17 -4
- package/dist/herdr-runtime.js +3 -1
- package/dist/integration-git.js +3 -3
- package/dist/prune.d.ts +50 -0
- package/dist/prune.js +192 -0
- package/dist/runner.d.ts +13 -0
- package/dist/runner.js +104 -10
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +10 -5
- package/dist/store.d.ts +2 -0
- package/dist/store.js +14 -2
- package/dist/worktree.d.ts +3 -0
- package/dist/worktree.js +3 -1
- package/docs/orchestration.md +4 -2
- package/extensions/isolated.ts +113 -36
- package/extensions/result-transport.ts +22 -2
- package/extensions/subagent-command.ts +88 -2
- package/extensions/subagent.ts +15 -5
- package/package.json +1 -1
- package/skills/pi-subagent/SKILL.md +2 -2
|
@@ -49,6 +49,22 @@ export interface IsolatedInventory {
|
|
|
49
49
|
invalidIds: readonly string[];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** One pi-subagent branch and its checkout as reviewed by /subagent prune. */
|
|
53
|
+
export interface PruneItem {
|
|
54
|
+
branch: string;
|
|
55
|
+
tip: string;
|
|
56
|
+
path?: string;
|
|
57
|
+
prunable: boolean;
|
|
58
|
+
/** Why the item is prunable, or every exact blocker. */
|
|
59
|
+
reason: string;
|
|
60
|
+
/** Ignored entries that removing the checkout deletes. */
|
|
61
|
+
ignored: readonly string[];
|
|
62
|
+
}
|
|
63
|
+
export interface PrunePlan {
|
|
64
|
+
root: string;
|
|
65
|
+
items: readonly PruneItem[];
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
/** Mutation adapters MUST check current() synchronously alongside their live ownership
|
|
53
69
|
* validation and queue operation. They must not await between those operations. */
|
|
54
70
|
export interface SubagentCommandAdapter {
|
|
@@ -57,6 +73,10 @@ export interface SubagentCommandAdapter {
|
|
|
57
73
|
stopDirect(task: DirectTask, entryId: string, current: () => boolean): Promise<string>;
|
|
58
74
|
isolated(cwd: string, current: () => boolean): Promise<IsolatedInventory>;
|
|
59
75
|
recover(cwd: string): Promise<string>;
|
|
76
|
+
/** Read-only review of pi-subagent worktrees and branches in the canonical repository. */
|
|
77
|
+
prunePlan(cwd: string): Promise<PrunePlan>;
|
|
78
|
+
/** Re-verify and remove reviewed prunable items; returns a bounded report. */
|
|
79
|
+
prune(cwd: string, items: readonly PruneItem[], allowIgnored: boolean, current: () => boolean): Promise<string>;
|
|
60
80
|
/** Read-only Role catalog report with routes and per-file load errors. */
|
|
61
81
|
roles(ctx: ExtensionContext): string;
|
|
62
82
|
inspectInTab(root: string, requestId: string, ctx: ExtensionContext, current: () => boolean): Promise<{ ref: string; tabId: string; name: string; sessionFile: string }>;
|
|
@@ -73,6 +93,39 @@ const errorText = (error: unknown): string => clean(error instanceof Error ? err
|
|
|
73
93
|
const completed = (status: string) => ["completed", "success", "failed", "failure", "aborted", "integrated"].includes(status);
|
|
74
94
|
const priority = (status: string) => /attention|blocked|retained|failed/i.test(status) ? 0 : completed(status) ? 2 : 1;
|
|
75
95
|
|
|
96
|
+
const ignoredSummary = ({ ignored }: PruneItem) => !ignored.length ? "none"
|
|
97
|
+
: `${ignored.length} (${ignored.slice(0, 5).join(", ")}${ignored.length > 5 ? `, +${ignored.length - 5} more` : ""})`;
|
|
98
|
+
const PRUNE_IGNORED_CONFIRM_CODE_POINTS = 4_000;
|
|
99
|
+
const NOT_SHOWN = "they are not removed in this run; run /subagent prune again after this run.";
|
|
100
|
+
|
|
101
|
+
/** Fit whole item lines within `max` code points; never cut silently, and report exactly which items were shown. */
|
|
102
|
+
function fitItems<T>(header: string, items: readonly T[], line: (item: T) => string, max: number): { text: string; shown: T[] } {
|
|
103
|
+
const lines = [header];
|
|
104
|
+
const shown: T[] = [];
|
|
105
|
+
let size = [...header].length;
|
|
106
|
+
const reserve = [...NOT_SHOWN].length + 32;
|
|
107
|
+
for (const item of items) {
|
|
108
|
+
const text = line(item);
|
|
109
|
+
const length = [...text].length + 1;
|
|
110
|
+
if (size + length + reserve > max) break;
|
|
111
|
+
lines.push(text);
|
|
112
|
+
size += length;
|
|
113
|
+
shown.push(item);
|
|
114
|
+
}
|
|
115
|
+
if (shown.length < items.length) lines.push(`+${items.length - shown.length} more not shown; ${NOT_SHOWN}`);
|
|
116
|
+
return { text: lines.join("\n"), shown };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Prunable items come first so a long blocked list cannot hide them; only shown items may be removed. */
|
|
120
|
+
function pruneReview({ root, items }: PrunePlan): { text: string; shown: PruneItem[] } {
|
|
121
|
+
if (!items.length) return { text: `No pi-subagent worktrees or branches in ${root}.`, shown: [] };
|
|
122
|
+
const prunable = items.filter((item) => item.prunable).length;
|
|
123
|
+
return fitItems(`Subagent prune review in ${root}: ${prunable} prunable, ${items.length - prunable} blocked.`,
|
|
124
|
+
[...items.filter((item) => item.prunable), ...items.filter((item) => !item.prunable)],
|
|
125
|
+
(item) => clean(`${item.prunable ? "✓ prunable" : "✗ blocked"} · ${item.path ?? "(no checkout)"} · ${item.branch} · ${item.tip.slice(0, 12)} · ${item.reason} · ignored: ${ignoredSummary(item)}`, 600),
|
|
126
|
+
ROLES_REPORT_CODE_POINTS);
|
|
127
|
+
}
|
|
128
|
+
|
|
76
129
|
type Choice<T> = { label: string; value: T };
|
|
77
130
|
async function choose<T>(ctx: ExtensionContext, title: string, items: readonly Choice<T>[]): Promise<T | undefined> {
|
|
78
131
|
// Numeric suffixes are UI-only. Never parse display text into an identity.
|
|
@@ -84,10 +137,10 @@ async function choose<T>(ctx: ExtensionContext, title: string, items: readonly C
|
|
|
84
137
|
|
|
85
138
|
export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentCommandAdapter): void {
|
|
86
139
|
pi.registerCommand("subagent", {
|
|
87
|
-
description: "Inspect direct and isolated work, list Roles with /subagent roles,
|
|
140
|
+
description: "Inspect direct and isolated work, list Roles with /subagent roles, recover orphaned isolated requests with /subagent recover, or remove merged leftover worktrees with /subagent prune",
|
|
88
141
|
handler: async (args, ctx) => {
|
|
89
142
|
const command = args.trim();
|
|
90
|
-
if (command &&
|
|
143
|
+
if (command && !["prune", "recover", "roles"].includes(command)) throw new Error("Usage: /subagent [prune|recover|roles]");
|
|
91
144
|
if (command === "roles") {
|
|
92
145
|
const report = clean(adapter.roles(ctx), ROLES_REPORT_CODE_POINTS, true);
|
|
93
146
|
if (ctx.hasUI) ctx.ui.notify(report, "info");
|
|
@@ -121,6 +174,39 @@ export function registerSubagentCommand(pi: ExtensionAPI, adapter: SubagentComma
|
|
|
121
174
|
ctx.ui.notify("Session or branch changed; reopen /subagent in the current context.", "warning");
|
|
122
175
|
return false;
|
|
123
176
|
};
|
|
177
|
+
if (command === "prune") {
|
|
178
|
+
const plan = await adapter.prunePlan(ctx.cwd);
|
|
179
|
+
const reviewed = pruneReview(plan);
|
|
180
|
+
const review = clean(reviewed.text, ROLES_REPORT_CODE_POINTS, true);
|
|
181
|
+
if (!ctx.hasUI) {
|
|
182
|
+
pi.sendMessage({ customType: "pi-subagent-prune", content: `${review}\nReport only: removal requires interactive confirmation.`, display: true });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (!valid()) return;
|
|
186
|
+
ctx.ui.notify(review, "info");
|
|
187
|
+
const prunable = reviewed.shown.filter((item) => item.prunable);
|
|
188
|
+
if (!prunable.length) return;
|
|
189
|
+
const confirmed = await ctx.ui.confirm("Prune subagent worktrees", `Remove all ${prunable.length} prunable item(s) listed above? Each is re-verified first; unique commits, tracked or untracked changes, and unlisted items are never removed.`);
|
|
190
|
+
if (!valid() || !confirmed) return;
|
|
191
|
+
const ignored = prunable.filter((item) => item.ignored.length);
|
|
192
|
+
// The header uses the full count's width so the shown count always fits.
|
|
193
|
+
const ignoredList = fitItems(`Also remove ${ignored.length} checkout(s) with ignored files? This permanently deletes these ignored entries and everything inside them:`,
|
|
194
|
+
// Approval names every ignored entry; a checkout whose full list does not fit is left out of this run.
|
|
195
|
+
ignored, (item) => clean(`${item.path}: ${item.ignored.join(", ")}`, Number.MAX_SAFE_INTEGER), PRUNE_IGNORED_CONFIRM_CODE_POINTS);
|
|
196
|
+
const approvable = ignoredList.shown;
|
|
197
|
+
const allowIgnored = approvable.length > 0 && await ctx.ui.confirm("Delete ignored content",
|
|
198
|
+
clean(ignoredList.text.replace(`Also remove ${ignored.length} checkout(s)`, `Also remove ${approvable.length} checkout(s)`), PRUNE_IGNORED_CONFIRM_CODE_POINTS, true));
|
|
199
|
+
if (!valid()) return;
|
|
200
|
+
const kept = ignored.length - (allowIgnored ? approvable.length : 0);
|
|
201
|
+
if (kept) ctx.ui.notify(`Kept ${kept} checkout(s) with ignored content; their deletion was not approved.`, "info");
|
|
202
|
+
const selected = prunable.filter((item) => !item.ignored.length || (allowIgnored && approvable.includes(item)));
|
|
203
|
+
if (!selected.length) return;
|
|
204
|
+
try {
|
|
205
|
+
const report = await adapter.prune(ctx.cwd, selected, allowIgnored, current);
|
|
206
|
+
if (valid()) ctx.ui.notify(clean(report, ROLES_REPORT_CODE_POINTS, true), "info");
|
|
207
|
+
} catch (error) { if (valid()) ctx.ui.notify(`Prune failed: ${errorText(error)}`, "error"); }
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
124
210
|
let history = false;
|
|
125
211
|
for (;;) {
|
|
126
212
|
if (!valid()) return;
|
package/extensions/subagent.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { registerCheckoutAdmission } from "./admission.ts";
|
|
|
8
8
|
import { registerIsolatedExtension } from "./isolated.ts";
|
|
9
9
|
import { directRef, registerSubagentCommand, type DirectDisplay, type DirectTask } from "./subagent-command.ts";
|
|
10
10
|
import { MODEL_CLASS_GUIDANCE } from "./model-class-policy.ts";
|
|
11
|
-
import { ENTRY_STATUS_PRESENTATION, formatWorkflowResult, workerOutputForMain, type BackgroundWorkflowTransportDetails, type WorkflowTransportEntry } from "./result-transport.ts";
|
|
11
|
+
import { compactTokens, ENTRY_STATUS_PRESENTATION, formatWorkflowResult, workerOutputForMain, type BackgroundWorkflowTransportDetails, type WorkflowTransportEntry } from "./result-transport.ts";
|
|
12
12
|
import { DelegateTaskParameters, identifyWorkflowEntries, parseDelegateTask, runForegroundWorkflow, writesCheckout, type ParsedWorkflow, type WorkflowEntry } from "./workflow.ts";
|
|
13
13
|
import { assertSettledDirectSession, createDirectHerdr, DirectAllocationError, type DirectAllocation, type DirectHandle, type DirectTab, type DirectToolActivity } from "../dist/direct-herdr.js";
|
|
14
14
|
import { materializeTransientLaunch } from "../dist/launch-runtime.js";
|
|
@@ -55,7 +55,7 @@ function statusLabel(status: WidgetStatus): string {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function tokenLabel(tokens: number | undefined): string {
|
|
58
|
-
return tokens === undefined ? "— tok" :
|
|
58
|
+
return tokens === undefined ? "— tok" : compactTokens(tokens);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function activityLabel(activity: DirectToolActivity | undefined): string | undefined {
|
|
@@ -304,6 +304,8 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
304
304
|
},
|
|
305
305
|
isolated: isolatedSurface.inventory,
|
|
306
306
|
recover: (cwd) => isolatedSurface.recover(cwd),
|
|
307
|
+
prunePlan: (cwd) => isolatedSurface.prunePlan(cwd),
|
|
308
|
+
prune: (cwd, items, allowIgnored, current) => isolatedSurface.prune(cwd, items, allowIgnored, current),
|
|
307
309
|
async inspectInTab(root, id, ctx, current) {
|
|
308
310
|
const notices = await isolatedSurface.inspect(root, id);
|
|
309
311
|
if (!current()) throw new Error("Session or branch changed before status inspection.");
|
|
@@ -654,6 +656,11 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
654
656
|
id, index, role: delegation.role, status: "pending",
|
|
655
657
|
name: delegation.continueFrom === undefined ? delegation.name : `${delegation.name} (continued from ${delegation.continueFrom})`,
|
|
656
658
|
}]));
|
|
659
|
+
const startedAt = new Map<string, number>();
|
|
660
|
+
const measured = (id: string, tokens: number | undefined) => ({
|
|
661
|
+
...(tokens === undefined ? {} : { tokens }),
|
|
662
|
+
...(startedAt.has(id) ? { elapsedMs: Date.now() - startedAt.get(id)! } : {}),
|
|
663
|
+
});
|
|
657
664
|
const taskId = `direct-${++directSequence}-${Date.now().toString(36)}`;
|
|
658
665
|
const workflowNumber = nextWorkflow(ctx);
|
|
659
666
|
const ref = `D${workflowNumber}`;
|
|
@@ -717,6 +724,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
717
724
|
const prepared = await prepareDirectLaunch(role, latestCtx ?? ctx, {
|
|
718
725
|
...(modelClass === undefined ? {} : { modelClass }), ...(model === undefined ? {} : { model }) });
|
|
719
726
|
states.set(entry.id, { ...states.get(entry.id)!, model: modelReference(prepared.model), thinkingLevel: prepared.thinkingLevel, status: "running", assistantOutput: "" });
|
|
727
|
+
startedAt.set(entry.id, Date.now());
|
|
720
728
|
const display = displayOf(entry);
|
|
721
729
|
startWidgetItem(entry.id, taskId, display, prepared.model.id, prepared.thinkingLevel, ctx);
|
|
722
730
|
const transient = await materializeTransientLaunch({ launch: prepared, prompt: prepared.systemPrompt,
|
|
@@ -795,24 +803,26 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
795
803
|
if (writerFailed) throw new Error("Previous direct task did not complete exactly; inspect recorded tabs before new shared-checkout work.");
|
|
796
804
|
handle = entry.index === 0 ? first : await launch(entry, entrySignal(entry));
|
|
797
805
|
const answer = await handle.answer(Math.floor(40 * 1024 / entries.length));
|
|
806
|
+
let tokens: number | undefined;
|
|
798
807
|
try {
|
|
799
|
-
|
|
808
|
+
tokens = await handle.usageTokens();
|
|
800
809
|
const item = widgetItems.get(entry.id);
|
|
801
810
|
if (item) { item.tokens = tokens; item.activity = handle.activity; }
|
|
802
811
|
} catch { /* Optional telemetry. */ }
|
|
803
|
-
states.set(entry.id, { ...states.get(entry.id)!, status: "succeeded", assistantOutput: answer });
|
|
812
|
+
states.set(entry.id, { ...states.get(entry.id)!, status: "succeeded", assistantOutput: answer, ...measured(entry.id, tokens) });
|
|
804
813
|
finishWidgetItem(entry.id, "success");
|
|
805
814
|
return answer;
|
|
806
815
|
} catch (error) {
|
|
807
816
|
if (writes) writerFailed = true;
|
|
808
817
|
const { assistantOutput: _partial, ...base } = states.get(entry.id)!;
|
|
809
818
|
const partialOutput = await handle?.partialText(PARTIAL_OUTPUT_CHARS).catch(() => undefined);
|
|
819
|
+
const tokens = await handle?.usageTokens().catch(() => undefined);
|
|
810
820
|
const message = (value: unknown) => value instanceof Error ? value.message : String(value);
|
|
811
821
|
const stopped = await stopProofs.get(entry.id)?.then(() => STOPPED_BY_USER,
|
|
812
822
|
(stopError: unknown) => `${STOPPED_BY_USER}; termination unproved: ${message(stopError)}`);
|
|
813
823
|
const tab = tabByEntry.get(entry.id);
|
|
814
824
|
const recover = tab && stopped !== STOPPED_BY_USER ? `recover from Herdr tab ${tab.tabId}, agent ${tab.name}, session ${tab.sessionFile}: ` : "";
|
|
815
|
-
states.set(entry.id, { ...base, status: "rejected", ...(partialOutput ? { partialOutput } : {}), failure: `${recover}${capOutput(stopped ?? message(error))}` });
|
|
825
|
+
states.set(entry.id, { ...base, status: "rejected", ...(partialOutput ? { partialOutput } : {}), ...measured(entry.id, tokens), failure: `${recover}${capOutput(stopped ?? message(error))}` });
|
|
816
826
|
finishWidgetItem(entry.id, "failure");
|
|
817
827
|
throw error;
|
|
818
828
|
} finally { release(); }
|
package/package.json
CHANGED
|
@@ -32,10 +32,10 @@ To build on a finished direct worker, add `continueFrom` with its reference (suc
|
|
|
32
32
|
|
|
33
33
|
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, preserves declared order, and implies a scheduling dependency; do not repeat an edge in `dependsOn` and `contextFrom`.
|
|
34
34
|
|
|
35
|
-
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.
|
|
35
|
+
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. If a worker's turn ends with uncommitted changes, the runner sends it one commit reminder per attempt; this uses no correction, and a worktree still dirty afterwards needs attention. 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.
|
|
36
36
|
|
|
37
37
|
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`: it acknowledges saved intent, runs full root checks and optional final judgment in the session-owned background, then sends a follow-up. Acknowledgement is not passing evidence; `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.
|
|
38
38
|
|
|
39
|
-
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. For interrupted validation, prove old coordinator/check processes stopped, then guarded `reconcile` classifies the exact generation; never resume a live lease. A `validation_failed` generation without checks, review, or a prior combined correction can `refresh` with identical `expectedMain`/`newMain`, the old exact combined `expectedTip`, and the next generation number. Main and the retained integration tip must be exact and clean. Restage immutable candidates explicitly and rerun all final gates in the new checkout; never replay old uncertain checks, infer passing evidence, or reset correction allowances. 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; release them first. For workers without candidates, abort cleans only proven unchanged, clean owned resources. If cleanup is deferred or blocked, inspect status and repeat abort after resolving the blocker. Dirty or committed work and uncertain allocations remain for manual recovery. 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.
|
|
39
|
+
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. For leftover merged worker or integration checkouts, ask the user to run `/subagent prune`; it removes only reviewed, confirmed, merged, clean, unowned items. 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. For a retry that sends a same-worker correction, pass `instruction` to name the exact fixes, including the review or check findings the worker must address; the correction prompt carries only a short saved failure. Follow-ups show a short preview of a failed review verdict; `subagent_status` returns it in full. `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. For interrupted validation, prove old coordinator/check processes stopped, then guarded `reconcile` classifies the exact generation; never resume a live lease. A `validation_failed` generation without checks, review, or a prior combined correction can `refresh` with identical `expectedMain`/`newMain`, the old exact combined `expectedTip`, and the next generation number. Main and the retained integration tip must be exact and clean. Restage immutable candidates explicitly and rerun all final gates in the new checkout; never replay old uncertain checks, infer passing evidence, or reset correction allowances. 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; release them first. For workers without candidates, abort cleans only proven unchanged, clean owned resources. If cleanup is deferred or blocked, inspect status and repeat abort after resolving the blocker. Dirty or committed work and uncertain allocations remain for manual recovery. 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.
|
|
40
40
|
|
|
41
41
|
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.
|