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
|
@@ -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
|
-
):
|
|
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
|
|
3068
|
+
const sessionExists = existsSync(sessionFile);
|
|
3069
|
+
const childEntries = sessionExists
|
|
3037
3070
|
? getNewEntries(sessionFile, 0)
|
|
3038
3071
|
: [];
|
|
3039
|
-
const
|
|
3040
|
-
|
|
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];
|