@henryqw/pi-subagent 22.2.1 → 22.2.3
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 +1 -1
- package/dist/herdr-runtime.js +8 -12
- package/extensions/result-transport.ts +7 -8
- package/extensions/subagent.ts +13 -30
- package/package.json +1 -1
- package/skills/pi-subagent/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ An isolated request has one durable ID, one goal, and 1–8 typed tasks:
|
|
|
126
126
|
|
|
127
127
|
`dependsOn` controls scheduling. `contextFrom` may name completed text tasks and preserves the declared order. Changesets require focused task checks; a graph with changesets requires final checks. A canonical root `package.json` test script is required: the runner adds `pnpm test` to final checks if missing, and Main runs it on the combined integration checkout before promotion. Checks and judgments bind to exact Git identities; candidate drift, Main drift, mutation during validation, ambiguity, or conflicts stop promotion and retain evidence.
|
|
128
128
|
|
|
129
|
-
`delegate_task`, `subagent_resume`, and `subagent_integrate advance` return a durable request ID after the wave is saved; productive work continues while Main is free. Pi sends a follow-up in the launching session whenever a checked worker becomes ready, and again when the wave finishes or needs attention. Use `subagent_status` if delivery is missed or you need current state.
|
|
129
|
+
`delegate_task`, `subagent_resume`, and `subagent_integrate advance` return a durable request ID after the wave is saved; productive work continues while Main is free. Main can do other work or end its turn instead of sleeping or polling. Pi sends a follow-up in the launching session whenever a checked worker becomes ready, and again when the wave finishes or needs attention. Use `subagent_status` if delivery is missed or you need exact current state.
|
|
130
130
|
|
|
131
131
|
The worker produces a committed candidate without tracked or untracked changes, then runs focused preliminary checks and optional judgment. Ignored generated files do not block candidacy, but prevent automatic worktree cleanup; inspect them before deleting retained work. The runner records each ready candidate immediately but **does not integrate it automatically**. Main can inspect `subagent_status` and use `subagent_stage` to select the exact candidate in an owned integration worktree while siblings are still working; conflicts are resolved there. Stage and resolve are available during an active wave, but rejection and revision wait for the wave to settle. `subagent_integrate` can advance dependent tasks from a staged snapshot, validate the combined tip with full root checks and optional final judgment after the wave settles, and promote it to Main only after exact validation. A definitive failed combined check/review allows one committed correction in the integration checkout followed by another validation. Main must explicitly promote; promotion and cleanup never silently discard retained work.
|
|
132
132
|
|
package/dist/herdr-runtime.js
CHANGED
|
@@ -152,9 +152,6 @@ function worktreeIntent(attempt) {
|
|
|
152
152
|
export function workspaceLabel(attempt) {
|
|
153
153
|
return attempt.correlationToken.slice(-6);
|
|
154
154
|
}
|
|
155
|
-
function legacyWorkspaceLabel(requestId, task, attempt) {
|
|
156
|
-
return `${requestId}/${task.id}#${attempt.number}`;
|
|
157
|
-
}
|
|
158
155
|
function expectedWorkerLabel(task) {
|
|
159
156
|
return `${task.role}/${task.modelClass}`;
|
|
160
157
|
}
|
|
@@ -174,10 +171,9 @@ function requireIntentIdentity(intent, attempt) {
|
|
|
174
171
|
throw new Error(`${intent.kind} allocation intent does not match the task attempt.`);
|
|
175
172
|
}
|
|
176
173
|
}
|
|
177
|
-
function assertWorkspaceIntent(intent,
|
|
174
|
+
function assertWorkspaceIntent(intent, attempt) {
|
|
178
175
|
const worktree = worktreeIntent(attempt);
|
|
179
|
-
|
|
180
|
-
if ((label !== workspaceLabel(attempt) && label !== legacyWorkspaceLabel(requestId, task, attempt))
|
|
176
|
+
if (exactString(intent.label, "workspace label") !== workspaceLabel(attempt)
|
|
181
177
|
|| exactAbsolutePath(intent.worktreeCwd, "workspace worktree cwd") !== worktree.cwd
|
|
182
178
|
|| exactAbsolutePath(intent.mainRoot, "workspace Main root") !== worktree.repoRoot) {
|
|
183
179
|
throw new Error("Workspace allocation plan drifted from the exact owned worktree.");
|
|
@@ -368,7 +364,7 @@ export class HerdrHostRuntime {
|
|
|
368
364
|
if (input.intent.kind === "workspace") {
|
|
369
365
|
if (input.acquireLaunch)
|
|
370
366
|
throw new Error("Role launch acquisition is valid only at the exact agent allocation boundary.");
|
|
371
|
-
return await this.allocateWorkspace(input.intent, input.
|
|
367
|
+
return await this.allocateWorkspace(input.intent, input.attempt, context);
|
|
372
368
|
}
|
|
373
369
|
if (input.intent.kind === "worker_tab") {
|
|
374
370
|
if (input.acquireLaunch)
|
|
@@ -381,7 +377,7 @@ export class HerdrHostRuntime {
|
|
|
381
377
|
requireIntentIdentity(input.intent, input.attempt);
|
|
382
378
|
const allocation = input.intent;
|
|
383
379
|
if (allocation.kind === "workspace") {
|
|
384
|
-
assertWorkspaceIntent(allocation, input.
|
|
380
|
+
assertWorkspaceIntent(allocation, input.attempt);
|
|
385
381
|
await this.assertRepositoryIdentity(allocation, context);
|
|
386
382
|
const response = await this.herdr.json(["worktree", "list", "--cwd", allocation.worktreeCwd], this.processOptions(allocation.herdrRepoRoot, context, HERDR_OPERATION_CAP_MS));
|
|
387
383
|
const result = resultRecord(response, "Herdr worktree list response");
|
|
@@ -656,7 +652,7 @@ export class HerdrHostRuntime {
|
|
|
656
652
|
assertWorkerTabIntent(allocation, input.task, input.attempt);
|
|
657
653
|
const leasePath = exactString(allocation.leasePath, "saved worker lease path");
|
|
658
654
|
this.assertLeasePath(leasePath, input.attempt.correlationToken);
|
|
659
|
-
assertWorkspaceIntent(workspace, input.
|
|
655
|
+
assertWorkspaceIntent(workspace, input.attempt);
|
|
660
656
|
await this.assertRepositoryIdentity(workspace, context);
|
|
661
657
|
const tabId = exactString(allocation.tabId, "saved worker tab ID");
|
|
662
658
|
const workspaceId = exactString(workspace.workspaceId, "saved workspace ID");
|
|
@@ -688,7 +684,7 @@ export class HerdrHostRuntime {
|
|
|
688
684
|
return { outcome: "blocked", failure: "Workspace cleanup must follow worker-tab reconciliation." };
|
|
689
685
|
const allocation = ownedIntent(input.attempt, "workspace");
|
|
690
686
|
requireIntentIdentity(allocation, input.attempt);
|
|
691
|
-
assertWorkspaceIntent(allocation, input.
|
|
687
|
+
assertWorkspaceIntent(allocation, input.attempt);
|
|
692
688
|
await this.assertRepositoryIdentity(allocation, context);
|
|
693
689
|
const workspaceId = exactString(allocation.workspaceId, "saved workspace ID");
|
|
694
690
|
if (await this.workspaceAbsent(workspaceId, allocation, context))
|
|
@@ -708,8 +704,8 @@ export class HerdrHostRuntime {
|
|
|
708
704
|
return { outcome: "blocked", failure: safeText(error) };
|
|
709
705
|
}
|
|
710
706
|
}
|
|
711
|
-
async allocateWorkspace(allocation,
|
|
712
|
-
assertWorkspaceIntent(allocation,
|
|
707
|
+
async allocateWorkspace(allocation, attempt, context) {
|
|
708
|
+
assertWorkspaceIntent(allocation, attempt);
|
|
713
709
|
await this.assertRepositoryIdentity(allocation, context);
|
|
714
710
|
const args = ["worktree", "open", "--cwd", allocation.herdrRepoRoot, "--path", allocation.worktreeCwd, "--label", allocation.label, "--no-focus"];
|
|
715
711
|
const response = await this.herdr.exec(args, this.processOptions(allocation.herdrRepoRoot, context, HERDR_OPERATION_CAP_MS));
|
|
@@ -3,7 +3,7 @@ import type { WorkflowEntry, WorkflowMode } from "./workflow.ts";
|
|
|
3
3
|
|
|
4
4
|
const EVIDENCE_PREVIEW_CODE_POINTS = 256;
|
|
5
5
|
|
|
6
|
-
export type WorkflowTransportStatus = "pending" | "running" | "succeeded" | "
|
|
6
|
+
export type WorkflowTransportStatus = "pending" | "running" | "succeeded" | "rejected" | "skipped";
|
|
7
7
|
|
|
8
8
|
type TransportEntryBase = {
|
|
9
9
|
id: WorkflowEntry["id"];
|
|
@@ -17,7 +17,7 @@ type TransportEntryBase = {
|
|
|
17
17
|
export type WorkflowTransportEntry =
|
|
18
18
|
| TransportEntryBase & { status: "pending" | "skipped"; assistantOutput?: never; failure?: never }
|
|
19
19
|
| TransportEntryBase & { status: "running" | "succeeded"; assistantOutput: string; failure?: never }
|
|
20
|
-
| TransportEntryBase & { status: "
|
|
20
|
+
| TransportEntryBase & { status: "rejected"; assistantOutput?: never; failure: string };
|
|
21
21
|
|
|
22
22
|
export type WorkflowTransportEntryDetails = {
|
|
23
23
|
id: string;
|
|
@@ -37,7 +37,7 @@ export type WorkflowTransportDetails = {
|
|
|
37
37
|
|
|
38
38
|
export type BackgroundWorkflowTransportDetails = WorkflowTransportDetails & {
|
|
39
39
|
taskId: string;
|
|
40
|
-
outcome: "completed" | "failed"
|
|
40
|
+
outcome: "completed" | "failed";
|
|
41
41
|
recovery?: true;
|
|
42
42
|
};
|
|
43
43
|
|
|
@@ -63,7 +63,7 @@ function workflowTitle(mode: WorkflowMode): string {
|
|
|
63
63
|
function statusCounts(entries: readonly WorkflowTransportEntry[]): string[] {
|
|
64
64
|
const count = (statuses: WorkflowTransportStatus[]) => entries.filter(({ status }) => statuses.includes(status)).length;
|
|
65
65
|
return ([
|
|
66
|
-
[["
|
|
66
|
+
[["rejected"], "failed"],
|
|
67
67
|
[["succeeded"], "completed"],
|
|
68
68
|
[["skipped"], "skipped"],
|
|
69
69
|
] as const).flatMap(([statuses, word]) => {
|
|
@@ -84,7 +84,6 @@ const ENTRY_STATUS_PRESENTATION = {
|
|
|
84
84
|
pending: { glyph: "○", fallback: "queued" },
|
|
85
85
|
running: { glyph: "◌", fallback: "working" },
|
|
86
86
|
succeeded: { glyph: "✓", fallback: "completed" },
|
|
87
|
-
failed: { glyph: "✗", fallback: "failed" },
|
|
88
87
|
rejected: { glyph: "✗", fallback: "failed" },
|
|
89
88
|
skipped: { glyph: "–", fallback: "skipped" },
|
|
90
89
|
} as const satisfies Record<WorkflowTransportEntryDetails["status"], WorkflowEntryStatusPresentation>;
|
|
@@ -94,7 +93,7 @@ export function presentWorkflowEntryStatus(status: WorkflowTransportEntryDetails
|
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
function sourceFor(entry: WorkflowTransportEntry): string | undefined {
|
|
97
|
-
if (entry.status === "
|
|
96
|
+
if (entry.status === "rejected") return entry.failure;
|
|
98
97
|
if (entry.status === "running" || entry.status === "succeeded") return entry.assistantOutput;
|
|
99
98
|
}
|
|
100
99
|
|
|
@@ -103,7 +102,7 @@ function evidenceFor(entry: WorkflowTransportEntry, position: number, total: num
|
|
|
103
102
|
if (source === undefined || !source) return;
|
|
104
103
|
const [preview, remainder] = splitEvidence(source);
|
|
105
104
|
return {
|
|
106
|
-
heading: `- [${position}/${total}] ${entry.name} · ${entry.role} · ${entry.status === "
|
|
105
|
+
heading: `- [${position}/${total}] ${entry.name} · ${entry.role} · ${entry.status === "rejected" ? "failure" : "result"}:`,
|
|
107
106
|
preview,
|
|
108
107
|
remainder,
|
|
109
108
|
};
|
|
@@ -114,7 +113,7 @@ export function formatWorkflowResult(mode: WorkflowMode, entries: readonly Workf
|
|
|
114
113
|
if (ordered.some(({ status }) => status === "pending" || status === "running")) {
|
|
115
114
|
throw new TypeError("Final workflow transport requires terminal entry states.");
|
|
116
115
|
}
|
|
117
|
-
const failed = ordered.some(({ status }) => status === "
|
|
116
|
+
const failed = ordered.some(({ status }) => status === "rejected");
|
|
118
117
|
const positioned = ordered.map((entry, index) => ({ entry, position: index + 1 }));
|
|
119
118
|
const evidence = positioned.flatMap(({ entry, position }) => {
|
|
120
119
|
const value = evidenceFor(entry, position, ordered.length);
|
package/extensions/subagent.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { type Component, Text, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import { availableTaskModels, loadTaskModelsConfig, modelReference, registerModelTask, resolveAvailableModel, type ResolvedTaskRoute, taskThinkingLevels } from "@henryqw/pi-task-models";
|
|
5
|
-
import { capEphemeralSubagentOutput as capOutput, createEphemeralSubagentExecutor, DELEGATE_TASK, formatDuration, loadRoles, prepareRoleLaunch, ROLE_TOOL_POLICY_FLAG, type
|
|
5
|
+
import { capEphemeralSubagentOutput as capOutput, createEphemeralSubagentExecutor, DELEGATE_TASK, formatDuration, loadRoles, prepareRoleLaunch, ROLE_TOOL_POLICY_FLAG, type Role } from "@henryqw/pi-subagent";
|
|
6
6
|
import { DEFAULT_EXECUTION_POLICY, DEFAULT_TIMEOUT_CONFIG, readSubagentConfig, resolveExecutionPolicy, type EffectiveExecutionPolicy } from "./config.ts";
|
|
7
7
|
import { createCheckoutAdmission, roleCanWrite } from "./admission.ts";
|
|
8
8
|
import { registerIsolatedExtension } from "./isolated.ts";
|
|
@@ -18,8 +18,7 @@ const MAX_WIDGET_ITEMS = 8;
|
|
|
18
18
|
const MAX_WIDGET_LINES = 6;
|
|
19
19
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
20
20
|
|
|
21
|
-
type
|
|
22
|
-
type WidgetStatus = "working" | "success" | "failure" | "aborted";
|
|
21
|
+
type WidgetStatus = "working" | "success" | "failure";
|
|
23
22
|
type WidgetItem = {
|
|
24
23
|
role: string;
|
|
25
24
|
model: string;
|
|
@@ -42,7 +41,6 @@ function statusGlyph(status: WidgetStatus, spinnerIndex: number, theme: Theme):
|
|
|
42
41
|
case "working": return theme.fg("accent", SPINNER_FRAMES[spinnerIndex % SPINNER_FRAMES.length]!);
|
|
43
42
|
case "success": return theme.fg("success", "✓");
|
|
44
43
|
case "failure": return theme.fg("error", "✗");
|
|
45
|
-
case "aborted": return theme.fg("warning", "■");
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
46
|
|
|
@@ -51,7 +49,6 @@ function statusLabel(status: WidgetStatus): string {
|
|
|
51
49
|
case "working": return "working";
|
|
52
50
|
case "success": return "complete";
|
|
53
51
|
case "failure": return "failed";
|
|
54
|
-
case "aborted": return "stopped";
|
|
55
52
|
}
|
|
56
53
|
}
|
|
57
54
|
|
|
@@ -87,11 +84,11 @@ function renderWidgetRows(
|
|
|
87
84
|
});
|
|
88
85
|
const hidden = ordered.slice(visible.length);
|
|
89
86
|
if (hidden.length) {
|
|
90
|
-
const counts: Record<WidgetStatus, number> = { working: 0, success: 0, failure: 0
|
|
87
|
+
const counts: Record<WidgetStatus, number> = { working: 0, success: 0, failure: 0 };
|
|
91
88
|
for (const { status } of hidden) counts[status] += 1;
|
|
92
89
|
lines.push(truncateToWidth(theme.fg("muted", [
|
|
93
90
|
`… ${hidden.length} more`,
|
|
94
|
-
...(["working", "success", "failure"
|
|
91
|
+
...(["working", "success", "failure"] as const).flatMap((status) =>
|
|
95
92
|
counts[status] ? [`${counts[status]} ${statusLabel(status)}`] : []),
|
|
96
93
|
].join(" · ")), width));
|
|
97
94
|
}
|
|
@@ -128,10 +125,7 @@ const roleSummary = (): string => {
|
|
|
128
125
|
}
|
|
129
126
|
};
|
|
130
127
|
|
|
131
|
-
export default function subagentExtension(
|
|
132
|
-
pi: ExtensionAPI,
|
|
133
|
-
overrideTimeoutPolicy?: TimeoutPolicy,
|
|
134
|
-
): void {
|
|
128
|
+
export default function subagentExtension(pi: ExtensionAPI): void {
|
|
135
129
|
if (process.argv.includes(`--${ROLE_TOOL_POLICY_FLAG}`)) return;
|
|
136
130
|
registerModelTask(pi, DELEGATE_TASK);
|
|
137
131
|
pi.registerMessageRenderer(DIRECT_RESULT_TYPE, (message, { expanded, outputPad }, theme) => {
|
|
@@ -142,11 +136,9 @@ export default function subagentExtension(
|
|
|
142
136
|
if (!details?.entries) return new Text(content, outputPad, 0);
|
|
143
137
|
const count = details.entries.length;
|
|
144
138
|
const subject = count === 1 ? "Direct subagent" : `${count} direct subagents`;
|
|
145
|
-
const state = details.recovery ? "stopped; recovery needed"
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const glyph = details.recovery || details.outcome === "aborted" ? "■" : details.outcome === "completed" ? "✓" : "✗";
|
|
149
|
-
const color = details.recovery || details.outcome === "aborted" ? "warning" : details.outcome === "completed" ? "success" : "error";
|
|
139
|
+
const state = details.recovery ? "stopped; recovery needed" : details.outcome;
|
|
140
|
+
const glyph = details.recovery ? "■" : details.outcome === "completed" ? "✓" : "✗";
|
|
141
|
+
const color = details.recovery ? "warning" : details.outcome === "completed" ? "success" : "error";
|
|
150
142
|
const rows = details.entries.map(({ name, role, status, summary }) => {
|
|
151
143
|
const { glyph, fallback } = presentWorkflowEntryStatus(status);
|
|
152
144
|
return `${glyph} ${name} · ${role} — ${details.recovery ? fallback : summary || fallback}`;
|
|
@@ -175,20 +167,11 @@ export default function subagentExtension(
|
|
|
175
167
|
maxCorrections: DEFAULT_EXECUTION_POLICY.maxCorrections,
|
|
176
168
|
};
|
|
177
169
|
}
|
|
178
|
-
const timeoutPolicy: TimeoutPolicy = overrideTimeoutPolicy ?? {
|
|
179
|
-
idleMs: initialPolicy.childIdleMs,
|
|
180
|
-
maxMs: initialPolicy.childMaxMs,
|
|
181
|
-
};
|
|
182
|
-
const effectiveInitialPolicy: EffectiveExecutionPolicy = {
|
|
183
|
-
...initialPolicy,
|
|
184
|
-
childIdleMs: timeoutPolicy.idleMs,
|
|
185
|
-
childMaxMs: timeoutPolicy.maxMs ?? initialPolicy.childMaxMs,
|
|
186
|
-
};
|
|
187
170
|
const executor = createEphemeralSubagentExecutor({
|
|
188
|
-
maxConcurrency:
|
|
189
|
-
maxTurns:
|
|
190
|
-
maxTokens:
|
|
191
|
-
timeout:
|
|
171
|
+
maxConcurrency: initialPolicy.maxSubagents,
|
|
172
|
+
maxTurns: initialPolicy.maxTurns,
|
|
173
|
+
maxTokens: initialPolicy.maxTokens,
|
|
174
|
+
timeout: { idleMs: initialPolicy.childIdleMs, maxMs: initialPolicy.childMaxMs },
|
|
192
175
|
});
|
|
193
176
|
const currentPolicy = (): EffectiveExecutionPolicy => resolveExecutionPolicy(readSubagentConfig());
|
|
194
177
|
const canWrite = (input: unknown): boolean => {
|
|
@@ -205,7 +188,7 @@ export default function subagentExtension(
|
|
|
205
188
|
admission.register(pi, canWrite);
|
|
206
189
|
const isolatedSurface = registerIsolatedExtension(pi, {
|
|
207
190
|
executor,
|
|
208
|
-
policy:
|
|
191
|
+
policy: initialPolicy,
|
|
209
192
|
currentPolicy,
|
|
210
193
|
});
|
|
211
194
|
let directSequence = 0;
|
package/package.json
CHANGED
|
@@ -18,6 +18,8 @@ Keep trivial, mechanically verifiable work in Main. Choose the user-requested mo
|
|
|
18
18
|
|
|
19
19
|
Roles describe responsibility and capabilities; they do not select isolation. Bash, MCP tools, unknown tools, and arbitrary extensions are potentially writing. The checkout coordinator reduces races among Pi-owned calls but is not an OS sandbox and cannot control external processes.
|
|
20
20
|
|
|
21
|
+
After delegation acknowledges a handle or durable request, do other work or end the turn; do not `sleep`, block on a Herdr CLI wait, or repeatedly poll for progress. Pi queues verified direct results and checked isolated candidates as follow-ups, which Main receives after its current turn. Use `subagent_status` for exact isolated evidence when a follow-up arrives or delivery was missed; Herdr lifecycle badges and screen output are not proof of completion.
|
|
22
|
+
|
|
21
23
|
# Direct requests
|
|
22
24
|
|
|
23
25
|
Use one compact `role`/`name`/`task` packet, `tasks` for independent packets, or `chain` for dependent packets. Only text tasks are accepted; a write-capable Role, `kind: changeset`, checks, and judgment require isolated mode. The tool returns a handle after launching Herdr tabs in the current workspace. Main receives the verified result as a follow-up after the workflow finishes, without interrupting an active turn. If observation stops, open `/subagent` to inspect exact recorded tabs and session files on this session branch; recorded does not prove still running.
|