@maxiaochao/pi-toolkit 0.11.18 → 0.11.20

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 2026-10-14 - v0.11.20
2
+
3
+ - herdr-subagents v0.5.3: allow optional Pi-native tool exposure while inheriting Pi defaults when unset
4
+
5
+ ## 2026-10-14 - v0.11.19
6
+
7
+ - herdr-subagents v0.5.2: stop Pi startup from scanning historical runs or closing recorded Herdr tabs
8
+
1
9
  ## 2026-10-14 - v0.11.18
2
10
 
3
11
  - simplify global worktree isolation guidance
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-toolkit",
3
- "version": "0.11.18",
3
+ "version": "0.11.20",
4
4
  "description": "Personal pi coding agent toolkit: scheduler, /btw, /cache_export, apply_patch, browser automation, and bundled theme/skills.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -107,6 +107,10 @@ There is no fixed drain delay and no transcript parsing. Pi retries are internal
107
107
 
108
108
  Writing the same report again is idempotent. A conflicting replacement is rejected.
109
109
 
110
- ## Cancellation and recovery
110
+ ## Cancellation and shutdown
111
111
 
112
- Caller or parent-session cancellation aborts active waits, marks nonterminal tasks cancelled, and closes the owned Herdr tab. Startup recovery is scoped by recorded parent ownership and retains readers for prior record formats. Recovery never resumes or mutates an already terminal task.
112
+ Caller or parent-session cancellation aborts active waits, marks nonterminal tasks cancelled, and closes the Herdr tab owned by that session. A new Pi session never scans, mutates, resumes, or closes historical runs during startup. If Pi is terminated before its shutdown handlers run, its visible Subagent tabs remain for explicit user inspection or cleanup.
113
+
114
+ ## Tool exposure
115
+
116
+ The extension reads optional `exposure` once before registering its two tools. When omitted, the extension also omits the registration field and delegates to Pi's default. Global or project configuration can select Pi's `model-only` or `direct` exposure explicitly. Changing this registration-time setting requires `/reload` or a Pi restart; the extension does not re-register tools during a session.
@@ -1,3 +1,11 @@
1
+ ## 2026-10-14 - v0.5.3
2
+
3
+ - allow optional Pi-native tool exposure while inheriting Pi defaults when unset
4
+
5
+ ## 2026-10-14 - v0.5.2
6
+
7
+ - stop Pi startup from scanning historical runs or closing recorded Herdr tabs
8
+
1
9
  ## 2026-10-14 - v0.5.1
2
10
 
3
11
  - fix focus-safe run cleanup and compact report output
@@ -36,6 +36,8 @@ The reproducible source recording is retained as [`docs/demo.webm`](docs/demo.we
36
36
 
37
37
  Each task starts a single-task interactive Pi process in its own Herdr pane. Herdr first confirms the interactive Pi is ready, then the prompt is delivered exactly once. Prompt acknowledgement never decides task completion. Panes remain ordinary Herdr panes: they can be focused and inspected while work is running or after it settles. An early-finishing child stays at the Pi UI while sibling tasks continue. After the whole run settles, cleanup closes an unfocused run tab immediately or waits for the user to leave a focused run tab.
38
38
 
39
+ Cleanup belongs to the Pi session that created the run. Starting a new Pi session never scans historical runs or closes their recorded Herdr tabs. If Pi terminates before shutdown cleanup runs, any surviving Subagent tab remains visible for explicit inspection or cleanup.
40
+
39
41
  ## Result protocol
40
42
 
41
43
  The child must call `subagent_report` once with a terminal status:
@@ -87,10 +89,12 @@ The orchestrator is the only writer of `run.json`, `task.json`, and promoted `re
87
89
 
88
90
  `herdr_subagents_control` supports `list`, `status`, `history`, `cancel`, and `cleanup`. Follow-up work is a new task or run; settled tasks are never revived.
89
91
 
92
+ When `exposure` is absent, both tools omit the registration field and inherit Pi's default exposure. Set `"exposure": "model-only"` to keep explicit top-level model calls while excluding Codemode scripts and other nested tools, or `"exposure": "direct"` to request Pi's direct-and-nested behavior explicitly. Exposure is read when the extension loads, so use `/reload` or restart Pi after changing it.
93
+
90
94
  Use `/herdr-subagents focus <task-number>` to focus a running pane. `/herdr-subagents active`, `history`, `agents`, `models`, and `settings` provide compact inspection.
91
95
 
92
96
  ## Personas and settings
93
97
 
94
98
  Bundled personas are `worker`, `explorer`, `analyst`, and `reviewer`. Project personas can be added under `.pi/agents/` using the same frontmatter format as the bundled files. Access is declared as `read` or `write` and drives concurrency safety.
95
99
 
96
- Settings are read from the existing package configuration. `defaultConcurrency`, `maxConcurrency`, and `stalledWarningSeconds` remain supported.
100
+ Settings are read from `~/.pi/agent/herdr-subagents.json` and the project-level `.pi/herdr-subagents.json`, with the project value taking precedence. Optional `exposure` accepts Pi's `model-only` or `direct`; omitting it delegates to Pi. `defaultConcurrency`, `maxConcurrency`, and `stalledWarningSeconds` remain supported.
@@ -16,6 +16,8 @@ It verifies:
16
16
  - waiting through a delayed authoritative report after a prompt transport error while the Pi process is alive;
17
17
  - explicit failure when the process exits without a report;
18
18
  - parent cancellation of running and queued tasks;
19
+ - startup isolation: a new Pi session neither mutates historical records nor closes their recorded Herdr tabs;
20
+ - omitted exposure delegates both tools to Pi's default, while project configuration can consistently select model-only exposure;
19
21
  - immutable terminal task records;
20
22
  - idempotent duplicate reports and rejection of conflicting replacements;
21
23
  - keeping a settled child alive for focus-aware cleanup instead of force-exiting its TUI.
@@ -2,12 +2,11 @@ import { randomUUID } from "node:crypto";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import type { AgentToolResult, ExtensionAPI, ExtensionCommandContext, Theme, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
4
4
  import { Container, Editor, Text } from "@earendil-works/pi-tui";
5
- import { loadSubagentConfiguration } from "../src/config.ts";
5
+ import { loadSubagentConfiguration, type ConfigurationPaths } from "../src/config.ts";
6
6
  import { CliHerdrAutomation, type HerdrAutomation } from "../src/herdr.ts";
7
7
  import { activeRunText } from "../src/monitor.ts";
8
8
  import { historyText, listSubagentHistory } from "../src/history.ts";
9
- import { cancelQueuedRuns, loadQueuedRuns } from "../src/queued-runs.ts";
10
- import { cancelActiveSubagentRuns, reconcileSubagentRuns } from "../src/reconcile.ts";
9
+ import { cancelActiveSubagentRuns } from "../src/reconcile.ts";
11
10
  import { executeHerdrSubagents, executeHerdrSubagentsControl, type HerdrSubagentsBatchDetails, type HerdrSubagentsControlToolParams, type HerdrSubagentsRunParams } from "../src/tool.ts";
12
11
  import { HerdrSubagentsControlParams, HerdrSubagentsParams } from "../src/parameters.ts";
13
12
  import { HERDR_ACTIONS } from "../src/validation.ts";
@@ -63,7 +62,7 @@ function renderToolResult(result: AgentToolResult<unknown>, _options: ToolRender
63
62
  return new Text(content?.type === "text" ? content.text : "(no output)", 0, 0);
64
63
  }
65
64
 
66
- async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fleet: FleetController): Promise<void> {
65
+ async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fleet: FleetController, registeredExposure: { value?: "direct" | "model-only"; source: string }): Promise<void> {
67
66
  const section = args.trim() || "active";
68
67
  if (section === "active") {
69
68
  ctx.ui.notify(fleet.active ? activeRunText(fleet.active) : "No active Subagent run.", "info");
@@ -91,7 +90,7 @@ async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fl
91
90
  if (section === "settings") {
92
91
  const settings = configuration.settings;
93
92
  const values: Record<string, string | number | undefined> = { defaultConcurrency: settings.defaultConcurrency, maxConcurrency: settings.maxConcurrency, stalledWarningSeconds: settings.stalledWarningSeconds, defaultModel: settings.defaultModel, defaultThinking: settings.defaultThinking };
94
- const lines = Object.entries(values).map(([key, value]) => `${key}=${String(value ?? "unset")} (${configuration.settingSources[key] ?? "built-in"})`);
93
+ const lines = [`exposure=${registeredExposure.value ?? "Pi default"} (${registeredExposure.source}; loaded at extension registration)`, ...Object.entries(values).map(([key, value]) => `${key}=${String(value ?? "unset")} (${configuration.settingSources[key] ?? "built-in"})`)];
95
94
  if (configuration.diagnostics.length) lines.push("Diagnostics:", ...configuration.diagnostics);
96
95
  ctx.ui.notify(lines.join("\n"), "info");
97
96
  return;
@@ -101,6 +100,8 @@ async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fl
101
100
 
102
101
  export interface HerdrSubagentsExtensionDependencies {
103
102
  herdr?: HerdrAutomation;
103
+ configurationCwd?: string;
104
+ configurationPaths?: ConfigurationPaths;
104
105
  execute?: typeof executeHerdrSubagents;
105
106
  executeControl?: typeof executeHerdrSubagentsControl;
106
107
  isEditor?: (value: unknown) => boolean;
@@ -108,19 +109,6 @@ export interface HerdrSubagentsExtensionDependencies {
108
109
  fleetWidgetRuntime?: FleetWidgetRuntime;
109
110
  }
110
111
 
111
- async function recoverParentSession(cwd: string, herdr: HerdrAutomation, owner: { sessionId?: string; processId: number }, notify: (message: string) => void): Promise<void> {
112
- try {
113
- const queued = await loadQueuedRuns(cwd, notify);
114
- await cancelQueuedRuns(cwd, queued.map((run) => run.runId), { reason: "Owning parent Pi session is no longer active.", owner, onlyOrphaned: true, onError: notify });
115
- }
116
- catch (error) { notify(`Subagent queue recovery failed: ${error instanceof Error ? error.message : String(error)}`); }
117
- try {
118
- const reconciliation = await reconcileSubagentRuns(cwd, herdr, owner);
119
- for (const message of reconciliation.cleanupErrors) notify(message);
120
- }
121
- catch (error) { notify(`Subagent recovery could not inspect active runs: ${error instanceof Error ? error.message : String(error)}`); }
122
- }
123
-
124
112
  async function shutdownParentSession(options: { cwd: string; herdr: HerdrAutomation; owner: { sessionId?: string; processId: number }; fleet: FleetController; sessionController: AbortController; runControllers: Map<string, AbortController>; inFlight: Set<Promise<unknown>>; notify: (message: string) => void }): Promise<void> {
125
113
  options.sessionController.abort(new Error("Parent Pi session closed."));
126
114
  for (const controller of options.runControllers.values()) controller.abort(new Error("Parent Pi session closed."));
@@ -135,6 +123,9 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
135
123
  const herdr = dependencies.herdr ?? new CliHerdrAutomation();
136
124
  const execute = dependencies.execute ?? executeHerdrSubagents;
137
125
  const executeControl = dependencies.executeControl ?? executeHerdrSubagentsControl;
126
+ const registrationConfiguration = loadSubagentConfiguration(dependencies.configurationCwd ?? process.cwd(), packageAgentsDir, undefined, dependencies.configurationPaths);
127
+ const exposure = registrationConfiguration.settings.exposure;
128
+ const registeredExposure = { value: exposure, source: registrationConfiguration.settingSources.exposure ?? "Pi" };
138
129
  const isEditorComponent = dependencies.isEditor ?? ((value: unknown) => value instanceof Editor);
139
130
  const fleet = new FleetController(herdr, isEditorComponent, dependencies.createFleetWidget, dependencies.fleetWidgetRuntime);
140
131
  const runControllers = new Map<string, AbortController>();
@@ -146,16 +137,16 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
146
137
  pi.on("session_shutdown", async (_event, ctx) => {
147
138
  await shutdownParentSession({ cwd: ctx.cwd, herdr, owner: { sessionId: ownerSessionId, processId: process.pid }, fleet, sessionController, runControllers, inFlight, notify: (message) => ctx.ui.notify(message, "error") });
148
139
  });
149
- pi.on("session_start", async (_event, ctx) => {
140
+ pi.on("session_start", (_event, ctx) => {
150
141
  sessionController.abort(new Error("Parent Pi session changed."));
151
142
  sessionController = new AbortController();
152
143
  ownerSessionId = `${ctx.sessionManager.getSessionId()}:${randomUUID()}`;
153
144
  fleet.bind(ctx.ui, ctx.mode === "tui");
154
- await recoverParentSession(ctx.cwd, herdr, { sessionId: ownerSessionId, processId: process.pid }, (message) => ctx.ui.notify(message, "error"));
155
145
  });
156
146
 
157
147
  pi.registerTool({
158
148
  name: "herdr_subagents",
149
+ ...(exposure ? { exposure } : {}),
159
150
  label: "Herdr Subagents",
160
151
  description: "Run one or more ordered tasks in visible, focusable Herdr Pi panes. concurrency 1 is strict serial execution; higher values form a bounded pool for read-only tasks. Write-capable tasks require concurrency 1. The call returns only after every task has one terminal completed, failed, or cancelled result. Complete results and Pi sessions stay in project-local records.",
161
152
  promptSnippet: "Run focused work in visible Herdr subagents",
@@ -196,6 +187,7 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
196
187
 
197
188
  pi.registerTool({
198
189
  name: "herdr_subagents_control",
190
+ ...(exposure ? { exposure } : {}),
199
191
  label: "Herdr Subagents Control",
200
192
  description: "Inspect Herdr personas, query one run by ID, list run history, cancel active work, or remove an archived run. Use herdr_subagents to start new work.",
201
193
  parameters: HerdrSubagentsControlParams,
@@ -246,7 +238,7 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
246
238
  pi.registerCommand("herdr-subagents", {
247
239
  description: "Inspect effective subagent agents, models, and settings",
248
240
  getArgumentCompletions: (prefix) => ["active", "history", "agents", "models", "settings", "focus "].filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value })),
249
- handler: (args, ctx) => handleHerdrCommand(args, ctx, fleet),
241
+ handler: (args, ctx) => handleHerdrCommand(args, ctx, fleet, registeredExposure),
250
242
  });
251
243
  }
252
244
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-herdr-subagents",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Run inspectable interactive Pi subagent batches through Herdr.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -4,8 +4,10 @@ import { dirname, join } from "node:path";
4
4
  import { discoverPackageAgents, type AgentDefinition } from "./personas.ts";
5
5
 
6
6
  export type PersonaAccess = "read" | "write";
7
+ export type SubagentToolExposure = "direct" | "model-only";
7
8
  export interface PersonaConfig { model?: string; thinking?: string; skills?: string[]; extensions?: string[]; }
8
9
  export interface SubagentConfig {
10
+ exposure?: SubagentToolExposure;
9
11
  defaultConcurrency: number;
10
12
  maxConcurrency: number;
11
13
  stalledWarningSeconds: number;
@@ -32,6 +34,7 @@ export interface SubagentConfiguration {
32
34
  diagnostics: string[];
33
35
  }
34
36
  interface ConfigFile {
37
+ exposure?: string;
35
38
  defaultConcurrency?: number;
36
39
  maxConcurrency?: number;
37
40
  stalledWarningSeconds?: number;
@@ -61,6 +64,10 @@ function readConfig(path: string, source: string, diagnostics: string[]): Config
61
64
 
62
65
  function mergeConfig(base: SubagentConfig, input: ConfigFile, source: string, sources: Record<string, string>, diagnostics: string[]): SubagentConfig {
63
66
  const result: SubagentConfig = { ...base, personas: { ...base.personas } };
67
+ if (input.exposure !== undefined) {
68
+ if (input.exposure === "direct" || input.exposure === "model-only") { result.exposure = input.exposure; sources.exposure = source; }
69
+ else diagnostics.push(`${source} exposure must be 'direct' or 'model-only'`);
70
+ }
64
71
  for (const key of ["defaultConcurrency", "maxConcurrency", "stalledWarningSeconds"] as const) {
65
72
  const value = input[key];
66
73
  if (value === undefined) continue;
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import { runHerdrSubagentsBatch } from "../src/batch-runner.ts";
6
6
  import registerSubagentReport from "../extensions/subagent-report.ts";
7
+ import { registerHerdrSubagents } from "../extensions/herdr-subagents.ts";
7
8
  import { applyTaskStatus } from "../src/records.ts";
8
9
 
9
10
  process.env.HERDR_ENV = "1";
@@ -150,6 +151,50 @@ await assert.rejects(() => run([task("writer", "edit", "write"), task("reader",
150
151
  const terminal = { id: "task", runId: "run", order: 1, status: "completed" };
151
152
  assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are immutable/);
152
153
 
154
+ {
155
+ const lifecycleRoot = join(root, "startup-history-isolation");
156
+ const runDirectory = join(lifecycleRoot, ".pi", "herdr-subagents", "runs", "historical-run");
157
+ const taskDirectory = join(runDirectory, "tasks", "01-task");
158
+ mkdirSync(taskDirectory, { recursive: true });
159
+ const historicalRun = { id: "historical-run", label: "historical", status: "completed", ownerSessionId: "old-session", ownerProcessId: 99999999, tabId: "w1:t-reused", taskIds: ["historical-task"], completedAt: "2026-10-01T00:00:00.000Z" };
160
+ writeFileSync(join(runDirectory, "run.json"), JSON.stringify(historicalRun));
161
+ writeFileSync(join(taskDirectory, "task.json"), JSON.stringify({ id: "historical-task", runId: "historical-run", order: 1, status: "completed", completedAt: historicalRun.completedAt }));
162
+ const events = new Map();
163
+ const registeredTools = new Map();
164
+ const herdrCalls = [];
165
+ registerHerdrSubagents({
166
+ on(name, handler) { events.set(name, handler); },
167
+ registerTool(tool) { registeredTools.set(tool.name, tool); },
168
+ registerCommand() {},
169
+ }, {
170
+ herdr: {
171
+ async isTabFocused(tabId) { herdrCalls.push(["isTabFocused", tabId]); return false; },
172
+ async closeTab(tabId) { herdrCalls.push(["closeTab", tabId]); },
173
+ },
174
+ isEditor: () => false,
175
+ });
176
+ await events.get("session_start")?.({}, {
177
+ cwd: lifecycleRoot,
178
+ mode: "rpc",
179
+ sessionManager: { getSessionId: () => "new-session" },
180
+ ui: { notify() {} },
181
+ });
182
+ assert.deepEqual(herdrCalls, [], "Pi startup must not inspect or close historical Subagent tabs");
183
+ assert.deepEqual(JSON.parse(readFileSync(join(runDirectory, "run.json"), "utf8")), historicalRun, "Pi startup must not mutate historical Subagent records");
184
+ assert.equal(registeredTools.get("herdr_subagents")?.exposure, undefined, "Unconfigured Subagent execution must inherit Pi's exposure default");
185
+ assert.equal(registeredTools.get("herdr_subagents_control")?.exposure, undefined, "Unconfigured Subagent control must inherit Pi's exposure default");
186
+ mkdirSync(join(lifecycleRoot, ".pi"), { recursive: true });
187
+ writeFileSync(join(lifecycleRoot, ".pi", "herdr-subagents.json"), JSON.stringify({ exposure: "model-only" }));
188
+ const configuredTools = new Map();
189
+ registerHerdrSubagents({
190
+ on() {},
191
+ registerTool(tool) { configuredTools.set(tool.name, tool); },
192
+ registerCommand() {},
193
+ }, { herdr: {}, isEditor: () => false, configurationCwd: lifecycleRoot });
194
+ assert.equal(configuredTools.get("herdr_subagents")?.exposure, "model-only", "Project configuration can exclude Subagent execution from Codemode");
195
+ assert.equal(configuredTools.get("herdr_subagents_control")?.exposure, "model-only", "Project configuration applies consistently to both Subagent tools");
196
+ }
197
+
153
198
  {
154
199
  const directory = join(root, "report-idempotency");
155
200
  mkdirSync(directory, { recursive: true });