@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 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
 
@@ -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, requestId, task, attempt) {
174
+ function assertWorkspaceIntent(intent, attempt) {
178
175
  const worktree = worktreeIntent(attempt);
179
- const label = exactString(intent.label, "workspace label");
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.requestId, input.task, input.attempt, context);
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.requestId, input.task, input.attempt);
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.requestId, input.task, input.attempt);
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.requestId, input.task, input.attempt);
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, requestId, task, attempt, context) {
712
- assertWorkspaceIntent(allocation, requestId, task, attempt);
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" | "failed" | "rejected" | "skipped";
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: "failed" | "rejected"; assistantOutput?: never; failure: string };
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" | "aborted";
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
- [["failed", "rejected"], "failed"],
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 === "failed" || entry.status === "rejected") return entry.failure;
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 === "failed" || entry.status === "rejected" ? "failure" : "result"}:`,
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 === "failed" || status === "rejected");
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);
@@ -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 EphemeralSubagentTimeout, type Role } from "@henryqw/pi-subagent";
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 TimeoutPolicy = EphemeralSubagentTimeout;
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, aborted: 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", "aborted"] as const).flatMap((status) =>
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
- : details.outcome === "completed" ? "completed"
147
- : details.outcome === "failed" ? "failed" : "stopped";
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: effectiveInitialPolicy.maxSubagents,
189
- maxTurns: effectiveInitialPolicy.maxTurns,
190
- maxTokens: effectiveInitialPolicy.maxTokens,
191
- timeout: timeoutPolicy,
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: effectiveInitialPolicy,
191
+ policy: initialPolicy,
209
192
  currentPolicy,
210
193
  });
211
194
  let directSequence = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "22.2.1",
3
+ "version": "22.2.3",
4
4
  "description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -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.