pi-herdr-subagents 0.1.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.
@@ -0,0 +1,203 @@
1
+ ---
2
+ name: plan
3
+ description: >
4
+ Planning workflow. Runs a pre-flight scout, then spawns the planner agent
5
+ which clarifies WHAT to build and figures out HOW, with the ability to
6
+ spawn its own scouts/researchers mid-session. Use when asked to "plan",
7
+ "brainstorm", "I want to build X", or "let's design". Requires the
8
+ subagents extension running inside herdr.
9
+ ---
10
+
11
+ # Plan
12
+
13
+ A planning workflow. A scout maps the relevant codebase, then an interactive planner clarifies intent + requirements and designs the technical approach, producing a `plan.md` and todos.
14
+
15
+ **Announce at start:** "Let me take a quick look, then I'll send a scout to map the codebase before we start the planning session."
16
+
17
+ ---
18
+
19
+ ## The Flow
20
+
21
+ ```
22
+ Phase 1: Quick Assessment (main session — 30s orientation)
23
+
24
+ Phase 2: Scout (autonomous — codebase context)
25
+
26
+ Phase 3: Spawn Planner Agent (interactive — clarifies WHAT, plans HOW, creates todos)
27
+
28
+ (Planner may spawn its own scouts/researchers mid-session as needed)
29
+
30
+ Phase 4: Review Plan & Todos (main session)
31
+
32
+ Phase 5: Execute Todos (workers — receive plan + scout context)
33
+
34
+ Phase 6: Review
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Phase 1: Quick Assessment
40
+
41
+ Quick orientation — just enough to give the scout a focused mission:
42
+
43
+ ```bash
44
+ ls -la
45
+ find . -type f -name "*.ts" | head -20 # or relevant extension
46
+ cat package.json 2>/dev/null | head -30
47
+ ```
48
+
49
+ Spend ~30 seconds. Tech stack, project shape, and the area relevant to the user's request. This tells you what to ask the scout to focus on.
50
+
51
+ ---
52
+
53
+ ## Artifact Paths
54
+
55
+ For a planning run, pick a short `<name>` (e.g. `auth-redesign`) and use a shared directory under `.pi/plans/YYYY-MM-DD-<name>/` for every deliverable. Pass explicit paths in each subagent's task and read them back with the plain `read` tool when a subagent finishes.
56
+
57
+ Standard filenames:
58
+
59
+ - `.pi/plans/YYYY-MM-DD-<name>/scout-context.md`
60
+ - `.pi/plans/YYYY-MM-DD-<name>/plan.md`
61
+ - `.pi/plans/YYYY-MM-DD-<name>/review.md` (optional, for reviewer output)
62
+
63
+ ---
64
+
65
+ ## Phase 2: Scout
66
+
67
+ **Always spawn a scout before the planner.** The scout's context feeds into the planning session — it lets the planner skip re-asking questions whose answers live in the code, and gives it a solid base to design from.
68
+
69
+ ```typescript
70
+ subagent({
71
+ name: "🔍 Scout",
72
+ agent: "scout",
73
+ task: `Analyze the codebase for [user's request area]. Map file structure, key modules, patterns, conventions, and existing code related to [feature area]. Focus on what a planner would need to understand before designing this feature.
74
+
75
+ Save your findings to: .pi/plans/YYYY-MM-DD-<name>/scout-context.md`,
76
+ });
77
+ ```
78
+
79
+ **Wait for the scout to finish.** Read the scout's context file with the `read` tool — you'll pass it to the planner.
80
+
81
+ The planner can spawn **additional** scouts or researchers mid-session if it hits a factual gap. That's expected — don't try to pre-scout every possible area.
82
+
83
+ ---
84
+
85
+ ## Phase 3: Spawn Planner Agent
86
+
87
+ Spawn the interactive planner with the scout's context and the user's request. The planner handles everything from here: clarifying intent, compact requirements engineering, ISC, approach exploration, design validation, premortem, plan artifact, and todos.
88
+
89
+ ```typescript
90
+ subagent({
91
+ name: "💬 Planner",
92
+ agent: "planner",
93
+ interactive: true,
94
+ task: `Plan: [what the user wants to build]
95
+
96
+ Scout context:
97
+ [paste scout findings here — file structure, conventions, patterns, relevant code]
98
+
99
+ Save the final plan to: .pi/plans/YYYY-MM-DD-<name>/plan.md
100
+ Create todos tagged with: <name>`,
101
+ });
102
+ ```
103
+
104
+ **The user works with the planner.** It will clarify requirements lightly (1-2 rounds of questions, not a deep spec session), propose approaches, validate the design, run a premortem, write the plan, and create todos with mandatory code examples.
105
+
106
+ When done, the user presses Ctrl+D and the plan + todos are returned to the main session.
107
+
108
+ ### The planner may spawn its own specialists
109
+
110
+ During the session, the planner can spawn:
111
+ - **`scout`** — when a design decision depends on existing code it hasn't read
112
+ - **`researcher`** — when a decision depends on external facts (library tradeoffs, best practices, API behaviors)
113
+
114
+ These are internal to the planning session. You'll see them in herdr but don't need to intervene.
115
+
116
+ ### Optional: extra scout after planning
117
+
118
+ If the planner significantly changed scope (new subsystems, areas the original scout didn't cover), spawn another scout targeting the new areas before workers start:
119
+
120
+ ```typescript
121
+ subagent({
122
+ name: "🔍 Scout (updated scope)",
123
+ agent: "scout",
124
+ task: "The plan changed scope. Gather context for [new areas]. Read the plan at [plan path]. Focus on [specific files/modules the planner identified that weren't in the original scout].",
125
+ });
126
+ ```
127
+
128
+ Fold the new context into the worker tasks.
129
+
130
+ ---
131
+
132
+ ## Phase 4: Review Plan & Todos
133
+
134
+ Once the planner closes, read the plan and list todos:
135
+
136
+ ```typescript
137
+ todo({ action: "list" });
138
+ ```
139
+
140
+ Review with the user:
141
+
142
+ > "Here's what the planner produced: [brief summary]. Ready to execute, or anything to adjust?"
143
+
144
+ ---
145
+
146
+ ## Phase 5: Execute Todos
147
+
148
+ Spawn workers sequentially. Each worker gets the plan path and scout context:
149
+
150
+ ```typescript
151
+ // Workers execute todos sequentially — one at a time
152
+ subagent({
153
+ name: "🔨 Worker 1/N",
154
+ agent: "worker",
155
+ task: "Implement TODO-xxxx. Mark the todo as done. Plan: [plan path]\n\nScout context: [paste scout summary from Phase 2, plus any re-scout from Phase 3]",
156
+ });
157
+
158
+ // Check result, then next todo
159
+ subagent({
160
+ name: "🔨 Worker 2/N",
161
+ agent: "worker",
162
+ task: "Implement TODO-yyyy. Mark the todo as done. Plan: [plan path]\n\nScout context: [paste scout summary]",
163
+ });
164
+ ```
165
+
166
+ **Always run workers sequentially in the same git repo** — parallel workers will conflict on commits.
167
+
168
+ ---
169
+
170
+ ## Phase 6: Review
171
+
172
+ After all todos are complete:
173
+
174
+ ```typescript
175
+ subagent({
176
+ name: "Reviewer",
177
+ agent: "reviewer",
178
+ interactive: false,
179
+ task: "Review the recent changes. Plan: [plan path]",
180
+ });
181
+ ```
182
+
183
+ Triage findings:
184
+
185
+ - **P0** — Real bugs, security issues → fix now
186
+ - **P1** — Genuine traps, maintenance dangers → fix before merging
187
+ - **P2** — Minor issues → fix if quick, note otherwise
188
+ - **P3** — Nits → skip
189
+
190
+ Create todos for P0/P1, run workers to fix, re-review only if fixes were substantial.
191
+
192
+ ---
193
+
194
+ ## ⚠️ Completion Checklist
195
+
196
+ Before reporting done:
197
+
198
+ 1. ✅ Scout ran before the planner?
199
+ 2. ✅ Scout context was passed to the planner?
200
+ 3. ✅ All worker todos closed?
201
+ 4. ✅ Every todo has a polished commit (using the `commit` skill)?
202
+ 5. ✅ Reviewer has run?
203
+ 6. ✅ Reviewer findings triaged and addressed?
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "pi-auto-exit",
3
+ "version": "1.0.0",
4
+ "description": "Auto-exit plugin for pi-spawned Claude sessions"
5
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "Stop": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/on-stop.sh",
9
+ "timeout": 10
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # Stop hook for pi-spawned Claude sessions.
3
+ # Writes a sentinel file when Claude completes autonomously (no user interjection).
4
+
5
+ set -euo pipefail
6
+
7
+ # Read JSON input from stdin
8
+ input=$(cat)
9
+
10
+ # Guard: if stop_hook_active is true, we're in a loop — bail out
11
+ stop_hook_active=$(echo "$input" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('stop_hook_active', False))" 2>/dev/null || echo "False")
12
+ if [ "$stop_hook_active" = "True" ]; then
13
+ exit 0
14
+ fi
15
+
16
+ # Guard: only act for pi-spawned sessions
17
+ if [ -z "${PI_CLAUDE_SENTINEL:-}" ]; then
18
+ exit 0
19
+ fi
20
+
21
+ # Get transcript path
22
+ transcript_path=$(echo "$input" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('transcript_path', ''))" 2>/dev/null || echo "")
23
+ if [ -z "$transcript_path" ] || [ ! -f "$transcript_path" ]; then
24
+ exit 0
25
+ fi
26
+
27
+ # Count real human messages in transcript (not tool results)
28
+ # Claude's transcript format:
29
+ # Human message: {"type": "user", "message": {"role": "user", "content": "..."}}
30
+ # Tool result: {"type": "user", "message": {"role": "user", "content": [{"type": "tool_result", ...}]}}
31
+ # We only count entries where content is a string (real human input)
32
+ user_msg_count=$(python3 - "$transcript_path" <<'EOF'
33
+ import sys, json
34
+
35
+ transcript_path = sys.argv[1]
36
+ count = 0
37
+ with open(transcript_path, 'r') as f:
38
+ for line in f:
39
+ line = line.strip()
40
+ if not line:
41
+ continue
42
+ try:
43
+ entry = json.loads(line)
44
+ if entry.get('type') != 'user':
45
+ continue
46
+ content = entry.get('message', {}).get('content', '')
47
+ # Real human messages have string content
48
+ # Tool results have array content with tool_result blocks
49
+ if isinstance(content, str):
50
+ count += 1
51
+ except (json.JSONDecodeError, AttributeError):
52
+ pass
53
+ print(count)
54
+ EOF
55
+ )
56
+
57
+ # Always write transcript path so the watcher can copy the session file
58
+ if [ -n "$transcript_path" ]; then
59
+ echo "$transcript_path" > "${PI_CLAUDE_SENTINEL}.transcript" 2>/dev/null || true
60
+ fi
61
+
62
+ # If exactly 1 user message (the initial prompt), this was autonomous — signal completion
63
+ if [ "$user_msg_count" -eq 1 ]; then
64
+ # Write last_assistant_message to sentinel so the watcher gets a clean result
65
+ echo "$input" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('last_assistant_message', ''))" > "$PI_CLAUDE_SENTINEL" 2>/dev/null || touch "$PI_CLAUDE_SENTINEL"
66
+ fi
67
+
68
+ exit 0
@@ -0,0 +1,180 @@
1
+ import { appendFileSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { randomBytes, randomUUID } from "node:crypto";
3
+ import { dirname, join } from "node:path";
4
+
5
+ export interface SessionEntry {
6
+ type: string;
7
+ id: string;
8
+ parentId?: string;
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ export interface MessageEntry extends SessionEntry {
13
+ type: "message";
14
+ message: {
15
+ role: "user" | "assistant" | "toolResult";
16
+ content: Array<{ type: string; text?: string; [key: string]: unknown }>;
17
+ };
18
+ }
19
+
20
+ export type SeededSubagentSessionMode = "lineage-only" | "fork";
21
+
22
+ function getForkContentLines(parentSessionFile: string): string[] {
23
+ const raw = readFileSync(parentSessionFile, "utf8");
24
+ const lines = raw.split("\n").filter((line) => line.trim());
25
+
26
+ let truncateAt = lines.length;
27
+ for (let i = lines.length - 1; i >= 0; i--) {
28
+ try {
29
+ const entry = JSON.parse(lines[i]);
30
+ if (entry.type === "message" && entry.message?.role === "user") {
31
+ truncateAt = i;
32
+ break;
33
+ }
34
+ } catch {
35
+ // ignore malformed lines
36
+ }
37
+ }
38
+
39
+ return lines.slice(0, truncateAt).filter((line) => {
40
+ try {
41
+ return JSON.parse(line).type !== "session";
42
+ } catch {
43
+ return true;
44
+ }
45
+ });
46
+ }
47
+
48
+ export function seedSubagentSessionFile(params: {
49
+ mode: SeededSubagentSessionMode;
50
+ parentSessionFile: string;
51
+ childSessionFile: string;
52
+ childCwd: string;
53
+ }): void {
54
+ const header = {
55
+ type: "session",
56
+ version: 3,
57
+ id: randomUUID(),
58
+ timestamp: new Date().toISOString(),
59
+ cwd: params.childCwd,
60
+ parentSession: params.parentSessionFile,
61
+ };
62
+ const contentLines =
63
+ params.mode === "fork" ? getForkContentLines(params.parentSessionFile) : [];
64
+ const lines = [JSON.stringify(header), ...contentLines];
65
+
66
+ mkdirSync(dirname(params.childSessionFile), { recursive: true });
67
+ writeFileSync(params.childSessionFile, lines.join("\n") + "\n", "utf8");
68
+ }
69
+
70
+ function readEntries(sessionFile: string): SessionEntry[] {
71
+ const raw = readFileSync(sessionFile, "utf8");
72
+ return raw
73
+ .split("\n")
74
+ .filter((line) => line.trim())
75
+ .map((line) => JSON.parse(line) as SessionEntry);
76
+ }
77
+
78
+ /**
79
+ * Return the id of the last entry in the session file (current branch point / leaf).
80
+ */
81
+ export function getLeafId(sessionFile: string): string | null {
82
+ const entries = readEntries(sessionFile);
83
+ return entries.length > 0 ? entries[entries.length - 1].id : null;
84
+ }
85
+
86
+ /**
87
+ * Return entries added after `afterLine` (1-indexed count of existing entries).
88
+ */
89
+ export function getNewEntries(sessionFile: string, afterLine: number): SessionEntry[] {
90
+ const raw = readFileSync(sessionFile, "utf8");
91
+ const lines = raw.split("\n").filter((line) => line.trim());
92
+ return lines.slice(afterLine).map((line) => JSON.parse(line) as SessionEntry);
93
+ }
94
+
95
+ /**
96
+ * Find the last assistant message text in a list of entries.
97
+ *
98
+ * Falls back to the `errorMessage` field when the last assistant message has
99
+ * `stopReason: "error"` and no usable text content — this happens when
100
+ * auto-retry exhausts on a provider overload / rate limit / server error, and
101
+ * without this fallback the parent would silently see a stale earlier message.
102
+ */
103
+ export function findLastAssistantMessage(entries: SessionEntry[]): string | null {
104
+ for (let i = entries.length - 1; i >= 0; i--) {
105
+ const entry = entries[i];
106
+ if (entry.type !== "message") continue;
107
+ const msg = entry as MessageEntry;
108
+ if (msg.message.role !== "assistant") continue;
109
+
110
+ const texts = msg.message.content
111
+ .filter(
112
+ (block) =>
113
+ block.type === "text" && typeof block.text === "string" && block.text.trim() !== "",
114
+ )
115
+ .map((block) => block.text as string);
116
+
117
+ if (texts.length > 0 && texts.join("").trim()) return texts.join("\n");
118
+
119
+ const stopReason = (msg.message as { stopReason?: unknown }).stopReason;
120
+ const errorMessage = (msg.message as { errorMessage?: unknown }).errorMessage;
121
+ if (
122
+ stopReason === "error" &&
123
+ typeof errorMessage === "string" &&
124
+ errorMessage.trim() !== ""
125
+ ) {
126
+ return `Subagent error: ${errorMessage.trim()}`;
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+
132
+ /**
133
+ * Append a branch_summary entry to the session file.
134
+ * Returns the new entry's id.
135
+ */
136
+ export function appendBranchSummary(
137
+ sessionFile: string,
138
+ branchPointId: string,
139
+ fromId: string | null,
140
+ summary: string,
141
+ ): string {
142
+ const id = randomBytes(4).toString("hex");
143
+ const entry = {
144
+ type: "branch_summary",
145
+ id,
146
+ parentId: branchPointId,
147
+ timestamp: new Date().toISOString(),
148
+ fromId: fromId ?? branchPointId,
149
+ summary,
150
+ };
151
+ appendFileSync(sessionFile, JSON.stringify(entry) + "\n", "utf8");
152
+ return id;
153
+ }
154
+
155
+ /**
156
+ * Copy the session file to destDir for parallel worker isolation.
157
+ * Returns the path of the copy.
158
+ */
159
+ export function copySessionFile(sessionFile: string, destDir: string): string {
160
+ const id = randomBytes(4).toString("hex");
161
+ const dest = join(destDir, `subagent-${id}.jsonl`);
162
+ copyFileSync(sessionFile, dest);
163
+ return dest;
164
+ }
165
+
166
+ /**
167
+ * Read new entries from sourceFile (after afterLine), append them to targetFile.
168
+ * Returns the appended entries.
169
+ */
170
+ export function mergeNewEntries(
171
+ sourceFile: string,
172
+ targetFile: string,
173
+ afterLine: number,
174
+ ): SessionEntry[] {
175
+ const entries = getNewEntries(sourceFile, afterLine);
176
+ for (const entry of entries) {
177
+ appendFileSync(targetFile, JSON.stringify(entry) + "\n", "utf8");
178
+ }
179
+ return entries;
180
+ }