pi-herdr-agents 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -370,6 +370,8 @@ subagent({
370
370
 
371
371
  Use one worktree per independent writing task; keep read-only agents in ordinary panes. `cwd` selects the source Git repository, `branch` must be unique, and `base` is resolved to an exact commit before creation. If `base` is omitted, the source checkout's committed `HEAD` is used. Parent-checkout changes that have not been committed are not copied.
372
372
 
373
+ A launch with `worktree` and an effective bundled `scout`, `reviewer`, or `adversarial-reviewer` returns a non-blocking warning. Scouts and reviewers normally need an ordinary pane; the adversarial reviewer is a coordinator that normally uses an ordinary pane for its review artifacts and child reviewers. To inspect or review an existing worker result, start an ordinary child in that retained worktree path. Project or global role overrides do not receive these bundled-role warnings.
374
+
373
375
  The child starts at the returned worktree root. Tell writing agents to test and commit when you want a commit-based handoff, and tell them not to push, merge, switch branches, or remove the worktree. The parent owns review and integration.
374
376
 
375
377
  Successful, failed, and help-requesting runs retain their workspace. Completion includes the worktree path, Herdr workspace, branch, base/head SHAs, commits ahead, changed and untracked files, and clean/dirty/conflicted state. Here, `clean` means no uncommitted files; the branch may still contain commits. If Git inspection fails, state is reported as unknown rather than guessed.
@@ -317,13 +317,18 @@ The full bounded `RunEnvelope` exists exactly once, inside the single terminal e
317
317
  ```text
318
318
  approved
319
319
  started
320
+ reader_checkout_ready | reader_checkout_retained | reader_checkout_disposed
320
321
  agent_started
322
+ agent_completed
321
323
  agent_result
324
+ pane_close_failed
322
325
  workflow_log
323
326
  completed | failed | cancelled | interrupted
324
327
  delivery
325
328
  ```
326
329
 
330
+ `agent_completed` records the observed child exit, session availability, final assistant-text length, and (when present) final stop reason before the corresponding `agent_result` records the workflow-facing success or failure envelope.
331
+
327
332
  `delivery` follows the terminal event and contains only its terminal-event ID/state, target parent session, attempt time, and `sent | failed` delivery status. It never duplicates the task result. Evidence readers obtain the result from the referenced terminal event. Do not add mutable snapshots, locks, leases, backups, call caching, or replay.
328
333
 
329
334
  One process-global owner records the run ID, canonical project identity, lifecycle gate, Worker, and private child handles. `/new`, `/resume`, and `/fork` cannot start a second workflow while this owner exists; cancellation names the run ID and must match the current canonical project identity. On same-process `/reload`, the owner and Worker continue, and completion selects the latest extension API once. On full startup, scan only direct child directories under the current project's `.pi/plans/` and inspect only each journal's last valid event. A `delivery` event carries its referenced terminal state, a terminal event is already settled, and a non-terminal running event with no live process-global owner receives one `interrupted` terminal event. Do not recurse, restart, clean, or expose history UI.
@@ -121,7 +121,27 @@ The parent receives the normal child summary plus:
121
121
 
122
122
  `clean` means there are no staged, unstaged, or untracked files. It does **not** mean the branch has no commits or diff relative to its base.
123
123
 
124
- If Git inspection fails, SHA/count/state/file fields are reported as unknown rather than guessed, and the warning is included in the handoff. Inspect the retained workspace directly before integrating or deleting it.
124
+ If Git inspection fails, SHA/count/state/file fields are reported as unknown rather than guessed, and the warning is included in the handoff. Inspect the retained workspace directly before integrating or deleting it. Every retained handoff also includes the exact `herdr worktree remove --workspace <workspace-id>` command, but run it only after useful state is preserved.
125
+
126
+ ## Parallel pull-request review without new worktrees
127
+
128
+ For parallel read-only review, prepare one stable existing checkout of the pull request or retained worker result. Do not create one managed worktree per reviewer.
129
+
130
+ 1. The parent records the exact base and head SHAs and makes sure no writer changes the checkout while review runs.
131
+ 2. Start each read-only child in an ordinary pane with `cwd` set to that checkout. Omit `worktree`.
132
+ 3. Give every reviewer the same exact base and head SHAs. Require it to report `git rev-parse HEAD` before its review result.
133
+ 4. Before the parent reports or publishes the review, recheck the checkout SHA. If it changed, treat the prior reviews as stale and review the new commit again.
134
+
135
+ ```typescript
136
+ subagent({
137
+ name: "PR reviewer",
138
+ agent: "reviewer",
139
+ cwd: "/path/to/pr-checkout",
140
+ task: "Review base <base-sha> through head <head-sha>. First report git rev-parse HEAD. Do not modify files.",
141
+ });
142
+ ```
143
+
144
+ A retained worker worktree can be this checkout. The parent owns any final report, PR action, integration, and cleanup.
125
145
 
126
146
  ## Review and integration
127
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-herdr-agents",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Asynchronous Pi subagents and approved review workflows in Herdr, with optional isolated Git worktrees",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -85,6 +85,7 @@ import {
85
85
 
86
86
  import {
87
87
  findLastAssistantMessage,
88
+ inspectFinalAssistantMessage,
88
89
  findObservedSessionRuntime,
89
90
  getNewEntries,
90
91
  seedSubagentSessionFile,
@@ -790,7 +791,7 @@ function resolveEffectiveInteractive(
790
791
  function loadAgentDefaults(
791
792
  agentName: string,
792
793
  pi?: Pick<ExtensionAPI, "events">,
793
- ): AgentDefaults | null {
794
+ ): ListedAgentDefinition | null {
794
795
  return (
795
796
  discoverAgentCatalog(pi).agents.find((agent) => agent.name === agentName) ??
796
797
  null
@@ -955,6 +956,32 @@ function shouldRetainSubagentSurface(
955
956
  return !!running.worktree;
956
957
  }
957
958
 
959
+ const BUNDLED_WORKTREE_WARNINGS: Readonly<Record<string, string>> = {
960
+ scout:
961
+ "The bundled scout role is read-only and normally does not need a new worktree. " +
962
+ "Use an ordinary pane instead; to inspect an existing worker result, start it in the retained worktree path. " +
963
+ "Herdr worktree workspaces persist until explicitly removed.",
964
+ reviewer:
965
+ "The bundled reviewer role is read-only and normally does not need a new worktree. " +
966
+ "Use an ordinary pane instead; to review an existing worker result, start it in the retained worktree path. " +
967
+ "Herdr worktree workspaces persist until explicitly removed.",
968
+ "adversarial-reviewer":
969
+ "The bundled adversarial-reviewer coordinates read-only reviewers and writes review artifacts. " +
970
+ "It normally uses an ordinary pane, not a new worktree. " +
971
+ "Herdr worktree workspaces persist until explicitly removed.",
972
+ };
973
+
974
+ function resolveWorktreeLaunchWarning(
975
+ params: Pick<Static<typeof SubagentParams>, "agent" | "worktree">,
976
+ pi?: Pick<ExtensionAPI, "events">,
977
+ ): string | undefined {
978
+ if (!params.worktree || !params.agent) return undefined;
979
+ const warning = BUNDLED_WORKTREE_WARNINGS[params.agent];
980
+ return warning && loadAgentDefaults(params.agent, pi)?.source === "package"
981
+ ? warning
982
+ : undefined;
983
+ }
984
+
958
985
  function runSubagentScript(
959
986
  surface: string,
960
987
  command: string,
@@ -1125,6 +1152,10 @@ function formatWorktreeHandoff(worktree: WorktreeHandoff): string {
1125
1152
  lines.push(`Untracked: ${worktree.untrackedFiles.join(", ")}`);
1126
1153
  if (worktree.gitError)
1127
1154
  lines.push(`Git inspection warning: ${worktree.gitError}`);
1155
+ lines.push(
1156
+ "After review and preservation, remove the workspace with:",
1157
+ ` herdr worktree remove --workspace ${worktree.workspaceId}`,
1158
+ );
1128
1159
  return lines.join("\n");
1129
1160
  }
1130
1161
 
@@ -1993,6 +2024,7 @@ export const __test__ = {
1993
2024
  sendSubagentResult,
1994
2025
  resolveResumeLaunchBehavior,
1995
2026
  shouldRetainSubagentSurface,
2027
+ resolveWorktreeLaunchWarning,
1996
2028
  captureWorktreeHandoff,
1997
2029
  runSubagentScript,
1998
2030
  writeWorktreeManifest,
@@ -3033,18 +3065,43 @@ export default function subagentsExtension(pi: ExtensionAPI) {
3033
3065
  if (childController.signal.aborted || watched.error === "cancelled") {
3034
3066
  return workflowFailure("cancelled", "Workflow cancelled.");
3035
3067
  }
3036
- const childEntries = existsSync(sessionFile)
3068
+ const sessionExists = existsSync(sessionFile);
3069
+ const childEntries = sessionExists
3037
3070
  ? getNewEntries(sessionFile, 0)
3038
3071
  : [];
3039
- const summary = findLastAssistantMessage(childEntries);
3040
- if (watched.exitCode !== 0 || watched.errorMessage || !summary) {
3072
+ const finalAssistant = inspectFinalAssistantMessage(childEntries);
3073
+ journal.append("agent_completed", {
3074
+ id,
3075
+ role: roleName,
3076
+ sessionFile,
3077
+ sessionExists,
3078
+ exitCode: watched.exitCode,
3079
+ ...(watched.errorMessage
3080
+ ? { errorMessage: watched.errorMessage }
3081
+ : {}),
3082
+ finalAssistantContentLength: finalAssistant.contentLength,
3083
+ ...(finalAssistant.stopReason
3084
+ ? { finalAssistantStopReason: finalAssistant.stopReason }
3085
+ : {}),
3086
+ });
3087
+ if (watched.exitCode !== 0 || watched.errorMessage) {
3041
3088
  return workflowFailure(
3042
3089
  "child_error",
3043
3090
  watched.errorMessage ??
3044
- summary ??
3045
3091
  `Workflow child exited with code ${watched.exitCode}`,
3046
3092
  );
3047
3093
  }
3094
+ if (!finalAssistant.text) {
3095
+ return workflowFailure(
3096
+ "empty_completion",
3097
+ `Workflow child completed without assistant text${
3098
+ finalAssistant.stopReason
3099
+ ? ` (stopReason: ${finalAssistant.stopReason})`
3100
+ : ""
3101
+ }.`,
3102
+ );
3103
+ }
3104
+ const summary = finalAssistant.text;
3048
3105
  const observed = findObservedSessionRuntime(childEntries);
3049
3106
  const observedModel =
3050
3107
  observed.provider && observed.modelId
@@ -3717,6 +3774,10 @@ export default function subagentsExtension(pi: ExtensionAPI) {
3717
3774
  ctx,
3718
3775
  parentThinking,
3719
3776
  );
3777
+ const worktreeLaunchWarning = resolveWorktreeLaunchWarning(
3778
+ params,
3779
+ runtime.pi,
3780
+ );
3720
3781
  const { running, index: initialPlanIndex } = await launchSubagentWithFallbacks(
3721
3782
  params,
3722
3783
  ctx,
@@ -3844,6 +3905,9 @@ export default function subagentsExtension(pi: ExtensionAPI) {
3844
3905
  (running.worktree
3845
3906
  ? ` in worktree ${running.worktree.path} on branch ${running.worktree.branch}. `
3846
3907
  : ". ") +
3908
+ (worktreeLaunchWarning
3909
+ ? `Warning: ${worktreeLaunchWarning} `
3910
+ : "") +
3847
3911
  `Do NOT generate or assume any results — you have no idea what the sub-agent will do or produce. ` +
3848
3912
  `The results will be delivered to you automatically as a steer message when the sub-agent finishes. ` +
3849
3913
  `Until then, move on to other work or tell the user you're waiting.`,
@@ -3860,6 +3924,9 @@ export default function subagentsExtension(pi: ExtensionAPI) {
3860
3924
  thinking: running.runtimePlan?.thinking,
3861
3925
  runtimePlan: running.runtimePlan,
3862
3926
  ...(running.worktree ? { worktree: running.worktree } : {}),
3927
+ ...(worktreeLaunchWarning
3928
+ ? { warning: worktreeLaunchWarning }
3929
+ : {}),
3863
3930
  status: "started",
3864
3931
  },
3865
3932
  };
@@ -136,6 +136,39 @@ export function findObservedSessionRuntime(entries: SessionEntry[]): ObservedSes
136
136
  return observed;
137
137
  }
138
138
 
139
+ export interface FinalAssistantMessage {
140
+ text: string | null;
141
+ contentLength: number;
142
+ stopReason?: string;
143
+ }
144
+
145
+ /** Inspect only the final assistant message for workflow completion evidence. */
146
+ export function inspectFinalAssistantMessage(
147
+ entries: SessionEntry[],
148
+ ): FinalAssistantMessage {
149
+ for (let i = entries.length - 1; i >= 0; i--) {
150
+ const entry = entries[i];
151
+ if (entry.type !== "message") continue;
152
+ const msg = entry as MessageEntry;
153
+ if (msg.message.role !== "assistant") continue;
154
+
155
+ const texts = msg.message.content
156
+ .filter(
157
+ (block) =>
158
+ block.type === "text" && typeof block.text === "string",
159
+ )
160
+ .map((block) => block.text as string);
161
+ const text = texts.join("\n");
162
+ const stopReason = (msg.message as { stopReason?: unknown }).stopReason;
163
+ return {
164
+ text: text.trim() ? text : null,
165
+ contentLength: text.length,
166
+ ...(typeof stopReason === "string" ? { stopReason } : {}),
167
+ };
168
+ }
169
+ return { text: null, contentLength: 0 };
170
+ }
171
+
139
172
  export function findLastAssistantMessage(entries: SessionEntry[]): string | null {
140
173
  for (let i = entries.length - 1; i >= 0; i--) {
141
174
  const entry = entries[i];