pi-subagents 0.41.0 → 0.42.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.
@@ -0,0 +1,11 @@
1
+ import { createJiti } from "jiti";
2
+
3
+ const jiti = createJiti(import.meta.url);
4
+ const { runInspector } = await jiti.import("./src/inspectors/herdr/inspector-runner.ts");
5
+
6
+ try {
7
+ runInspector();
8
+ } catch (cause) {
9
+ process.stderr.write(`Herdr inspector failed: ${cause instanceof Error ? cause.message : String(cause)}\n`);
10
+ process.exitCode = 1;
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -94,9 +94,9 @@
94
94
  "devDependencies": {
95
95
  "@earendil-works/pi-agent-core": "0.81.0",
96
96
  "@earendil-works/pi-ai": "0.81.0",
97
- "@earendil-works/pi-coding-agent": "0.81.0",
98
97
  "@earendil-works/pi-tui": "0.81.0",
99
98
  "@types/node": "24.13.3",
100
- "typescript": "5.9.3"
99
+ "typescript": "5.9.3",
100
+ "@earendil-works/pi-coding-agent": "file:./test/fixtures/pi-coding-agent-shim"
101
101
  }
102
102
  }
@@ -31,9 +31,11 @@ For broad or uncertain requests, read more than one reference. For complex work,
31
31
 
32
32
  - Keep the parent as orchestrator and final decision-maker.
33
33
  - Use one writer per cwd/worktree unless isolated worktrees are intentional.
34
- - For parallel fanout, compare child prompts before launch. Do not send clone prompts with only issue numbers, titles, or broad file globs swapped; each child needs a lane-specific task, source seam, prior evidence, and decision that remains distinct without the item number. Launch that fanout as one `workflowScript` with stable keys and aggregate output unless there is truly only one child.
34
+ - For cross-codebase work, record the target repo, explicit `cwd`, authority boundary, and expected output before launch. Do not assume the parent session cwd is the child repo.
35
+ - For parallel fanout, compare child prompts before launch. Do not send clone prompts with only issue numbers, titles, or broad file globs swapped; each child needs a lane-specific task, source seam, prior evidence, and decision that remains distinct without the item number. Launch that fanout as one async `workflowScript` with stable keys and aggregate output unless there is truly only one child.
35
36
  - Prefer fresh-context review/validation fanout, then synthesize and apply fixes in the parent.
36
- - Use async/background only when work can proceed independently; do not poll just to wait. For adaptive gates, branch in `workflowScript`. Approval controls remain available only for already-running durable legacy chains.
37
+ - Use async/background by default when work can proceed independently; do not poll just to wait. For adaptive gates, branch in `workflowScript`. Approval controls remain available only for already-running durable legacy chains.
37
38
  - Preserve capability ceilings, including child tool restrictions and session-scoped allowed-agent restrictions.
38
- - Escalate unresolved product, architecture, or safety decisions upward instead of letting a child decide silently.
39
+ - Escalate unresolved product, architecture, authority, release, merge, or safety decisions upward instead of letting a child decide silently.
40
+ - Treat receipts, CI, review bots, and external-run records as evidence, not authority to merge, close, comment, publish, or release.
39
41
  - As a conservative orchestration policy, do not pass `turnBudget`, a hard `toolBudget`, or a tight `usageBudget` to mutation-capable workers. The default tool budget blocks read/search tools rather than mutation tools, and reported usage has no reservation model. If a worker is interrupted after a tool call starts, checkpoint after the current tool returns with changed files, build/test state, and commit or PR state.
@@ -44,7 +44,7 @@ If config or `PI_SUBAGENT_WAIT_TOOL_ENABLED` disables blocking behavior, direct
44
44
 
45
45
  ### Keep writes single-threaded by default
46
46
 
47
- A strong pattern is one main decision-maker plus advisory/research/review/validation subagents around it. Use `oracle` for advice and `worker` for the actual write path. Parallelize reading, review, validation, and synthesis support, not normal writes, unless you deliberately isolate writers with worktrees. A child that writes should report what changed, what was left undone, commands run with exit codes, validation evidence, surprises, and any decisions that need parent approval.
47
+ A strong pattern is one main decision-maker plus advisory/research/review/validation subagents around it. Use `oracle` for advice and `worker` for the actual write path. Parallelize reading, review, validation, and synthesis support, not normal writes, unless you deliberately isolate writers with worktrees. Across repositories, each repo/worktree still gets at most one writer, with explicit `cwd` and authority in the child prompt. A child that writes should report what changed, what was left undone, commands run with exit codes, validation evidence, surprises, and any decisions that need parent approval.
48
48
 
49
49
  ### Use fork for branched advisory or execution threads
50
50
 
@@ -61,8 +61,7 @@ Give subagents specific tasks rather than vague mandates.
61
61
 
62
62
  ### Escalate decisions upward
63
63
 
64
- If a subagent encounters an unapproved product, architecture, or scope choice,
65
- it should use `contact_supervisor` and wait for the reply instead of deciding alone. Generic `intercom` is a fallback only when the bridge-provided supervisor tool is unavailable.
64
+ If a subagent encounters an unapproved product, architecture, scope, merge, release, credential, or authority choice, it should use `contact_supervisor` and wait for the reply instead of deciding alone. Generic `intercom` is a fallback only when the bridge-provided supervisor tool is unavailable. External checks, receipts, and review bots provide evidence only; they do not grant authority.
66
65
 
67
66
  ### Intervene only on clear control signals
68
67
 
@@ -91,12 +90,12 @@ Fable mode is the default orchestration posture for complex work. It is not a se
91
90
  Run the work through seven gated phases:
92
91
 
93
92
  1. **Understand** — use `scout` or `context-builder` fanout for breadth, but the parent personally reads the load-bearing files and lets direct source reading decide disagreements. Gate: the parent can quote the exact code or behavior being changed and knows the repo's verification harness.
94
- 2. **Decide** — separate user-owned decisions from implementation judgments. Use `interview` for product, naming, cost, taste, or risk decisions; decide routine engineering details in the parent and state them. Gate: every user-owned decision needed for design is answered.
93
+ 2. **Decide** — separate user-owned decisions from implementation judgments. Use `interview` for product, naming, cost, taste, risk, release, merge, or authority decisions; decide routine engineering details in the parent and state them. Gate: every user-owned decision needed for design is answered or recorded as a blocked follow-up.
95
94
  3. **Design** — use `planner`, `context-builder`, or read-only design/review children for parallel perspectives. Before parallel workstreams, write seam contracts: ownership boundaries, composition points, assumptions, and validation handoffs. Gate: one parent-synthesized plan and written seams for parallel work.
96
- 4. **Implement** — capture a baseline first, then launch one async `worker` as the sole writer for the active worktree unless isolated worktrees were intentionally requested. Break large work into serial milestones instead of concurrent writes. Gate: build/typecheck is green and every output or diff delta is characterized as intended or fixed.
95
+ 4. **Implement** — capture a baseline first, then launch one async `worker` as the sole writer for the active worktree unless isolated worktrees were intentionally requested. For cross-codebase work, launch separate async workers only when each has its own repo/worktree, explicit `cwd`, and non-overlapping authority. Break large work into serial milestones instead of concurrent writes. Gate: build/typecheck is green and every output or diff delta is characterized as intended or fixed.
97
96
  5. **Verify** — climb the spend ladder: static checks, free end-to-end/dry-run, cheapest live probe, targeted changed-path live test, then full realistic run when warranted. Observe the artifact itself, not only exit codes or scores, and confirm the changed code actually executed. Gate: the highest necessary rung has directly observed evidence matching intent.
98
97
  6. **Iterate** — when a gate or reviewer finds a defect, the parent names the failure class, searches for siblings, synthesizes fixes, and sends exactly one fix worker for accepted changes. For LLM judges, gates, or detectors, trigger on concrete findings rather than scores, record pass/violations/error verdicts, cache nondeterministic verdicts by input hash, budget enough output tokens, and sanitize judge text before reusing it downstream. Gate: the class is fixed or explicitly bounded, and recurrence detection exists when feasible.
99
- 7. **Ship** — run adversarial fresh-context review/validation outside the implementation path, disposition every finding, rerun affected gates, then have the parent inspect the final diff. Commit, push, release, or open PRs only inside user-approved boundaries. Gate: findings are dispositioned, gates re-pass, and the final summary names evidence, artifacts, residual risks, and output paths.
98
+ 7. **Ship** — run adversarial fresh-context review/validation outside the implementation path, disposition every finding, rerun affected gates, then have the parent inspect the final diff. Commit, push, comment, close, merge, release, or open PRs only inside user-approved boundaries for that repo. Gate: findings are dispositioned, gates re-pass, and the final summary names evidence, artifacts, residual risks, and output paths.
100
99
 
101
100
  ### Clarify → Plan → Implement → Review (self-orchestrated workflow)
102
101
 
@@ -56,7 +56,7 @@ its resolved launch context as `[fresh]` or `[fork]`. Aggregate headers show
56
56
 
57
57
  ### Scripted workflows
58
58
 
59
- `workflowScript` is the sole public orchestration surface. Use `runs.run(key, { agent, task, ... })` for one child, `runs.all([...])` for parallel children, and ordinary JavaScript for sequence, branching, filtering, retries, and aggregation. Prefer a single scripted workflow whenever the parent is starting a coordinated wave, such as multiple reviews, review plus gate monitor, worker then monitor setup, or a fanout that the parent will consume together. Use a direct `{ agent, task }` call only for one isolated child with no sibling work or aggregate handoff.
59
+ `workflowScript` is the sole public orchestration surface. Use `runs.run(key, { agent, task, ... })` for one child, `runs.all([...])` for parallel children, and ordinary JavaScript for sequence, branching, filtering, retries, and aggregation. Prefer a single scripted workflow whenever the parent is starting a coordinated wave, such as multiple reviews, review plus gate monitor, worker then monitor setup, cross-repo prep lanes, or a fanout that the parent will consume together. Use a direct `{ agent, task }` call only for one isolated child with no sibling work or aggregate handoff.
60
60
 
61
61
  ```js
62
62
  subagent({
@@ -93,7 +93,7 @@ subagent({
93
93
  })
94
94
  ```
95
95
 
96
- File-only output mode works for async single runs and workflowScript child launches. Use distinct absolute or durable output paths when later script steps need stable references.
96
+ File-only output mode works for async single runs and workflowScript child launches. Use distinct absolute or durable output paths when later script steps need stable references. For cross-codebase waves, include the repo slug or lane key in each output path so reports from different repositories cannot collide.
97
97
 
98
98
  For review fanout where the parent continues a local audit:
99
99
 
@@ -298,10 +298,11 @@ After compaction, restart, or confusing history, recover from durable state firs
298
298
 
299
299
  Routing rule:
300
300
  - Same project: ordinary mission-backed subagents.
301
- - Different project, small/bounded task: ordinary subagent with explicit `cwd`.
302
- - Different project, substantial or long-running work: open a project-owned Herdr pane rooted there, then give that project Pi session a narrow mission/result contract. Do not model it as ordinary child nesting, and do not expect existing headless runs to move into the pane.
301
+ - Different project, small/bounded task: ordinary async subagent with explicit `cwd`, an authority boundary, and durable output.
302
+ - Several projects with independent work: one async `workflowScript` whose child keys include repo slugs and whose child calls set explicit `cwd`; keep publication and merge decisions serial per repo.
303
+ - Different project, substantial or long-running work: open a project-owned Herdr pane rooted there when a separate visible project session is useful, then give that project Pi session a narrow mission/result contract. Do not model it as ordinary child nesting, and do not expect existing headless runs to move into the pane.
303
304
 
304
- Project panes run a separate Pi session from the target directory. Subagents launched inside that pane use that project's config, agents, skills, files, git state, and mission records. The pane binding lives under `<projectRoot>/.pi-subagents/project-panes/herdr.json`.
305
+ Project panes run a separate Pi session from the target directory. Subagents launched inside that pane use that project's config, agents, skills, files, git state, and mission records. The pane binding lives under `<projectRoot>/.pi-subagents/project-panes/herdr.json`. For ordinary headless delegation to another repo, prefer explicit `cwd` first; reserve project panes for visible or persistent project ownership.
305
306
 
306
307
  ```typescript
307
308
  subagent({ action: "mission.create", mission: { title: "Ship auth refresh", goal: "Implement and validate refresh handling" } })
@@ -50,7 +50,7 @@ Packaged prompt shortcuts are also available for repeatable workflows. Treat the
50
50
 
51
51
  ## Applying Prompt Techniques Without Slash Commands
52
52
 
53
- The prompt templates in `prompts/` encode workflows the parent agent can run on demand. If the user provides a URL, issue, PR, plan, local file, screenshot, or freeform target, treat that target as the primary scope: read or fetch it before launching children, then include it explicitly in every child task. Do not depend on the parent conversation history when the recipe calls for fresh context.
53
+ The prompt templates in `prompts/` encode workflows the parent agent can run on demand. If the user provides a URL, issue, PR, plan, local file, screenshot, or freeform target, treat that target as the primary scope: read or fetch it before launching children, then include it explicitly in every child task. For targets outside the parent cwd, include the exact repository, explicit `cwd`, authority boundary, and expected output path in each child task. Do not depend on the parent conversation history when the recipe calls for fresh context.
54
54
 
55
55
  ### Parallel review technique
56
56
 
@@ -212,12 +212,14 @@ Builtin role agents inherit the current Pi default model unless you override the
212
212
 
213
213
  A strong subagent prompt usually includes:
214
214
  - **Goal**: the concrete outcome the child should produce.
215
+ - **Target**: repository, explicit `cwd`, branch/ref/head, and source seam when the target is not the parent cwd.
216
+ - **Authority boundary**: whether the child may read, edit, commit, push, comment, close, merge, publish, or release. Omit or forbid actions that are not approved.
215
217
  - **Context/evidence**: relevant plan paths, files, diffs, decisions, or user constraints already approved.
216
218
  - **Success criteria**: what must be true before the child can finish.
217
219
  - **Hard constraints**: true invariants only, such as no edits for review-only tasks, one writer thread, child must not run subagents unless it is an explicitly assigned `tools: subagent` fanout child, or escalation for unapproved decisions.
218
220
  - **Validation**: targeted checks to run, or the next-best check when validation is impossible.
219
- - **Output**: the expected summary shape, artifact path, or finding format.
220
- - **Stop rules**: when to ask via `intercom`, when to stop after enough evidence, and when not to keep searching.
221
+ - **Output**: the expected summary shape, artifact path, or finding format. Use repo-qualified durable output paths for cross-codebase waves.
222
+ - **Stop rules**: when to ask via `intercom` or `contact_supervisor`, when to stop after enough evidence, and when not to keep searching.
221
223
 
222
224
  Avoid carrying over old prompt habits that over-specify every step. Use `must`, `always`, and `never` for real invariants; for judgment calls, give decision rules. For example, tell a reviewer to inspect the staged diff directly and report only evidence-backed findings, rather than prescribing every file or command. Tell a researcher the retrieval budget: start with broad targeted searches, fetch only the strongest sources, search again only when a required fact is missing, then stop.
223
225
 
@@ -313,7 +313,7 @@ const SubagentParamsSchema = Type.Object({
313
313
  ],
314
314
  description: "Agent/chain config for create/update. Object or JSON string; presence of steps creates a chain."
315
315
  })),
316
- workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript orchestration. Starts asynchronously by default; pass async:false for a small foreground run. Use await runs.run(key, {agent, task, worktree?}), runs.all([...]), runs.status(id), runs.ref(s), emit(value), console, and return. Set worktree:true at workflow or child level for a separate managed worktree per child; child fields override workflow defaults. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
316
+ workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript orchestration. Starts asynchronously by default; pass async:false for a small foreground run. Use await runs.run(key, {agent, task, worktree?}), runs.all([...]), runs.status(id), runs.ref(s), emit(value), console, and return. Use ordinary JavaScript loops, branches, awaits, and arrays to mix sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree per child; child fields override workflow defaults. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
317
317
  chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "terminal", "milestones", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; background and other-repo workflows stay quieter with terminal/milestone summaries." })),
318
318
  worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives a direct single child or each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })),
319
319
  step: Type.Optional(Type.Unsafe({ ...ChainItem, description: "One chain step for action='append-step' only. Not an execution mode." })),
@@ -90,7 +90,7 @@ function shellQuote(value: string): string {
90
90
  }
91
91
 
92
92
  function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean }): string {
93
- const args = [process.execPath, "--experimental-strip-types", input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop)];
93
+ const args = [process.execPath, input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop)];
94
94
  if (input.index !== undefined) args.push("--index", String(input.index));
95
95
  if (input.missionPath) args.push("--mission-path", input.missionPath);
96
96
  return args.map(shellQuote).join(" ");
@@ -195,7 +195,7 @@ export async function handleHerdrInspectorAction(action: HerdrInspectorAction, p
195
195
  const paneId = extractPaneId(split.data);
196
196
  if (!paneId) return result("Herdr inspector error (PANE_GONE): pane split returned no pane id.", true);
197
197
  const mission = missionForRun(target.asyncDir, deps.cwd, deps.missions, target.runId);
198
- const runnerPath = deps.runnerPath ?? fileURLToPath(new URL("./inspector-runner.ts", import.meta.url));
198
+ const runnerPath = deps.runnerPath ?? fileURLToPath(new URL("../../../inspector-runner.mjs", import.meta.url));
199
199
  const command = inspectorCommand({
200
200
  runnerPath,
201
201
  asyncDir: target.asyncDir,
@@ -101,17 +101,19 @@ export function validateMissionLaunch(value: unknown): MissionLaunchInput {
101
101
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("mission must be an object");
102
102
  const input = value as Record<string, unknown>;
103
103
  for (const key of Object.keys(input)) {
104
- if (key !== "title" && key !== "goal" && key !== "labels") throw new Error(`mission.${key} is unknown`);
104
+ if (key !== "title" && key !== "summary" && key !== "goal" && key !== "labels") throw new Error(`mission.${key} is unknown`);
105
105
  }
106
- if (typeof input.title !== "string" || !input.title.trim()) throw new Error("mission.title must be a non-empty string");
106
+ if (input.title !== undefined && input.summary !== undefined) throw new Error("mission.title and mission.summary cannot both be set");
107
+ const title = input.title ?? input.summary;
108
+ if (typeof title !== "string" || !title.trim()) throw new Error("mission.title or mission.summary must be a non-empty string");
107
109
  if (input.goal !== undefined && (typeof input.goal !== "string" || !input.goal.trim())) throw new Error("mission.goal must be a non-empty string");
108
110
  if (input.labels !== undefined && (!Array.isArray(input.labels) || input.labels.some((label) => typeof label !== "string" || !label.trim()))) {
109
111
  throw new Error("mission.labels must contain only non-empty strings");
110
112
  }
111
113
  return {
112
- title: input.title.trim(),
113
- ...(typeof input.goal === "string" ? { goal: input.goal.trim() } : {}),
114
- ...(Array.isArray(input.labels) ? { labels: input.labels as string[] } : {}),
114
+ title: title.trim(),
115
+ ...(input.goal !== undefined ? { goal: input.goal.trim() } : {}),
116
+ ...(input.labels !== undefined ? { labels: input.labels.map((label) => label.trim()) } : {}),
115
117
  };
116
118
  }
117
119
 
@@ -28,7 +28,7 @@ import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
28
28
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
29
29
  import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
30
30
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
31
- import { resolveEffectiveAcceptance } from "../shared/acceptance.ts";
31
+ import { resolveEffectiveAcceptance, validateAcceptanceInput, validateExecutionAcceptance } from "../shared/acceptance.ts";
32
32
  import {
33
33
  type AcceptanceInput,
34
34
  type AgentContract,
@@ -943,6 +943,15 @@ export function executeAsyncChain(
943
943
  nestedRoute,
944
944
  } = params;
945
945
  const resultMode = params.resultMode ?? "chain";
946
+ const acceptanceErrors = validateExecutionAcceptance({
947
+ chain: chain.map((step) => {
948
+ if (isCheckpointStep(step)) return {};
949
+ if (isParallelStep(step)) return { parallel: step.parallel };
950
+ if (isDynamicParallelStep(step)) return { acceptance: step.acceptance, parallel: step.parallel };
951
+ return { acceptance: step.acceptance };
952
+ }),
953
+ });
954
+ if (acceptanceErrors.length > 0) return formatAsyncStartError(resultMode, acceptanceErrors.join(" "));
946
955
  const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
947
956
  const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
948
957
  const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
@@ -1209,6 +1218,8 @@ export function executeAsyncSingle(
1209
1218
  nestedRoute,
1210
1219
  } = params;
1211
1220
  const task = params.task ?? "";
1221
+ const acceptanceErrors = validateAcceptanceInput(params.acceptance);
1222
+ if (acceptanceErrors.length > 0) return formatAsyncStartError("single", acceptanceErrors.join(" "));
1212
1223
  const externalRunner = agentConfig.runner?.type === "external-cli";
1213
1224
  const permissionRules = resolvePermissionRules(ctx.permissions, agentConfig.permissions);
1214
1225
  if (externalRunner) {
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { renderWidget, widgetRenderKey } from "../../tui/render.ts";
4
+ import { renderWidget, requestWidgetRender, widgetRenderKey } from "../../tui/render.ts";
5
5
  import { formatControlNoticeMessage } from "../shared/subagent-control.ts";
6
6
  import {
7
7
  type AsyncJobState,
@@ -34,6 +34,7 @@ const CONTROL_EVENT_READ_CHUNK_BYTES = 64 * 1024;
34
34
  const MAX_CONTROL_EVENT_LINE_BYTES = 1024 * 1024;
35
35
  const CONTROL_EVENT_SCAN_WINDOW_BYTES = 2 * 1024 * 1024;
36
36
  const MAX_RECENT_FLEET_JOBS = 20;
37
+ const WIDGET_ANIMATION_REFRESH_MS = 500;
37
38
 
38
39
  function rememberFleetJob(state: SubagentState, job: AsyncJobState): void {
39
40
  state.fleetJobs ??= new Map();
@@ -56,9 +57,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
56
57
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
57
58
  const resultsDir = options.resultsDir ?? DIRS.results;
58
59
  const steeringNoticeSeen = new Map<string, number>();
60
+ let lastWidgetAnimationAt = 0;
61
+ const requestStatusRender = (ctx: ExtensionContext) => {
62
+ if (requestWidgetRender()) return;
63
+ (ctx.ui as { requestRender?: () => void }).requestRender?.();
64
+ };
59
65
  const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
60
66
  renderWidget(ctx, options.widgetEnabled === false ? [] : jobs);
61
- ctx.ui.requestRender?.();
67
+ requestStatusRender(ctx);
62
68
  };
63
69
  const rerenderLastWidget = (jobs = Array.from(state.asyncJobs.values())) => {
64
70
  const ctx = state.lastUiContext;
@@ -74,6 +80,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
74
80
  }
75
81
  };
76
82
  const refreshWidget = (ctx: ExtensionContext) => rerenderWidget(ctx);
83
+ const hasRunningWidgetJobs = () => options.widgetEnabled !== false && [...state.asyncJobs.values()].some((job) => job.status === "running");
84
+ const refreshWidgetAnimation = () => {
85
+ if (!hasRunningWidgetJobs()) return;
86
+ const now = Date.now();
87
+ if (now - lastWidgetAnimationAt < WIDGET_ANIMATION_REFRESH_MS) return;
88
+ lastWidgetAnimationAt = now;
89
+ requestWidgetRender();
90
+ };
77
91
  const restoredControlEventCursor = (asyncDir: string) => {
78
92
  try {
79
93
  return fs.statSync(path.join(asyncDir, "events.jsonl")).size;
@@ -400,6 +414,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
400
414
  }
401
415
 
402
416
  if (widgetChanged) rerenderLastWidget();
417
+ else refreshWidgetAnimation();
403
418
  }, pollIntervalMs);
404
419
  state.poller.unref?.();
405
420
  };
@@ -87,7 +87,7 @@ import {
87
87
  shouldEscalateMutatingFailures,
88
88
  summarizeRecentMutatingFailures,
89
89
  } from "../shared/long-running-guard.ts";
90
- import { acceptanceFailureMessage, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
90
+ import { acceptanceFailureMessage, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport, validateAcceptanceInput } from "../shared/acceptance.ts";
91
91
  import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
92
92
  import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
93
93
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
@@ -984,14 +984,14 @@ async function runSingleAttempt(
984
984
  }
985
985
  };
986
986
 
987
- if (controlConfig.enabled) {
987
+ fireUpdate();
988
+ if (controlConfig.enabled || options.onUpdate) {
988
989
  activityTimer = setInterval(() => {
989
- if (processClosed || lifecycleFinished) return;
990
- const now = Date.now();
991
- if (updateActivityState(now)) {
992
- progress.durationMs = now - startTime;
993
- fireUpdate();
990
+ if (processClosed || lifecycleFinished) {
991
+ return;
994
992
  }
993
+ updateActivityState(Date.now());
994
+ fireUpdate();
995
995
  }, 1000);
996
996
  activityTimer.unref?.();
997
997
  }
@@ -1368,6 +1368,18 @@ async function runSyncCompletion(
1368
1368
  ...(options.capabilityCeiling ? { capabilityCeiling: options.capabilityCeiling } : {}),
1369
1369
  }, options.context);
1370
1370
  }
1371
+ const acceptanceErrors = validateAcceptanceInput(options.acceptance);
1372
+ if (acceptanceErrors.length > 0) {
1373
+ return withRunContext({
1374
+ index: options.index ?? 0,
1375
+ agent: agentName,
1376
+ task,
1377
+ exitCode: 1,
1378
+ messages: [],
1379
+ usage: emptyUsage(),
1380
+ error: acceptanceErrors.join(" "),
1381
+ }, options.context);
1382
+ }
1371
1383
  const outputModeValidationError = validateFileOnlyOutputMode(options.outputMode, options.outputPath, `Single run (${agentName})`);
1372
1384
  if (outputModeValidationError) {
1373
1385
  return withRunContext({
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
6
  import { resolveAgentName, type AgentConfig, type AgentScope } from "../../agents/agents.ts";
7
- import { getArtifactsDir, getChainRunsDir } from "../../shared/artifacts.ts";
7
+ import { getArtifactsDir, getChainRunsDir, getProjectArtifactPackagingWarning } from "../../shared/artifacts.ts";
8
8
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
9
9
  import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
10
10
  import { resolveEffectiveThinking, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
@@ -1631,6 +1631,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
1631
1631
  mode: SubagentRunMode;
1632
1632
  details: Details;
1633
1633
  nestedChildren?: NestedRunSummary[];
1634
+ preserveDetailsOutputs?: boolean;
1634
1635
  }): Promise<{ text: string; details: Details } | null> {
1635
1636
  const payload = await emitForegroundResultIntercom({
1636
1637
  pi: input.pi,
@@ -1645,7 +1646,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
1645
1646
  if (!payload) return null;
1646
1647
  return {
1647
1648
  text: formatSubagentResultReceipt({ mode: input.mode, runId: input.runId, payload }),
1648
- details: stripDetailsOutputsForIntercomReceipt(input.details),
1649
+ details: input.preserveDetailsOutputs ? input.details : stripDetailsOutputsForIntercomReceipt(input.details),
1649
1650
  };
1650
1651
  }
1651
1652
 
@@ -3450,6 +3451,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3450
3451
  runId,
3451
3452
  mode: "parallel",
3452
3453
  details,
3454
+ ...(params.workflowParentRunId !== undefined ? { preserveDetailsOutputs: true } : {}),
3453
3455
  ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
3454
3456
  });
3455
3457
  if (intercomReceipt) {
@@ -3810,6 +3812,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3810
3812
  runId,
3811
3813
  mode: "single",
3812
3814
  details,
3815
+ ...(params.workflowParentRunId !== undefined ? { preserveDetailsOutputs: true } : {}),
3813
3816
  ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
3814
3817
  });
3815
3818
  if (intercomReceipt) {
@@ -3866,7 +3869,10 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
3866
3869
  }
3867
3870
 
3868
3871
  function workflowChildResult(key: string, result: AgentToolResult<Details>): WorkflowScriptChildResult {
3869
- const output = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n");
3872
+ const receiptOutput = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n");
3873
+ const output = result.details.results.length === 1 && result.details.results[0]?.finalOutput !== undefined
3874
+ ? result.details.results[0].finalOutput
3875
+ : receiptOutput;
3870
3876
  const artifactPaths = new Set<string>();
3871
3877
  if (result.details.asyncDir) artifactPaths.add(result.details.asyncDir);
3872
3878
  if (result.details.parallelHandoff?.path) artifactPaths.add(result.details.parallelHandoff.path);
@@ -3881,7 +3887,7 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
3881
3887
  ok: result.isError !== true,
3882
3888
  ...(result.details.runId || result.details.asyncId ? { runId: result.details.runId ?? result.details.asyncId } : {}),
3883
3889
  output,
3884
- ...(result.isError === true ? { error: output || "Child run failed." } : {}),
3890
+ ...(result.isError === true ? { error: receiptOutput || output || "Child run failed." } : {}),
3885
3891
  ...(structured.length === 1 ? { structuredOutput: structured[0] } : structured.length > 1 ? { structuredOutput: structured } : {}),
3886
3892
  artifactPaths: [...artifactPaths],
3887
3893
  results: result.details.results,
@@ -4018,6 +4024,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4018
4024
  } {
4019
4025
  const delegatedThinkingOverrides = new WeakMap<object, AgentConfig["thinking"]>();
4020
4026
  const delegatedZeroToolBudgets = new WeakSet<object>();
4027
+ const warnedArtifactPackageDirs = new Set<string>();
4021
4028
  const scheduledOwnerExecutors = new Map<string, ReturnType<typeof createSubagentExecutor>>();
4022
4029
  const execute = async (
4023
4030
  _id: string,
@@ -4902,6 +4909,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4902
4909
  dir: deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
4903
4910
  });
4904
4911
  const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
4912
+ if (artifactConfig.dir === "project" && !warnedArtifactPackageDirs.has(effectiveCwd)) {
4913
+ warnedArtifactPackageDirs.add(effectiveCwd);
4914
+ const warning = getProjectArtifactPackagingWarning(effectiveCwd);
4915
+ if (warning) console.warn(`[pi-subagents] ${warning}`);
4916
+ }
4905
4917
 
4906
4918
  let sessionRoot: string;
4907
4919
  if (effectiveParams.sessionDir) {
@@ -169,7 +169,7 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
169
169
  if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
170
170
  else if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
171
171
  else if (input === "none") errors.push(`${pathLabel} level "none" requires a reason; use { level: "none", reason: "..." }.`);
172
- else if (input === "verified") errors.push(`${pathLabel} level "verified" requires object form with at least one verify command.`);
172
+ else if (input === "verified") errors.push(`${pathLabel} level "verified" requires object form with at least one runtime verify command. Use level "checked" or provide a non-empty acceptance.verify array.`);
173
173
  return errors;
174
174
  }
175
175
  if (!input || typeof input !== "object" || Array.isArray(input)) {
@@ -234,7 +234,7 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
234
234
  errors.push(`${pathLabel}.evidence must be an array. ${ACCEPTANCE_EVIDENCE_HELP}`);
235
235
  }
236
236
  if (value.level === "verified" && (!Array.isArray(value.verify) || value.verify.length === 0)) {
237
- errors.push(`${pathLabel}.verify must contain at least one command when level is verified.`);
237
+ errors.push(`${pathLabel}.verify must contain at least one runtime command when level is verified. Use level "checked" or provide a non-empty acceptance.verify array.`);
238
238
  } else if (value.verify !== undefined && !Array.isArray(value.verify)) {
239
239
  errors.push(`${pathLabel}.verify must be an array.`);
240
240
  }
@@ -38,6 +38,7 @@ interface ServerEntry {
38
38
  exposeResources?: boolean;
39
39
  includeTools?: string[];
40
40
  excludeTools?: string[];
41
+ protocolVersion?: string;
41
42
  directTools?: boolean | string[];
42
43
  }
43
44
 
@@ -284,6 +285,7 @@ export function computeMcpServerHash(definition: ServerEntry): string {
284
285
  exposeResources: definition.exposeResources,
285
286
  includeTools: definition.includeTools,
286
287
  excludeTools: definition.excludeTools,
288
+ protocolVersion: definition.protocolVersion,
287
289
  };
288
290
  return createHash("sha256").update(stableStringify(identity)).digest("hex");
289
291
  }