pi-herdr-agents 2.0.4 → 3.0.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.
Files changed (77) hide show
  1. package/AGENTS.md +20 -14
  2. package/CHANGELOG.md +63 -1
  3. package/CONTEXT.md +86 -32
  4. package/README.md +262 -141
  5. package/RELEASING.md +4 -2
  6. package/config.json.example +0 -3
  7. package/docs/README.md +38 -15
  8. package/docs/adr/0001-btw-ephemeral-side-questions.md +1 -1
  9. package/docs/adr/0002-agent-workflow-skill-runtime-taxonomy.md +1 -1
  10. package/docs/adr/0003-installable-role-packs.md +21 -13
  11. package/docs/adr/0009-remove-workflow-subsystem.md +1 -1
  12. package/docs/adr/0011-explicit-worktree-cleanup.md +1 -1
  13. package/docs/adr/0012-adopt-maestro-seams-in-repo.md +45 -0
  14. package/docs/adr/0013-pack-neutral-execution-host.md +63 -0
  15. package/docs/adr/0014-operator-cancel-terminal-intent.md +134 -0
  16. package/docs/worktree-subagents.md +10 -6
  17. package/maestro/adapters/fake/fake-harness-adapter.ts +263 -0
  18. package/{pi-extension/subagents/activity.ts → maestro/adapters/pi/activity-file.ts} +15 -72
  19. package/{pi-extension/subagents → maestro/adapters/pi/child}/subagent-done.ts +30 -3
  20. package/{pi-extension/subagents → maestro/adapters/pi}/completion.ts +4 -4
  21. package/{pi-extension/subagents → maestro/adapters/pi}/launch.ts +388 -285
  22. package/maestro/adapters/pi/model-registry.ts +46 -0
  23. package/maestro/adapters/pi/pi-harness-adapter.ts +701 -0
  24. package/maestro/adapters/pi/process-identity.ts +303 -0
  25. package/{pi-extension/subagents → maestro/adapters/pi}/session.ts +1 -13
  26. package/maestro/adapters/pi/task-model-init.ts +55 -0
  27. package/maestro/core/activity.ts +29 -0
  28. package/maestro/core/config/model-config.ts +473 -0
  29. package/{pi-extension/subagents → maestro/core/config}/pane-config.ts +4 -6
  30. package/{pi-extension/subagents → maestro/core/config}/persistent-config.ts +4 -6
  31. package/{pi-extension/subagents → maestro/core/config}/role-config.ts +15 -10
  32. package/{pi-extension/subagents → maestro/core/config}/supervision-config.ts +4 -6
  33. package/{pi-extension/subagents → maestro/core/config}/task-model-init.ts +45 -58
  34. package/maestro/core/config/task-model-types.ts +22 -0
  35. package/maestro/core/harness-adapter.ts +117 -0
  36. package/maestro/core/index.ts +3 -0
  37. package/{pi-extension/subagents → maestro/core}/lifecycle.ts +27 -126
  38. package/maestro/core/roles/discovery.ts +555 -0
  39. package/{pi-extension/subagents/runtime-routing.ts → maestro/core/routing.ts} +36 -43
  40. package/maestro/core/shell.ts +3 -0
  41. package/{pi-extension/subagents → maestro/core}/status.ts +3 -7
  42. package/{pi-extension/subagents → maestro/core}/supervision.ts +1 -1
  43. package/maestro/core/surface-provider.ts +185 -0
  44. package/maestro/core/types.ts +449 -0
  45. package/maestro/core/worktree-cleanup.ts +374 -0
  46. package/maestro/core/worktree.ts +93 -0
  47. package/maestro/runtime/index.ts +44 -0
  48. package/maestro/runtime/pi-run-session.ts +1210 -0
  49. package/maestro/runtime/run-session.ts +801 -0
  50. package/maestro/runtime/surface-retention.ts +6 -0
  51. package/maestro/runtime/task-model-init.ts +22 -0
  52. package/maestro/runtime/worktree-operations.ts +627 -0
  53. package/maestro/surfaces/fake/fake-surface-provider.ts +311 -0
  54. package/maestro/surfaces/herdr/herdr-surface-provider.ts +200 -0
  55. package/{pi-extension/subagents → maestro/surfaces/herdr}/herdr.ts +65 -14
  56. package/{pi-extension/subagents → maestro/surfaces/herdr}/terminal.ts +7 -5
  57. package/package.json +10 -9
  58. package/pi-extension/subagents/config-path.ts +5 -1
  59. package/pi-extension/subagents/index.ts +848 -1805
  60. package/pi-extension/subagents/model-registry.ts +46 -0
  61. package/skills/pi-herdr-agents/SKILL.md +276 -0
  62. package/agents/adversarial-reviewer.md +0 -153
  63. package/agents/planner.md +0 -87
  64. package/agents/poteto.md +0 -32
  65. package/agents/reviewer.md +0 -132
  66. package/agents/scout.md +0 -105
  67. package/agents/visual-tester.md +0 -234
  68. package/agents/worker.md +0 -121
  69. package/docs/review-evaluation.md +0 -183
  70. package/pi-extension/subagents/model-config.ts +0 -305
  71. package/pi-extension/subagents/plan-skill.md +0 -280
  72. package/pi-extension/subagents/worktree-cleanup.ts +0 -789
  73. package/skills/orchestrate/SKILL.md +0 -91
  74. package/skills/orchestrate/adversarial-review-example.js +0 -400
  75. package/skills/orchestrate/adversarial-review.md +0 -87
  76. /package/{pi-extension/subagents → maestro/core/config}/type-guards.ts +0 -0
  77. /package/{pi-extension/subagents → maestro/core}/wake.ts +0 -0
@@ -0,0 +1,263 @@
1
+ import type {
2
+ HarnessAdapter,
3
+ ResumeOptions,
4
+ SpawnOptions,
5
+ } from "../../core/harness-adapter.ts";
6
+ import type {
7
+ AgentHandle,
8
+ AgentState,
9
+ CompletionEvidence,
10
+ } from "../../core/types.ts";
11
+
12
+ const ABORT_MESSAGE = "Aborted while waiting for subagent to finish";
13
+
14
+ interface FakeHarnessRecord {
15
+ handle: AgentHandle;
16
+ request: SpawnOptions | ResumeOptions;
17
+ state: AgentState;
18
+ output: string[];
19
+ inputs: string[];
20
+ completion?: CompletionEvidence;
21
+ exitCode?: number;
22
+ waiters: Set<Waiter>;
23
+ }
24
+
25
+ interface Waiter {
26
+ resolve(evidence: CompletionEvidence): void;
27
+ reject(error: Error): void;
28
+ signal: AbortSignal;
29
+ onAbort(): void;
30
+ }
31
+
32
+ export class FakeHarnessAdapter implements HarnessAdapter {
33
+ readonly name = "fake";
34
+
35
+ #nextHandle = 1;
36
+ #records = new Map<string, FakeHarnessRecord>();
37
+ #sessionHandles = new Map<string, Set<string>>();
38
+
39
+ isAvailable(): boolean {
40
+ return true;
41
+ }
42
+
43
+ async spawn(opts: SpawnOptions): Promise<AgentHandle> {
44
+ const handle: AgentHandle = {
45
+ id: this.#newHandleId(),
46
+ name: opts.name,
47
+ role: opts.role.name,
48
+ harness: this.name,
49
+ cwd: opts.cwd,
50
+ startedAt: Date.now(),
51
+ sessionId: opts.sessionId,
52
+ surfaceId: opts.surface?.id,
53
+ worktree: opts.worktree,
54
+ };
55
+ this.#records.set(handle.id, {
56
+ handle,
57
+ request: cloneSpawnOptions(opts),
58
+ state: "working",
59
+ output: [],
60
+ inputs: [],
61
+ waiters: new Set(),
62
+ });
63
+ this.#rememberSession(handle);
64
+ return {
65
+ ...handle,
66
+ worktree: handle.worktree ? { ...handle.worktree } : undefined,
67
+ };
68
+ }
69
+
70
+ async resume(opts: ResumeOptions): Promise<AgentHandle> {
71
+ if (!opts.name) throw new Error("resume requires name");
72
+ const prior = this.#latestSessionHandle(opts.sessionId);
73
+ const handle: AgentHandle = {
74
+ id: this.#newHandleId(),
75
+ name: opts.name,
76
+ role: prior?.role ?? "resumed",
77
+ harness: this.name,
78
+ cwd: prior?.cwd ?? process.cwd(),
79
+ startedAt: Date.now(),
80
+ sessionId: opts.sessionId,
81
+ surfaceId: opts.surface?.id,
82
+ };
83
+ this.#records.set(handle.id, {
84
+ handle,
85
+ request: { ...opts },
86
+ state: "working",
87
+ output: [],
88
+ inputs: opts.message ? [opts.message] : [],
89
+ waiters: new Set(),
90
+ });
91
+ this.#rememberSession(handle);
92
+ return { ...handle };
93
+ }
94
+
95
+ async getState(handle: AgentHandle): Promise<AgentState> {
96
+ return this.#records.get(handle.id)?.state ?? "unknown";
97
+ }
98
+
99
+ async interrupt(handle: AgentHandle): Promise<void> {
100
+ const record = this.#record(handle.id);
101
+ if (record.state !== "done") record.state = "idle";
102
+ }
103
+
104
+ async kill(handle: AgentHandle): Promise<void> {
105
+ const record = this.#record(handle.id);
106
+ record.state = "done";
107
+ this.#settle(handle.id, {
108
+ reason: "error",
109
+ exitCode: 143,
110
+ errorMessage: "terminated by kill",
111
+ });
112
+ }
113
+
114
+ async sendInput(handle: AgentHandle, text: string): Promise<void> {
115
+ this.#record(handle.id).inputs.push(text);
116
+ }
117
+
118
+ async readOutput(handle: AgentHandle, lines?: number): Promise<string> {
119
+ const output = this.#record(handle.id).output;
120
+ return (lines === undefined ? output : output.slice(-lines)).join("\n");
121
+ }
122
+
123
+ exitCode(handle: AgentHandle): number | undefined {
124
+ return this.#records.get(handle.id)?.exitCode;
125
+ }
126
+
127
+ awaitCompletion(
128
+ handle: AgentHandle,
129
+ signal: AbortSignal,
130
+ ): Promise<CompletionEvidence> {
131
+ const record = this.#record(handle.id);
132
+ if (signal.aborted) return Promise.reject(new Error(ABORT_MESSAGE));
133
+ if (record.completion) return Promise.resolve({ ...record.completion });
134
+
135
+ return new Promise((resolve, reject) => {
136
+ const waiter: Waiter = {
137
+ resolve: (evidence) => {
138
+ signal.removeEventListener("abort", waiter.onAbort);
139
+ resolve({ ...evidence });
140
+ },
141
+ reject: (error) => {
142
+ signal.removeEventListener("abort", waiter.onAbort);
143
+ reject(error);
144
+ },
145
+ signal,
146
+ onAbort: () => {
147
+ record.waiters.delete(waiter);
148
+ waiter.reject(new Error(ABORT_MESSAGE));
149
+ },
150
+ };
151
+ record.waiters.add(waiter);
152
+ signal.addEventListener("abort", waiter.onAbort, { once: true });
153
+ });
154
+ }
155
+
156
+ complete(handleId: string, evidence: CompletionEvidence): void {
157
+ this.#settle(handleId, evidence);
158
+ }
159
+
160
+ fail(handleId: string, error: string): void {
161
+ this.#settle(handleId, {
162
+ reason: "error",
163
+ exitCode: 1,
164
+ errorMessage: error,
165
+ });
166
+ }
167
+
168
+ ping(handleId: string, message: string): void {
169
+ const record = this.#record(handleId);
170
+ this.#settle(handleId, {
171
+ reason: "ping",
172
+ exitCode: 0,
173
+ ping: { name: record.handle.name, message },
174
+ });
175
+ }
176
+
177
+ spawned(): AgentHandle[] {
178
+ return Array.from(this.#records.values(), (record) =>
179
+ cloneHandle(record.handle),
180
+ );
181
+ }
182
+
183
+ inputs(handleId: string): string[] {
184
+ return [...this.#record(handleId).inputs];
185
+ }
186
+
187
+ request(handleId: string): SpawnOptions | ResumeOptions | undefined {
188
+ const request = this.#records.get(handleId)?.request;
189
+ return request ? cloneRequest(request) : undefined;
190
+ }
191
+
192
+ appendOutput(handleId: string, text: string): void {
193
+ this.#record(handleId).output.push(...text.split("\n"));
194
+ }
195
+
196
+ #settle(handleId: string, evidence: CompletionEvidence): void {
197
+ const record = this.#record(handleId);
198
+ if (record.completion) return;
199
+ const completion = {
200
+ ...evidence,
201
+ sessionRef: evidence.sessionRef ?? record.handle.sessionId,
202
+ };
203
+ record.completion = completion;
204
+ record.exitCode = completion.exitCode;
205
+ record.state = "done";
206
+ const waiters = [...record.waiters];
207
+ record.waiters.clear();
208
+ for (const waiter of waiters) waiter.resolve(completion);
209
+ }
210
+
211
+ #record(handleId: string): FakeHarnessRecord {
212
+ const record = this.#records.get(handleId);
213
+ if (!record) throw new Error(`unknown handle: ${handleId}`);
214
+ return record;
215
+ }
216
+
217
+ #newHandleId(): string {
218
+ const id = `fake-agent-${this.#nextHandle}`;
219
+ this.#nextHandle += 1;
220
+ return id;
221
+ }
222
+
223
+ #rememberSession(handle: AgentHandle): void {
224
+ const handles =
225
+ this.#sessionHandles.get(handle.sessionId) ?? new Set<string>();
226
+ handles.add(handle.id);
227
+ this.#sessionHandles.set(handle.sessionId, handles);
228
+ }
229
+
230
+ #latestSessionHandle(sessionId: string): AgentHandle | undefined {
231
+ const handles = this.#sessionHandles.get(sessionId);
232
+ if (!handles) return undefined;
233
+ const handleId = Array.from(handles).at(-1);
234
+ return handleId ? this.#records.get(handleId)?.handle : undefined;
235
+ }
236
+ }
237
+
238
+ function cloneRequest(request: SpawnOptions | ResumeOptions) {
239
+ if ("task" in request) return cloneSpawnOptions(request);
240
+ return {
241
+ ...request,
242
+ env: request.env ? { ...request.env } : undefined,
243
+ };
244
+ }
245
+
246
+ function cloneSpawnOptions(options: SpawnOptions): SpawnOptions {
247
+ return {
248
+ ...options,
249
+ role: { ...options.role, allowedTools: [...options.role.allowedTools] },
250
+ worktree: options.worktree ? { ...options.worktree } : undefined,
251
+ worktreeRequest: options.worktreeRequest
252
+ ? { ...options.worktreeRequest }
253
+ : undefined,
254
+ env: options.env ? { ...options.env } : undefined,
255
+ };
256
+ }
257
+
258
+ function cloneHandle(handle: AgentHandle): AgentHandle {
259
+ return {
260
+ ...handle,
261
+ worktree: handle.worktree ? { ...handle.worktree } : undefined,
262
+ };
263
+ }
@@ -12,62 +12,18 @@ import {
12
12
  isFiniteNumber,
13
13
  isPlainObject,
14
14
  isString,
15
- } from "./type-guards.ts";
16
-
17
- export type SubagentActivityPhase = "starting" | "active" | "waiting" | "done";
18
- export type SubagentActivityScope =
19
- | "agent"
20
- | "turn"
21
- | "provider"
22
- | "streaming"
23
- | "tool";
24
-
25
- export type SubagentActivityEvent =
26
- | "session_start"
27
- | "input"
28
- | "before_agent_start"
29
- | "agent_start"
30
- | "agent_end"
31
- | "turn_start"
32
- | "turn_end"
33
- | "before_provider_request"
34
- | "after_provider_response"
35
- | "message_update"
36
- | "tool_execution_start"
37
- | "tool_call"
38
- | "tool_execution_update"
39
- | "tool_result"
40
- | "tool_execution_end"
41
- | "caller_ping"
42
- | "subagent_done"
43
- | "session_shutdown";
44
-
45
- export interface SubagentActivityState {
46
- version: 1;
47
- runningChildId: string;
48
- createdAt: number;
49
- updatedAt: number;
50
- sequence: number;
51
- latestEvent: SubagentActivityEvent;
52
- phase: SubagentActivityPhase;
53
- agentActive: boolean;
54
- turnActive: boolean;
55
- providerActive: boolean;
56
- toolActive: boolean;
57
- activeScope?: SubagentActivityScope;
58
- activeSince?: number;
59
- waitingSince?: number;
60
- turnIndex?: number;
61
- messageEventType?: string;
62
- toolCallId?: string;
63
- toolName?: string;
64
- toolStartedAt?: number;
65
- toolEndedAt?: number;
66
- }
67
-
68
- export type ActivityReadResult =
69
- | { ok: true; activity: SubagentActivityState }
70
- | { ok: false; reason: "missing" | "invalid" | "wrong-id"; error?: string };
15
+ } from "../../core/config/type-guards.ts";
16
+ import {
17
+ isSubagentActivityScope,
18
+ projectActivity,
19
+ } from "../../core/activity.ts";
20
+ import type {
21
+ ActivityReadResult,
22
+ SubagentActivityEvent,
23
+ SubagentActivityPhase,
24
+ SubagentActivityScope,
25
+ SubagentActivityState,
26
+ } from "../../core/types.ts";
71
27
 
72
28
  export type SubagentShutdownReason =
73
29
  | "quit"
@@ -106,13 +62,6 @@ const KNOWN_PHASES = new Set<SubagentActivityPhase>([
106
62
  "waiting",
107
63
  "done",
108
64
  ]);
109
- const KNOWN_SCOPES = new Set<SubagentActivityScope>([
110
- "agent",
111
- "turn",
112
- "provider",
113
- "streaming",
114
- "tool",
115
- ]);
116
65
  const KNOWN_EVENTS = new Set<SubagentActivityEvent>([
117
66
  "session_start",
118
67
  "input",
@@ -142,12 +91,6 @@ export function getSubagentActivityFile(
142
91
  return join(artifactDir, "subagent-activity", `${runningChildId}.json`);
143
92
  }
144
93
 
145
- export function isSubagentActivityScope(
146
- value: any,
147
- ): value is SubagentActivityScope {
148
- return isString(value) && KNOWN_SCOPES.has(value);
149
- }
150
-
151
94
  function requireObject(value: any) {
152
95
  return isPlainObject(value) ? value : null;
153
96
  }
@@ -225,7 +168,7 @@ function validateActivity(
225
168
  }
226
169
  if (
227
170
  object.activeScope != null &&
228
- (!isString(object.activeScope) || !KNOWN_SCOPES.has(object.activeScope))
171
+ !isSubagentActivityScope(object.activeScope)
229
172
  ) {
230
173
  return invalidActivity("unknown activeScope");
231
174
  }
@@ -249,14 +192,14 @@ function validateActivity(
249
192
  ].find((error) => error != null);
250
193
  if (validationError) return invalidActivity(validationError);
251
194
 
252
- return { ok: true, activity: object };
195
+ return projectActivity(object);
253
196
  }
254
197
 
255
198
  export function readSubagentActivityFile(
256
199
  activityFile: string,
257
200
  expectedRunningChildId: string,
258
201
  ): ActivityReadResult {
259
- if (!existsSync(activityFile)) return { ok: false, reason: "missing" };
202
+ if (!existsSync(activityFile)) return projectActivity(undefined);
260
203
 
261
204
  let parsed: unknown;
262
205
  try {
@@ -12,9 +12,13 @@ import {
12
12
  consumePersistentTaskInbox,
13
13
  readPersistentDeliveryLedger,
14
14
  readPersistentTaskEvents,
15
- } from "./session.ts";
16
- import { createSubagentActivityRecorder } from "./activity.ts";
17
- import { isString } from "./type-guards.ts";
15
+ } from "../session.ts";
16
+ import { createSubagentActivityRecorder } from "../activity-file.ts";
17
+ import {
18
+ readOwnProcessIdentity,
19
+ writeProcessIdentityRecord,
20
+ } from "../process-identity.ts";
21
+ import { isString } from "../../../core/config/type-guards.ts";
18
22
 
19
23
  export function shouldMarkUserTookOver(agentStarted: boolean): boolean {
20
24
  return agentStarted;
@@ -130,6 +134,28 @@ export function parseDeniedTools(rawValue: string | undefined): string[] {
130
134
  .filter(Boolean);
131
135
  }
132
136
 
137
+ /**
138
+ * A managed worktree child names its own process at load from immutable
139
+ * kernel facts, which Pi's process-title rewrite cannot alter.
140
+ */
141
+ export function recordProcessIdentity(
142
+ id: string | undefined,
143
+ sessionFile: string | undefined,
144
+ file = process.env.PI_SUBAGENT_PROCESS_FILE,
145
+ ): void {
146
+ if (!file || !id || !sessionFile) return;
147
+ try {
148
+ writeProcessIdentityRecord(file, {
149
+ version: 1,
150
+ id,
151
+ sessionFile,
152
+ ...readOwnProcessIdentity(),
153
+ });
154
+ } catch {
155
+ // Unrecorded identity leaves a worktree cancel unconfirmed, never guessed.
156
+ }
157
+ }
158
+
133
159
  export default function (pi: ExtensionAPI) {
134
160
  let toolNames: string[] = [];
135
161
  let denied: string[] = [];
@@ -157,6 +183,7 @@ export default function (pi: ExtensionAPI) {
157
183
  runningChildId: process.env.PI_SUBAGENT_ID,
158
184
  activityFile: process.env.PI_SUBAGENT_ACTIVITY_FILE,
159
185
  });
186
+ recordProcessIdentity(process.env.PI_SUBAGENT_ID, sessionFile);
160
187
 
161
188
  function renderWidget(ctx: { ui: { setWidget: Function } }, _theme: any) {
162
189
  ctx.ui.setWidget(
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, rmSync } from "node:fs";
2
2
 
3
- import { isNonEmptyString, isString } from "./type-guards.ts";
3
+ import { isNonEmptyString, isString } from "../../core/config/type-guards.ts";
4
4
 
5
5
  const ABORT_MESSAGE = "Aborted while waiting for subagent to finish";
6
6
  const TERMINAL_SENTINEL = /__SUBAGENT_DONE_(\d+)__/;
@@ -15,11 +15,11 @@ export interface CompletionResult {
15
15
  export interface CompletionOptions {
16
16
  intervalMs: number;
17
17
  readTerminalTail: () => Promise<string>;
18
- inspectPane?: () => Promise<import("./lifecycle.ts").PaneInspection>;
18
+ inspectPane?: () => Promise<import("../../core/types.ts").PaneInspection>;
19
19
  /** Bounded artifact grace after explicit pane disappearance. Default: 500ms. */
20
20
  paneDisappearanceGraceMs?: number;
21
21
  onPaneInspection?: (
22
- inspection: import("./lifecycle.ts").PaneInspection,
22
+ inspection: import("../../core/types.ts").PaneInspection,
23
23
  observedAt: number,
24
24
  ) => void;
25
25
  sessionFile?: string;
@@ -168,7 +168,7 @@ export async function waitForCompletion(
168
168
  }
169
169
 
170
170
  if (reconcile && options.inspectPane) {
171
- let inspection: import("./lifecycle.ts").PaneInspection;
171
+ let inspection: import("../../core/types.ts").PaneInspection;
172
172
  try {
173
173
  inspection = await options.inspectPane();
174
174
  } catch {