@maxiaochao/pi-toolkit 0.10.5 → 0.10.6

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,10 @@
1
+ ## 2026-10-09 - v0.10.6
2
+
3
+ - bundle herdr-subagents v0.4.3: add per-run status, neutral analyst persona, and tool-agnostic Agent Team protocol
4
+ - Add per-run Herdr Subagents status queries so background review gates can inspect one run without polling the full history archive.
5
+ - Make Agent Team a tool-agnostic concurrent review protocol, decouple models from rotating methodological roles, and add a neutral read-only analyst persona.
6
+ - Remove Todo from the default toolkit bundle and loadout; users who want its progress UI can install `@maxiaochao/pi-todo` explicitly.
7
+
1
8
  ## 2026-10-08 - v0.10.5
2
9
 
3
10
  - bundle herdr-subagents v0.4.2: add Alt+P and /parent controls to return from child Agents
package/README.md CHANGED
@@ -16,13 +16,21 @@ Update it:
16
16
  pi update npm:@maxiaochao/pi-toolkit
17
17
  ```
18
18
 
19
- The root package embeds the child-package source snapshots listed below. Install the complete toolkit or selected standalone children, not both.
19
+ The root package embeds its enabled child-package source snapshots. Todo is intentionally excluded from the default toolkit and remains available as an opt-in standalone package.
20
+
21
+ Existing toolkit users can pick up the change with:
22
+
23
+ ```bash
24
+ pi update npm:@maxiaochao/pi-toolkit
25
+ ```
26
+
27
+ Restart Pi after updating. The `todo` tool and widget will no longer load. `pi update --extensions` has the same effect when updating all installed packages. Explicitly versioned sources remain pinned and must be changed or reinstalled without the old version suffix.
20
28
 
21
29
  ## Child packages
22
30
 
23
31
  | Package | Capability | Install | Update |
24
32
  |---|---|---|---|
25
- | [pi-todo](packages/todo/README.md) | Branch-local execution plans, guarded advancement, `/todos` history | `pi install npm:@maxiaochao/pi-todo` | `pi update npm:@maxiaochao/pi-todo` |
33
+ | [pi-todo](packages/todo/README.md) | Optional branch-local execution plan UI; not bundled with pi-toolkit | `pi install npm:@maxiaochao/pi-todo` | `pi update npm:@maxiaochao/pi-todo` |
26
34
  | [pi-cache-export](packages/cache-export/README.md) | Tau-aligned interactive cache dashboard for Pi sessions | `pi install npm:@maxiaochao/pi-cache-export` | `pi update npm:@maxiaochao/pi-cache-export` |
27
35
  | [pi-codex-edit](packages/codex-edit/README.md) | Codex-harness `apply_patch` routing for selected GPT models | `pi install npm:@maxiaochao/pi-codex-edit` | `pi update npm:@maxiaochao/pi-codex-edit` |
28
36
  | [pi-herdr-subagents](packages/herdr-subagents/README.md) | Interactive Herdr Pi Agents with bounded concurrency, direct navigation, and recovery | `pi install npm:@maxiaochao/pi-herdr-subagents` | `pi update npm:@maxiaochao/pi-herdr-subagents` |
@@ -30,7 +38,21 @@ The root package embeds the child-package source snapshots listed below. Install
30
38
 
31
39
  Each child README owns its feature description, motivation or upstream alignment, examples, screenshots/demos, configuration, and test documentation.
32
40
 
33
- Multiple standalone children may be installed together. Do not install a child alongside the complete toolkit because that exposes the same resource twice.
41
+ Multiple standalone children may be installed together. `pi-todo` may also be installed alongside the complete toolkit because Todo is no longer bundled. Do not install another bundled child alongside the complete toolkit because that exposes the same resource twice.
42
+
43
+ To keep Todo after updating the toolkit:
44
+
45
+ ```bash
46
+ pi install npm:@maxiaochao/pi-todo
47
+ ```
48
+
49
+ If Todo was already installed separately and should be removed completely:
50
+
51
+ ```bash
52
+ pi remove npm:@maxiaochao/pi-todo
53
+ ```
54
+
55
+ Use `--local` with install or remove when the package declaration belongs to a project's `.pi/settings.json` rather than personal settings.
34
56
 
35
57
  Root and child versions remain independent, but repository release tooling publishes a modified child and a new toolkit snapshot from the same commit. Run `pi update --extensions` to update all installed packages. Explicit version sources such as `npm:@maxiaochao/pi-toolkit@0.9.3` remain pinned.
36
58
 
@@ -64,7 +86,7 @@ bash scripts/install.sh
64
86
  | `html-artifact` | Build self-contained HTML explainers and diagrams |
65
87
  | `pdlog` | Decompress and inspect Pudu `.pdlog` files |
66
88
  | `herdr` | Control Herdr workspaces, tabs, panes, and agents |
67
- | `agent-team` | Run cross-vendor adversarial review through Herdr |
89
+ | `agent-team` | Run tool-agnostic, cross-vendor adversarial review with independent concurrent agents |
68
90
  | `tapd` | Use the TAPD CLI for requirements, defects, tasks, and wikis |
69
91
 
70
92
  Package-specific skills, such as `pi-worktree`, are documented by their child package.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-toolkit",
3
- "version": "0.10.5",
4
- "description": "Personal pi coding agent toolkit: execution plans, /btw, /cache_export and apply_patch extensions, browser automation, and bundled theme/skills.",
3
+ "version": "0.10.6",
4
+ "description": "Personal pi coding agent toolkit: /btw, /cache_export and apply_patch extensions, browser automation, and bundled theme/skills.",
5
5
  "type": "module",
6
6
  "workspaces": [
7
7
  "packages/*"
@@ -12,7 +12,6 @@
12
12
  ],
13
13
  "files": [
14
14
  "extensions",
15
- "packages/todo",
16
15
  "packages/cache-export",
17
16
  "packages/codex-edit",
18
17
  "packages/herdr-subagents",
@@ -30,7 +29,6 @@
30
29
  },
31
30
  "pi": {
32
31
  "extensions": [
33
- "./packages/todo/extensions/todo.ts",
34
32
  "./packages/cache-export/extensions/cache-export.ts",
35
33
  "./extensions/btw/index.ts",
36
34
  "./packages/codex-edit/extensions/codex-edit.ts",
@@ -58,7 +56,7 @@
58
56
  },
59
57
  "scripts": {
60
58
  "test": "node tests/package-layout.mjs && npm --prefix packages/todo test && npm --prefix packages/cache-export test && npm --prefix packages/codex-edit test && npm --prefix packages/herdr-subagents test && npm --prefix packages/worktree test && node --experimental-strip-types extensions/btw/tests/history.mjs",
61
- "load-test": "pi -ne -e ./packages/todo/extensions/todo.ts && pi -ne -e ./packages/cache-export/extensions/cache-export.ts && pi -ne -e ./extensions/btw/index.ts && pi -ne -e ./packages/codex-edit/extensions/codex-edit.ts && pi -ne -e ./packages/herdr-subagents/extensions/herdr-subagents.ts",
59
+ "load-test": "pi -ne -e ./packages/cache-export/extensions/cache-export.ts && pi -ne -e ./extensions/btw/index.ts && pi -ne -e ./packages/codex-edit/extensions/codex-edit.ts && pi -ne -e ./packages/herdr-subagents/extensions/herdr-subagents.ts",
62
60
  "eval:agents": "node evaluations/agents-md/run.mjs",
63
61
  "eval:agents:test": "node evaluations/agents-md/tests/run-tests.mjs",
64
62
  "postinstall": "node scripts/install-agents.mjs"
@@ -1,3 +1,9 @@
1
+ ## 2026-10-09 - v0.4.3
2
+
3
+ - add per-run status, neutral analyst persona, and tool-agnostic Agent Team protocol
4
+ - Add `herdr_subagents_control({ action: "status", runId })` for a compact snapshot of one queued, active, blocked, or settled run without loading the full history archive.
5
+ - Add a neutral read-only `analyst` persona for tasks that assign models and methodological roles dynamically.
6
+
1
7
  ## 2026-10-08 - v0.4.2
2
8
 
3
9
  - add Alt+P and /parent controls to return from child Agents
@@ -66,6 +66,14 @@ herdr_subagents({
66
66
 
67
67
  `tasks` is always a non-empty array: one item is a single task and multiple items form a batch. Set `background: true` for either form to return a stable run ID immediately while the queue continues. One run is dispatched at a time by an extension instance; later runs remain queued and start FIFO. Each background run sends one compact parent notification only when the whole run completes, partially fails, fails, or blocks. Individual successful tasks do not wake the parent model.
68
68
 
69
+ Background work normally relies on that terminal notification. When the user asks for progress or the parent must confirm a review gate before continuing, query only that run's compact snapshot:
70
+
71
+ ```text
72
+ herdr_subagents_control({ action: "status", runId: "<run-id>" })
73
+ ```
74
+
75
+ The snapshot reports the aggregate run state and every task's state, including a blocked question when present. It checks durable run state before queued state, never returns unrelated runs, and does not wait or trigger a parent turn. Do not poll full `history` for progress.
76
+
69
77
  A child missing required information reports `needs-input` with an exact question. The run becomes blocked, keeps its Herdr tab, pane, partial report, and Pi session, and returns control to the parent. Answer through the control tool to continue the original session:
70
78
 
71
79
  ```text
@@ -80,7 +88,7 @@ With an empty editor, press `↓` to enter the list, `↑`/`↓` to choose a tas
80
88
 
81
89
  ## History
82
90
 
83
- Run `/herdr-subagents history` or `herdr_subagents_control({ action: "history" })` to list compact project-local run and task reports after Herdr tabs close. Full results, described documents, artifacts, and persistent Pi sessions remain separate on disk and are not loaded into parent context.
91
+ Run `/herdr-subagents history` or `herdr_subagents_control({ action: "history" })` to list the full compact project-local run and task archive after Herdr tabs close. History does not accept a run ID and is not a status interface. Full results, described documents, artifacts, and persistent Pi sessions remain separate on disk and are not loaded into parent context.
84
92
 
85
93
  Continue a saved conversation in a new Herdr tab with `herdr_subagents_control({ action: "resume", runId: "<run-id>", task: 1, prompt: "Follow up..." })`. Remove only a selected archive with `herdr_subagents_control({ action: "cleanup", runId: "<run-id>" })`. History has no automatic count-based eviction. The extension adds the runtime path to local Git excludes when available (and otherwise uses `.pi/.gitignore`) without overwriting unrelated rules.
86
94
 
@@ -192,6 +200,9 @@ The package includes these personas:
192
200
  | `worker` | write | `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls` | Focused implementation work |
193
201
  | `explorer` | read | `read`, `bash`, `grep`, `find`, `ls` | Fast codebase investigation |
194
202
  | `reviewer` | read | `read`, `bash`, `grep`, `find`, `ls` | Correctness and maintainability review |
203
+ | `analyst` | read | `read`, `bash`, `grep`, `find`, `ls` | Neutral analysis with the method and role supplied by each task |
204
+
205
+ `analyst` deliberately does not bind a model or review method. Use the task-level `model` field when independent tasks need different models, and put roles such as auditor, replicator, or adversary in each task prompt.
195
206
 
196
207
  Persona definitions use Markdown frontmatter followed by the child system prompt. For example, save this as `.pi/herdr-subagents/agents/security-reviewer.md`:
197
208
 
@@ -10,18 +10,19 @@
10
10
 
11
11
  ## Cases
12
12
 
13
- - Resolve package, global, and project personas and validate frontmatter/configuration.
13
+ - Resolve package, global, and project personas and validate frontmatter/configuration, including the neutral read-only `analyst` persona.
14
14
  - Run foreground tasks and FIFO bounded-concurrency batches through an injectable Herdr boundary.
15
15
  - Verify all paths use interactive Agent start/prompt without print mode, shell injection, markers, or pane reuse.
16
- - Force write-capable batches to serial execution while allowing bounded read-only parallelism.
16
+ - Force write-capable batches to serial execution while allowing bounded read-only parallelism, including three concurrent `analyst` tasks with independent task-level models.
17
17
  - Preserve compact reports, full results, Pi sessions, blocked questions, and historical follow-ups.
18
18
  - Recover queued/running work after interruption without retrying post-prompt side effects, including truncated reports and recovery errors.
19
19
  - Verify the split public contracts (`herdr_subagents` with required `tasks`, plus `herdr_subagents_control` with required `action`) and injectable extension wiring, including conditional registration only inside Herdr, Fleet rendering, editor focus ownership, terminal-input passthrough, exact-pane navigation, child `/parent` and `Alt+P` return controls, and stable display while another background run queues.
20
20
  - Verify Fleet divider/row formatting, warning/yellow running indicators, success/green completed indicators, persona and task projections, cumulative active duration across blocking/resume, exact Pi usage aggregation from assistant messages, tool results, standalone usage, compaction, and branch summaries, malformed/truncated JSONL tolerance, queued metric omission, and visible-column bounds at narrow widths.
21
21
  - Verify live token updates are file-event driven and polled on each running one-second tick, elapsed repainting exists only while work runs, and widget disposal closes watchers and timers.
22
22
  - Verify selected active/queued run cancellation, blocked-response live Fleet updates, authoritative stale-field replacement, parent-shutdown cancellation, idempotent missing-tab cleanup, cleanup notifications, atomic state replacement, non-retried uncertain startup operations, and stalled warnings.
23
+ - Verify `status(runId)` isolates one run, projects queue-only state, prefers durable state over a stale queue record, exposes blocked questions, marks terminal states settled, rejects unknown IDs, and leaves full `history` semantics unchanged.
23
24
  - Verify resumed batches retain newly allocated pane IDs and historical follow-ups exclude simultaneous session writers.
24
25
 
25
26
  ## Latest baseline
26
27
 
27
- All 169 deterministic scheduler, recovery, process-boundary, schema-validation, navigation, metrics, responsive-formatting, and persona cases must pass. Release validation also requires extension loading and a real Herdr read-only batch smoke with distinct task panes and final tab cleanup. The latest smoke completed two concurrent tasks, retained its focused run tab through settlement, and closed it automatically after focus moved away.
28
+ All 179 deterministic scheduler, recovery, process-boundary, schema-validation, navigation, metrics, responsive-formatting, and persona cases must pass. Release validation also requires extension loading and a real Herdr read-only batch smoke with distinct task panes and final tab cleanup. The latest smoke completed two concurrent tasks, retained its focused run tab through settlement, and closed it automatically after focus moved away.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: analyst
3
+ description: Neutral read-only analyst for dynamically assigned roles
4
+ access: read
5
+ tools: read, bash, grep, find, ls
6
+ ---
7
+
8
+ Act as an independent analyst and follow the role and evaluation method in the task prompt. Do not modify project files. Separate direct evidence, inference, and unknowns, and report concrete evidence with useful paths.
@@ -259,7 +259,7 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
259
259
  pi.registerTool({
260
260
  name: "herdr_subagents_control",
261
261
  label: "Herdr Subagents Control",
262
- description: "Inspect Herdr personas and run history, cancel active or queued work, answer a blocked task, resume a saved task, or remove a selected archived run. Use herdr_subagents to start new work.",
262
+ description: "Inspect Herdr personas, query one run by ID, list run history, cancel active or queued work, answer a blocked task, resume a saved task, or remove a selected archived run. Use status for one run and history only for the full archive. Use herdr_subagents to start new work.",
263
263
  promptSnippet: "Inspect and control Herdr subagent runs",
264
264
  parameters: HerdrSubagentsControlParams,
265
265
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-herdr-subagents",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Run inspectable interactive Pi subagent batches through Herdr.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -16,8 +16,8 @@ export const HerdrSubagentsParams = Type.Object({
16
16
  }, { additionalProperties: false });
17
17
 
18
18
  export const HerdrSubagentsControlParams = Type.Object({
19
- action: StringEnum(["list", "respond", "history", "resume", "cancel", "cleanup"] as const, { description: "Control operation to perform." }),
20
- runId: Type.Optional(Type.String({ minLength: 1, description: "Durable run ID used by respond, resume, cancel, or cleanup." })),
19
+ action: StringEnum(["list", "status", "respond", "history", "resume", "cancel", "cleanup"] as const, { description: "Control operation to perform." }),
20
+ runId: Type.Optional(Type.String({ minLength: 1, description: "Durable run ID used by status, respond, resume, cancel, or cleanup." })),
21
21
  answer: Type.Optional(Type.String({ minLength: 1, description: "Answer sent to a blocked task when action is respond." })),
22
22
  task: Type.Optional(Type.Integer({ minimum: 1, description: "One-based task number selected for respond or resume." })),
23
23
  prompt: Type.Optional(Type.String({ minLength: 1, description: "Follow-up prompt used by resume." })),
@@ -0,0 +1,95 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { loadQueuedRuns } from "./queued-runs.ts";
4
+ import { isSettledRunStatus, readReport, readRunRecord, readTaskRecord, type RunStatus, type TaskStatus } from "./records.ts";
5
+ import { findRunDirectory } from "./run-locator.ts";
6
+
7
+ export interface RunStatusTask {
8
+ order: number;
9
+ name: string;
10
+ agent: string;
11
+ status: TaskStatus;
12
+ summary?: string;
13
+ question?: string;
14
+ error?: string;
15
+ }
16
+
17
+ export interface RunStatusSnapshot {
18
+ runId: string;
19
+ label: string;
20
+ status: RunStatus;
21
+ settled: boolean;
22
+ source: "run" | "queue";
23
+ queuedAt?: string;
24
+ startedAt?: string;
25
+ updatedAt?: string;
26
+ completedAt?: string;
27
+ question?: string;
28
+ error?: string;
29
+ tasks: RunStatusTask[];
30
+ }
31
+
32
+ async function loadDurableStatus(cwd: string, runId: string): Promise<RunStatusSnapshot | undefined> {
33
+ const runsDirectory = join(cwd, ".pi", "herdr-subagents", "runs");
34
+ const runDirectory = await findRunDirectory(runsDirectory, runId);
35
+ if (!runDirectory) return undefined;
36
+ const run = await readRunRecord(join(runDirectory, "run.json"));
37
+ const tasksDirectory = join(runDirectory, "tasks");
38
+ const tasks: RunStatusTask[] = [];
39
+ if (existsSync(tasksDirectory)) for (const entry of readdirSync(tasksDirectory).sort()) {
40
+ const taskDirectory = join(tasksDirectory, entry);
41
+ const task = await readTaskRecord(join(taskDirectory, "task.json"));
42
+ const reportFile = join(taskDirectory, "report.json");
43
+ const reportIsCurrent = task.status === "blocked" || isSettledRunStatus(task.status);
44
+ const report = reportIsCurrent && existsSync(reportFile) ? await readReport(reportFile) : undefined;
45
+ const question = report?.question ?? task.question;
46
+ const error = report?.error ?? task.error;
47
+ tasks.push({
48
+ order: task.order,
49
+ name: task.name ?? task.agent ?? `Task ${task.order}`,
50
+ agent: task.agent ?? "worker",
51
+ status: task.status,
52
+ ...(report?.summary ? { summary: report.summary } : {}),
53
+ ...(question ? { question } : {}),
54
+ ...(error ? { error } : {}),
55
+ });
56
+ }
57
+ return {
58
+ runId: run.id,
59
+ label: run.label,
60
+ status: run.status,
61
+ settled: isSettledRunStatus(run.status),
62
+ source: "run",
63
+ ...(run.startedAt ? { startedAt: run.startedAt } : {}),
64
+ ...(run.updatedAt ? { updatedAt: run.updatedAt } : {}),
65
+ ...(run.completedAt ? { completedAt: run.completedAt } : {}),
66
+ ...(run.question ? { question: run.question } : {}),
67
+ ...(run.error ? { error: run.error } : {}),
68
+ tasks: tasks.sort((left, right) => left.order - right.order),
69
+ };
70
+ }
71
+
72
+ export async function getSubagentRunStatus(cwd: string, runId: string): Promise<RunStatusSnapshot | undefined> {
73
+ const durable = await loadDurableStatus(cwd, runId);
74
+ if (durable) return durable;
75
+ const queued = (await loadQueuedRuns(cwd)).find((candidate) => candidate.runId === runId);
76
+ if (!queued) return undefined;
77
+ return {
78
+ runId: queued.runId,
79
+ label: queued.params.label ?? "batch",
80
+ status: "queued",
81
+ settled: false,
82
+ source: "queue",
83
+ queuedAt: queued.queuedAt,
84
+ tasks: queued.params.tasks.map((task, index) => ({ order: index + 1, name: task.name, agent: task.agent ?? "worker", status: "queued" })),
85
+ };
86
+ }
87
+
88
+ export function runStatusText(status: RunStatusSnapshot): string {
89
+ const lines = [`${status.runId} · ${status.label} · ${status.status} · ${status.tasks.length} tasks`];
90
+ for (const task of status.tasks) {
91
+ const detail = task.question ? ` — ${task.question}` : task.summary ? ` — ${task.summary}` : task.error ? ` — ${task.error}` : "";
92
+ lines.push(` ${task.order}. ${task.name}: ${task.status}${detail}`);
93
+ }
94
+ return lines.join("\n");
95
+ }
@@ -8,6 +8,7 @@ import { HERDR_ACTIONS, validateControlParams, validateRunParams } from "./valid
8
8
  import type { OwnerIdentity } from "./ownership.ts";
9
9
  import { cancelActiveSubagentRuns } from "./reconcile.ts";
10
10
  import { cancelQueuedRuns, loadQueuedRuns } from "./queued-runs.ts";
11
+ import { getSubagentRunStatus, runStatusText } from "./status.ts";
11
12
 
12
13
  export interface HerdrSubagentsTaskParams { name: string; prompt: string; agent?: string; model?: string; }
13
14
 
@@ -19,7 +20,7 @@ export interface HerdrSubagentsRunParams {
19
20
  }
20
21
 
21
22
  export interface HerdrSubagentsControlToolParams {
22
- action: "list" | "respond" | "history" | "resume" | "cancel" | "cleanup";
23
+ action: "list" | "status" | "respond" | "history" | "resume" | "cancel" | "cleanup";
23
24
  runId?: string;
24
25
  answer?: string;
25
26
  task?: number;
@@ -28,7 +29,7 @@ export interface HerdrSubagentsControlToolParams {
28
29
 
29
30
  /** Internal superset used by shared execution helpers. */
30
31
  export interface HerdrSubagentsToolParams {
31
- action?: "list" | "respond" | "history" | "resume" | "cancel" | "cleanup";
32
+ action?: "list" | "status" | "respond" | "history" | "resume" | "cancel" | "cleanup";
32
33
  prompt?: string;
33
34
  agent?: string;
34
35
  label?: string;
@@ -114,6 +115,16 @@ const errorResponse = (text: string, details: Record<string, unknown> = {}): Too
114
115
  });
115
116
 
116
117
  const actionHandlers = {
118
+ async status({ params, ctx }: ToolExecutionContext) {
119
+ try {
120
+ const runId = (params.runId as string).trim();
121
+ const status = await getSubagentRunStatus(ctx.cwd, runId);
122
+ if (!status) return errorResponse(`Unknown Subagent run '${runId}'.`, { action: "status", runId });
123
+ return { content: [{ type: "text", text: runStatusText(status) }], details: { action: "status", ...status } };
124
+ } catch (error) {
125
+ return errorResponse(error instanceof Error ? error.message : String(error), { action: "status", runId: params.runId });
126
+ }
127
+ },
117
128
  async history({ ctx }: ToolExecutionContext) {
118
129
  const history = await listSubagentHistory(ctx.cwd);
119
130
  return { content: [{ type: "text", text: historyText(history) }], details: { action: "history", runs: history } };
@@ -3,6 +3,7 @@ import type { HerdrSubagentsControlToolParams, HerdrSubagentsRunParams } from ".
3
3
  export const HERDR_ACTIONS = {
4
4
  list: { allowed: ["action"], includeRunId: false },
5
5
  history: { allowed: ["action"], includeRunId: false },
6
+ status: { allowed: ["action", "runId"], includeRunId: true },
6
7
  cancel: { allowed: ["action", "runId"], includeRunId: true },
7
8
  cleanup: { allowed: ["action", "runId"], includeRunId: true },
8
9
  respond: { allowed: ["action", "runId", "answer", "task"], includeRunId: true },
@@ -19,7 +20,7 @@ export function validateControlParams(params: HerdrSubagentsControlToolParams):
19
20
  if (!allowed) return "Provide a valid control action.";
20
21
  const extra = Object.keys(params).find((key) => !allowed.includes(key as keyof HerdrSubagentsControlToolParams));
21
22
  if (extra) return `Field '${extra}' is not valid with action '${params.action}'.`;
22
- if ((params.action === "cancel" || params.action === "cleanup") && !params.runId?.trim()) return `runId is required for action '${params.action}'.`;
23
+ if ((params.action === "status" || params.action === "cancel" || params.action === "cleanup") && !params.runId?.trim()) return `runId is required for action '${params.action}'.`;
23
24
  if (params.action === "respond" && (!params.runId?.trim() || !params.answer?.trim())) return "runId and answer are required for action 'respond'.";
24
25
  if (params.action === "resume" && (!params.runId?.trim() || !params.task || !params.prompt?.trim())) return "runId, task, and prompt are required for action 'resume'.";
25
26
  return undefined;
@@ -251,6 +251,10 @@ export default function (_pi: ExtensionAPI) {
251
251
  if (!controlSchema.properties?.[name]?.description) throw new Error(`control schema property '${name}' is missing or undocumented`);
252
252
  }
253
253
  if (!controlSchema.required?.includes("action")) throw new Error("herdr_subagents_control action must be required");
254
+ const controlActions = controlSchema.properties.action.enum ?? controlSchema.properties.action.anyOf?.map((entry: Record<string, unknown>) => entry.const);
255
+ if (!controlActions?.includes("status")) throw new Error("herdr_subagents_control action enum must include status");
256
+ if (!/status/.test(controlSchema.properties.runId.description)) throw new Error("control runId description must document status");
257
+ if (!/Use status for one run/.test(controlTool.description)) throw new Error("control tool description must distinguish status from history");
254
258
  if (!/Foreground calls block/.test(tool.description) || !/background calls return a run ID immediately/.test(tool.description)) {
255
259
  throw new Error("tool description does not distinguish foreground and background execution");
256
260
  }
@@ -141,14 +141,16 @@ function latestRunDirectory() {
141
141
  try {
142
142
  check("run validation accepts one or more tasks", validateRunParams({ tasks: [{ name: "one", prompt: "do it" }], concurrency: 2, background: true }) === undefined);
143
143
  check("control validation accepts supported calls", [
144
- { action: "list" }, { action: "history" }, { action: "cancel", runId: "run" }, { action: "cleanup", runId: "run" },
144
+ { action: "list" }, { action: "history" }, { action: "status", runId: "run" }, { action: "cancel", runId: "run" }, { action: "cleanup", runId: "run" },
145
145
  { action: "respond", runId: "run", answer: "yes", task: 1 },
146
146
  { action: "resume", runId: "run", task: 1, prompt: "continue" },
147
147
  ].every((params) => validateControlParams(params) === undefined));
148
- check("all supported actions have shared validation and rendering metadata", ["list", "history", "cancel", "cleanup", "respond", "resume"].every((action) => action in HERDR_ACTIONS));
148
+ check("all supported actions have shared validation and rendering metadata", ["list", "history", "status", "cancel", "cleanup", "respond", "resume"].every((action) => action in HERDR_ACTIONS));
149
149
  check("split validation rejects missing tasks and invalid control fields", [
150
150
  validateRunParams({ tasks: [] }),
151
151
  validateControlParams({ action: "list", prompt: "not allowed" }),
152
+ validateControlParams({ action: "status" }),
153
+ validateControlParams({ action: "history", runId: "not-allowed" }),
152
154
  validateControlParams({ action: "respond", runId: "run" }),
153
155
  validateControlParams({ action: "resume", runId: "run", task: 1 }),
154
156
  ].every((message) => typeof message === "string" && message.length > 0));
@@ -404,6 +406,9 @@ try {
404
406
  writeFileSync(join(agentsDir, "broken.md"), "not frontmatter");
405
407
  const discovery = discoverPackageAgents(agentsDir);
406
408
  check("persona discovery and diagnostics", discovery.agents.length === 1 && discovery.agents[0].name === "worker" && discovery.diagnostics.length === 1);
409
+ const packageDiscovery = discoverPackageAgents(join(process.cwd(), "agents"));
410
+ const analystPersona = packageDiscovery.agents.find((persona) => persona.name === "analyst");
411
+ check("built-in analyst is neutral and read-only", packageDiscovery.diagnostics.length === 0 && analystPersona?.access === "read" && analystPersona.model === undefined && analystPersona.tools?.join(",") === "read,bash,grep,find,ls" && /independent analyst/.test(analystPersona.systemPrompt) && /direct evidence, inference, and unknowns/.test(analystPersona.systemPrompt) && !/auditor|replicator|adversary/.test(analystPersona.systemPrompt));
407
412
 
408
413
  const configRoot = join(root, "configuration");
409
414
  const packageAgents = join(configRoot, "package-agents");
@@ -443,6 +448,18 @@ try {
443
448
  const configuredStart = configuredHerdr.calls.find(([name]) => name === "startAgent")[1];
444
449
  check("configured persona uses displayed effective values", !configuredRun.isError && configuredStart.args.includes("task/model") && configuredStart.args.includes("high") && configuredStart.args.includes("--skill") && configuredStart.args.includes("project-skill"));
445
450
 
451
+ const analystHerdr = fakeHerdr();
452
+ const analystBatch = await executeHerdrSubagents({ label: "independent-analysis", concurrency: 3, tasks: [
453
+ { name: "sol-audit", prompt: "slow audit", agent: "analyst", model: "vendor/sol" },
454
+ { name: "grok-replicate", prompt: "slow replicate", agent: "analyst", model: "vendor/grok" },
455
+ { name: "opus-challenge", prompt: "slow challenge", agent: "analyst", model: "vendor/opus" },
456
+ ] }, undefined, undefined, { cwd: root, model: { provider: "parent", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: analystHerdr });
457
+ const analystStarts = analystHerdr.calls.filter(([name]) => name === "startAgent").map(([, request]) => request);
458
+ const analystModels = analystStarts.map((request) => request.args[request.args.indexOf("--model") + 1]);
459
+ check("three read-only analyst tasks run concurrently", !analystBatch.isError && analystBatch.details.effectiveConcurrency === 3 && analystHerdr.maxActive === 3);
460
+ check("analyst tasks preserve independent model overrides", analystModels.join(",") === "vendor/sol,vendor/grok,vendor/opus");
461
+ check("completed analyst batch closes its run tab", analystHerdr.calls.at(-1)?.[0] === "closeTab");
462
+
446
463
  const batchHerdr = fakeHerdr();
447
464
  const batchUpdates = [];
448
465
  const batch = await executeHerdrSubagents({ label: "reviews", concurrency: 2, tasks: [
@@ -603,10 +620,31 @@ try {
603
620
  const stableHerdr = fakeHerdr();
604
621
  const stable = await executeHerdrSubagents({ tasks: [{ name: "stable", prompt: "stable", agent: "explorer" }] }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: stableHerdr, runId: backgroundId });
605
622
  check("preallocated background run id stays stable", stable.details.runId === backgroundId && readFileSync(join(stable.details.recordDirectory, "run.json"), "utf8").includes(backgroundId));
623
+ const stableStatus = await executeHerdrSubagents({ action: "status", runId: backgroundId }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents") });
624
+ check("status returns only one durable run with task details", !stableStatus.isError && stableStatus.details.runId === backgroundId && stableStatus.details.status === "completed" && stableStatus.details.settled === true && stableStatus.details.source === "run" && stableStatus.details.tasks[0].name === "stable" && !stableStatus.content[0].text.includes("run-one"));
625
+ await persistQueuedRun(root, backgroundId, { label: "stale-queue", tasks: [{ name: "wrong-source", prompt: "queued" }], background: true });
626
+ const durableFirstStatus = await executeHerdrSubagents({ action: "status", runId: backgroundId }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents") });
627
+ check("status prefers durable state over a stale queue record", durableFirstStatus.details.source === "run" && durableFirstStatus.details.label !== "stale-queue" && durableFirstStatus.details.tasks[0].name === "stable");
628
+ const queuedStatusId = "queued-status-id";
629
+ await persistQueuedRun(root, queuedStatusId, { label: "queued-status", tasks: [{ name: "waiting", prompt: "wait", agent: "reviewer" }], background: true });
630
+ const queuedStatus = await executeHerdrSubagents({ action: "status", runId: queuedStatusId }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents") });
631
+ check("status projects a queued run before durable records exist", !queuedStatus.isError && queuedStatus.details.source === "queue" && queuedStatus.details.status === "queued" && queuedStatus.details.settled === false && queuedStatus.details.tasks[0].status === "queued" && queuedStatus.details.tasks[0].agent === "reviewer");
632
+ const unknownStatus = await executeHerdrSubagents({ action: "status", runId: "unknown-status-id" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents") });
633
+ check("status rejects unknown run IDs without falling back to history", unknownStatus.isError && unknownStatus.content[0].text === "Unknown Subagent run 'unknown-status-id'.");
634
+ const staleStatusRoot = join(root, "stale-status-report");
635
+ const staleStatusTaskDirectory = join(staleStatusRoot, ".pi", "herdr-subagents", "runs", "stale", "tasks", "01-task");
636
+ mkdirSync(staleStatusTaskDirectory, { recursive: true });
637
+ writeFileSync(join(staleStatusTaskDirectory, "..", "..", "run.json"), JSON.stringify({ id: "stale-status", label: "stale-status", status: "running", startedAt: "2026-10-08T00:00:00.000Z" }));
638
+ writeFileSync(join(staleStatusTaskDirectory, "task.json"), JSON.stringify({ id: "stale-task", runId: "stale-status", order: 1, name: "stale-task", agent: "reviewer", status: "running" }));
639
+ writeFileSync(join(staleStatusTaskDirectory, "report.json"), JSON.stringify({ status: "needs-input", summary: "Old blocked report.", question: "Old question?", documents: [] }));
640
+ const runningStatus = await executeHerdrSubagents({ action: "status", runId: "stale-status" }, undefined, undefined, { cwd: staleStatusRoot }, { agentsDirectory: join(process.cwd(), "agents") });
641
+ check("running status does not expose a stale report from an earlier blocked turn", runningStatus.details.tasks[0].status === "running" && !runningStatus.details.tasks[0].question && !runningStatus.details.tasks[0].summary && !runningStatus.content[0].text.includes("Old question"));
606
642
 
607
643
  const blockedHerdr = fakeHerdr("blocked");
608
644
  const blocked = await executeHerdrSubagents({ concurrency: 1, tasks: [{ name: "question", prompt: "inspect branch", agent: "explorer" }, { name: "after-answer", prompt: "continue queued", agent: "reviewer" }] }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
609
645
  check("needs-input returns foreground control and preserves tab", blocked.details.status === "blocked" && blocked.details.tasks[0].question === "Which branch should I inspect?" && !blockedHerdr.calls.some(([name]) => name === "closeTab"));
646
+ const blockedStatus = await executeHerdrSubagents({ action: "status", runId: blocked.details.runId }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents") });
647
+ check("status exposes blocked task questions for respond", blockedStatus.details.status === "blocked" && blockedStatus.details.settled === false && blockedStatus.details.tasks[0].question === "Which branch should I inspect?" && blockedStatus.content[0].text.includes("Which branch should I inspect?"));
610
648
  const activeCleanup = await executeHerdrSubagents({ action: "cleanup", runId: blocked.details.runId }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
611
649
  check("cleanup refuses to delete an active blocked run", activeCleanup.isError && activeCleanup.content[0].text.includes("still active") && existsSync(blocked.details.recordDirectory));
612
650
  const resumeUpdates = [];
@@ -4,86 +4,62 @@ description: 以对抗评审的方式让不同厂商的模型交叉验证同一
4
4
  disable-model-invocation: true
5
5
  ---
6
6
 
7
- 当前会话是**管理者**,不是转述人:自己也承担一份实证工作,并对每一轮的裁决负责。队员是长驻的 herdr pane,不是一次性 subagent —— 它们要能被反复追问、被点名互相反驳。
7
+ 当前会话是**管理者**,不是转述人:自己也承担一份实证工作,并对每一轮的裁决负责。每轮并发启动三个相互独立的子代理,统一收集结果后由管理者裁决。子代理不需要常驻;跨轮连续性来自持久调查档案和逻辑成员身份,而不是聊天记忆。
8
8
 
9
9
  ## 何时值得开
10
10
 
11
- 只在用户要求组队时开,它会在用户的终端里真的分屏。值得开的条件:结论会驱动一个代价高的动作(改内核、下线设备、向客户定责),或者证据链已经长到单模型会自我确认。查一个事实、读一份文件用不上。
11
+ 只在用户要求组队时开。值得开的条件:结论会驱动一个代价高的动作(改内核、下线设备、向客户定责),或者证据链已经长到单模型容易自我确认。查一个事实、读一份文件用不上。
12
12
 
13
- ## 1. 建队
13
+ ## 1. 建立调查档案
14
14
 
15
- **必须跨厂商。** 同厂同代模型共享盲区,三个一起点头等于一个点头。
15
+ 为当前问题建立 `.pi/agent-team/<case>/`:
16
16
 
17
- 名单在同目录的 `models.json`,先读它,照里面的 `model` 字段写命令;用户指定了别的模型就用用户的。每个成员有 `name` / `model` / `role` / `charter`,`defaults` 里是共用的 `kind` 与 `thinking`。
18
-
19
- ```bash
20
- herdr pane split --current --direction right --cwd "$PWD" --no-focus
21
- herdr agent start <name> --kind pi --pane <pane> -- --model <model> --thinking <thinking>
22
- echo <pane> > /tmp/pane_<name>
17
+ ```text
18
+ .pi/agent-team/<case>/
19
+ ├── BRIEF.md # 问题、范围、术语、输入和排除项
20
+ ├── FINDINGS-stageN.md # 当前唯一事实源;新阶段整体取代旧阶段
21
+ ├── LEDGER.md # 只增不改的 N/X 记录
22
+ └── rounds/
23
+ └── N.md # 本轮角色映射和可证伪攻击点
23
24
  ```
24
25
 
25
- pane id 落盘,后面所有广播脚本都从这里取。开完把该成员的 `charter` 作为第一条 prompt 发过去,立常设职责。`herdr agent list` 确认都是 idle 再开工。
26
+ `BRIEF.md` 只记录稳定背景。`FINDINGS-stageN.md` 必含机制链、编号发现、已推翻清单和未闭合点。`LEDGER.md` 中发现按 `N1, N2…` 追加,被推翻的结论按 `X1, X2…` 追加并写明推翻者。只有管理者能更新这三个权威文件;子代理只提交候选证据。
26
27
 
27
- ## 2. 分角色:互不重叠的攻击面
28
+ ## 2. 配置成员与角色
28
29
 
29
- 每个 pane 领一个**方法论上互斥**的职责:`auditor` 审源码、`replicator` 复算数据、`adversary` 攻方法学。具体措辞是 `models.json` 里各自的 `charter`,改职责改那里。
30
+ 名单在同目录的 `models.json`。三个逻辑成员必须来自不同模型厂商,并显式使用各自的 `model`;不能依赖父会话或环境默认模型。启动后核对并在本轮记录中写下每个任务实际生效的模型;无法确认、与名单不符或三个成员落到同一厂商时,本轮无效。
30
31
 
31
- ## 3. 交底与通信
32
+ 三个成员使用相同的中性只读执行约束,方法角色由每轮任务动态赋予:
32
33
 
33
- herdr 没有邮箱,只有 `agent prompt`(往别人输入框塞文字)和 `agent read`(把别人屏幕刮下来)。拓扑是星型,管理者是唯一的路由:跨 pane 的话全部由管理者转述,把 A 的原话引进 B 的下一条 prompt(「opus 说你这个计数可能一对多,用数据回应」)。
34
+ - `auditor`:审源码、规范和提交,只接受可定位的直接证据。
35
+ - `replicator`:绕开管理者的脚本和口径,从原始数据独立复算。
36
+ - `adversary`:攻击前提和方法学,查样本敏感性、一对多计数和阴性对照。
34
37
 
35
- ```
36
- 管理者(本会话)
37
- 唯一写 FINDINGS 的人
38
- │ │ │
39
- 下行:消息 │ │ │ herdr agent prompt
40
- (短指针) ▼ ▼ ▼
41
- ┌────┐ ┌──────┐ ┌──────┐
42
- │ sol│ │ grok │ │ opus │ 彼此不相连
43
- └──┬──┘ └──┬───┘ └──┬───┘
44
- │ │ │
45
- 上行:文件 └───────┴───────┘
46
- ▼
47
- /tmp/<轮次>-<name>.md ← 管理者从磁盘读
48
- ```
38
+ 模型与角色不得永久绑定。首轮可以按 `models.json` 顺序分配三个角色;后续根据争议轮换或交叉复核,使模型差异和角色差异能够被区分。每轮在 `rounds/N.md` 写明成员、模型、角色、输入文件和攻击点。
49
39
 
50
- 技术上任何 pane 都能 prompt 任何 pane,让它们直连有四个代价:管理者看不见这次交换,ledger 就缺一环;对正在 working 的 agent 发 prompt 会被拒(`agent_blocked`)或插进它当前那一轮;没有投递回执;而且两两串供会让它们彼此收敛,恰好毁掉跨厂商独立性这唯一的价值来源。
40
+ ## 3. 交底与通信
51
41
 
52
- 下行只发短指针,正文留在文件里:长文本经终端会被折断或触发意外快捷键。上行走文件:队员把结论写 `/tmp/<轮次>-<name>.md`,终端只回一行路径,管理者从磁盘读。`agent read` 是终端截图,会截断、会按宽度折行、会混进 TUI 边框,拿它收长输出必丢内容;只用它确认回执和状态。
42
+ 拓扑是星型,管理者是唯一消息路由。成员在同一轮中独立作答,在提交前不得看到其他成员本轮答案。需要逻辑成员 A 反驳逻辑成员 B 的上一轮结论时,由管理者在新任务中引用 B 的原话和证据位置;不能假定新实例记得上一轮聊天。
53
43
 
54
- 基准文档 `FINDINGS-stageN.md` 是**唯一事实源**,只有管理者写,新版本整体取代旧版本而不是打补丁。必含四块:机制链、编号发现、已推翻清单、未闭合点。
55
-
56
- ```bash
57
- MSG='【N17】读 /tmp/FINDINGS-stage4.md 全文。本轮你的攻击点:……。结论写 /tmp/N17-<你的名字>.md,终端只回一行路径。'
58
- for a in sol grok opus; do herdr agent prompt "$a" "$MSG"; done
59
- for a in sol grok opus; do herdr agent wait "$a" --until idle --timeout 900000; done
60
- ```
44
+ 下行只给短任务和文件指针:每个成员阅读 `BRIEF.md`、最新 `FINDINGS-stageN.md`、`LEDGER.md` 和本轮文件。不得要求它重新梳理已经确认的全部背景。上行结果必须能归属到轮次和成员,并包含证据位置;长结果写入持久文件,不能依赖截断的终端文本。
61
45
 
62
46
  ## 4. 回合
63
47
 
64
- 一轮 = 广播 → 各自独立干 → 管理者从磁盘收文件 → 裁决 → 更新文档 → 再广播。
48
+ 一轮 = 记录受保护文件和工作树初始状态 → 写本轮任务 → 三个成员并发独立调查 → 管理者收齐结果 → 检查子代理未修改项目或权威文件 → 核验证据并裁决 → 更新 FINDINGS 和 LEDGER → 决定下一轮。
65
49
 
66
- 每轮广播里给每个 pane 一个**具体到能证伪的攻击点**(「查 ART 的 adbconnection.cc,确认这条错误日志与被销毁的 fd 是不是 1:1」),而不是「你看看有没有问题」。
50
+ 每个成员必须收到具体到能证伪的攻击点,例如“查 `adbconnection.cc`,确认该错误日志与被销毁的 fd 是否 1:1”,而不是“看看有没有问题”。子代理结束后即可释放;下一轮通过调查档案恢复事实状态,并重新创建独立任务。
67
51
 
68
52
  ## 5. 纪律
69
53
 
70
- - **ledger**:发现按 `N1, N2…` 只增不改;每一条被推翻的结论进 `X1, X2…` 清单,写明是谁推翻的。这份 X 清单是最终交付里最有价值的部分。
71
- - **retraction 要公开**:管理者被驳倒时,在下一次广播开头显式写「我错了,更正为……」。管理者一旦开始维护自己的结论,pane 就会停止攻击,整个队伍塌成回音室。
72
- - **replication**:任何承载结论的数字至少两方独立算。对不上先查口径再查代码。
73
- - **control**:任何「异常」指标必须配阴性样本才进文档。拿不出对照的指标只是一个数。
74
- - **grading**:每条结论标注 `源码读取 / 实测 / 推断` 和置信度。推断不能和实测并列陈述。
75
- - **裁决**:两个 pane 冲突时判出谁对,并说明依据。把冲突的结论平均掉会同时丢掉两边的信息。
54
+ - **独立性**:同轮答案不得互相可见;成员之间不直接通信。
55
+ - **只读完整性**:只读是协议约束,不假定执行环境构成沙箱。每轮结束后比较初始状态;发现子代理修改项目或权威文件时,该成员结果无效并显式报告污染范围。
56
+ - **retraction**:管理者被驳倒时,在下一轮开头明确写“我错了,更正为……”,并追加对应 X。
57
+ - **replication**:任何承载结论的数字至少由两个不同模型独立计算;对不上先查口径再查代码。
58
+ - **control**:任何“异常”指标必须配阴性样本才进入 FINDINGS。
59
+ - **grading**:每条结论标注 `源码读取 / 实测 / 推断` 和置信度;推断不能与实测并列陈述。
60
+ - **裁决**:成员冲突时管理者必须判定谁对并说明证据,不能多数投票或折中平均。
61
+ - **缺席视角**:成员失败或额度耗尽时,明确记录缺失的模型和方法角色,不能把不完整轮次当成完整评审。
76
62
 
77
63
  ## 6. 收尾
78
64
 
79
- 收敛判据:完整一轮广播后没有任何 pane 能产出新的 X。
80
-
81
- 最终文档必须单列**未闭合点**,以及**每一个需要人或设备批准才能执行的动作**。
82
-
83
- 用完 `herdr pane close <pane_id>` 关掉,pane 不会自己退出。
84
-
85
- ## 故障
86
-
87
- - pane 额度耗尽:降级为剩余 pane 继续,并在文档里记下缺失的是哪个视角。
88
- - agent 卡在 blocked:`herdr agent read <name> --source visible` 看它在等什么,多半是在等一个确认。
89
- - 广播后无人响应:`herdr agent list` 核对 pane 是否还活着,pane id 会随布局变化。
65
+ 收尾前必须进行一轮**开放攻击**:三个方法角色各自选择其认为最强的攻击点,不能只回答管理者预设的窄问题。只有完整开放攻击轮没有产生任何被管理者接受并记入 X 的新反证时才算收敛。最终文档单列未闭合点、缺失视角,以及每一个需要人或设备批准才能执行的动作。