pi-herdr-agents 0.0.1

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 (49) hide show
  1. package/AGENTS.md +116 -0
  2. package/CONTEXT.md +159 -0
  3. package/LICENSE +21 -0
  4. package/README.md +874 -0
  5. package/RELEASING.md +139 -0
  6. package/agents/adversarial-reviewer.md +80 -0
  7. package/agents/claude-reviewer.md +23 -0
  8. package/agents/planner.md +539 -0
  9. package/agents/poteto.md +32 -0
  10. package/agents/reviewer.md +164 -0
  11. package/agents/scout.md +106 -0
  12. package/agents/visual-tester.md +224 -0
  13. package/agents/worker.md +132 -0
  14. package/config.json.example +8 -0
  15. package/docs/README.md +42 -0
  16. package/docs/adr/0001-btw-ephemeral-side-questions.md +142 -0
  17. package/docs/adr/0002-agent-workflow-skill-runtime-taxonomy.md +265 -0
  18. package/docs/adr/0003-installable-role-packs.md +135 -0
  19. package/docs/adr/0004-require-active-user-approval-for-workflow-execution.md +17 -0
  20. package/docs/adr/0005-parent-owns-workflow-script-authority.md +17 -0
  21. package/docs/adr/0006-limit-v1-execution-effects-to-isolated-worktrees.md +18 -0
  22. package/docs/adr/0007-require-fresh-review-for-workflow-scripts.md +19 -0
  23. package/docs/orchestrated-review-workflow-plan.md +479 -0
  24. package/docs/research/pdw-architecture-assessment.md +525 -0
  25. package/docs/research/pi-workflows-sol-advisor.md +255 -0
  26. package/docs/research/worktree-subagent-orchestration.md +317 -0
  27. package/docs/worktree-subagents.md +196 -0
  28. package/examples/role-pack/extension.ts +18 -0
  29. package/examples/role-pack/package.json +16 -0
  30. package/examples/role-pack/roles/example-reviewer.md +12 -0
  31. package/package.json +58 -0
  32. package/pi-extension/subagents/activity.ts +511 -0
  33. package/pi-extension/subagents/completion.ts +177 -0
  34. package/pi-extension/subagents/herdr.ts +541 -0
  35. package/pi-extension/subagents/index.ts +4730 -0
  36. package/pi-extension/subagents/lifecycle.ts +477 -0
  37. package/pi-extension/subagents/model-config.ts +95 -0
  38. package/pi-extension/subagents/plan-skill.md +262 -0
  39. package/pi-extension/subagents/plugin/.claude-plugin/plugin.json +5 -0
  40. package/pi-extension/subagents/plugin/hooks/hooks.json +15 -0
  41. package/pi-extension/subagents/plugin/hooks/on-stop.sh +68 -0
  42. package/pi-extension/subagents/runtime-routing.ts +313 -0
  43. package/pi-extension/subagents/session.ts +216 -0
  44. package/pi-extension/subagents/status.ts +513 -0
  45. package/pi-extension/subagents/subagent-done.ts +326 -0
  46. package/pi-extension/subagents/terminal.ts +163 -0
  47. package/pi-extension/subagents/workflow-worker.js +56 -0
  48. package/pi-extension/subagents/workflow.ts +1210 -0
  49. package/skills/orchestrate/SKILL.md +184 -0
@@ -0,0 +1,216 @@
1
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
2
+ import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { randomBytes, randomUUID } from "node:crypto";
4
+ import { dirname, join } from "node:path";
5
+
6
+ export interface SessionEntry {
7
+ type: string;
8
+ id: string;
9
+ parentId?: string;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ export interface MessageEntry extends SessionEntry {
14
+ type: "message";
15
+ message: {
16
+ role: "user" | "assistant" | "toolResult";
17
+ content: Array<{ type: string; text?: string; [key: string]: unknown }>;
18
+ };
19
+ }
20
+
21
+ export type SeededSubagentSessionMode = "lineage-only" | "fork";
22
+
23
+ function getForkContentLines(parentSessionFile: string): string[] {
24
+ const raw = readFileSync(parentSessionFile, "utf8");
25
+ const lines = raw.split("\n").filter((line) => line.trim());
26
+
27
+ let truncateAt = lines.length;
28
+ for (let i = lines.length - 1; i >= 0; i--) {
29
+ try {
30
+ const entry = JSON.parse(lines[i]);
31
+ if (entry.type === "message" && entry.message?.role === "user") {
32
+ truncateAt = i;
33
+ break;
34
+ }
35
+ } catch {
36
+ // ignore malformed lines
37
+ }
38
+ }
39
+
40
+ return lines.slice(0, truncateAt).filter((line) => {
41
+ try {
42
+ return JSON.parse(line).type !== "session";
43
+ } catch {
44
+ return true;
45
+ }
46
+ });
47
+ }
48
+
49
+ export function createBtwSessionSnapshot(
50
+ parentSessionFile: string,
51
+ leafId: string,
52
+ ): string {
53
+ const detached = SessionManager.open(parentSessionFile);
54
+ const childSessionFile = detached.createBranchedSession(leafId);
55
+ if (!childSessionFile || !existsSync(childSessionFile)) {
56
+ throw new Error("Pi did not persist the BTW child session");
57
+ }
58
+ return childSessionFile;
59
+ }
60
+
61
+ export function seedSubagentSessionFile(params: {
62
+ mode: SeededSubagentSessionMode;
63
+ parentSessionFile: string;
64
+ childSessionFile: string;
65
+ childCwd: string;
66
+ }): void {
67
+ const header = {
68
+ type: "session",
69
+ version: 3,
70
+ id: randomUUID(),
71
+ timestamp: new Date().toISOString(),
72
+ cwd: params.childCwd,
73
+ parentSession: params.parentSessionFile,
74
+ };
75
+ const contentLines =
76
+ params.mode === "fork" ? getForkContentLines(params.parentSessionFile) : [];
77
+ const lines = [JSON.stringify(header), ...contentLines];
78
+
79
+ mkdirSync(dirname(params.childSessionFile), { recursive: true });
80
+ writeFileSync(params.childSessionFile, lines.join("\n") + "\n", "utf8");
81
+ }
82
+
83
+ function readEntries(sessionFile: string): SessionEntry[] {
84
+ const raw = readFileSync(sessionFile, "utf8");
85
+ return raw
86
+ .split("\n")
87
+ .filter((line) => line.trim())
88
+ .map((line) => JSON.parse(line) as SessionEntry);
89
+ }
90
+
91
+ /**
92
+ * Return the id of the last entry in the session file (current branch point / leaf).
93
+ */
94
+ export function getLeafId(sessionFile: string): string | null {
95
+ const entries = readEntries(sessionFile);
96
+ return entries.length > 0 ? entries[entries.length - 1].id : null;
97
+ }
98
+
99
+ /**
100
+ * Return entries added after `afterLine` (1-indexed count of existing entries).
101
+ */
102
+ export function getNewEntries(sessionFile: string, afterLine: number): SessionEntry[] {
103
+ const raw = readFileSync(sessionFile, "utf8");
104
+ const lines = raw.split("\n").filter((line) => line.trim());
105
+ return lines.slice(afterLine).map((line) => JSON.parse(line) as SessionEntry);
106
+ }
107
+
108
+ /**
109
+ * Find the last assistant message text in a list of entries.
110
+ *
111
+ * Falls back to the `errorMessage` field when the last assistant message has
112
+ * `stopReason: "error"` and no usable text content — this happens when
113
+ * auto-retry exhausts on a provider overload / rate limit / server error, and
114
+ * without this fallback the parent would silently see a stale earlier message.
115
+ */
116
+ export interface ObservedSessionRuntime {
117
+ provider?: string;
118
+ modelId?: string;
119
+ thinking?: string;
120
+ }
121
+
122
+ /** Read the effective model and thinking entries recorded by Pi at session startup. */
123
+ export function findObservedSessionRuntime(entries: SessionEntry[]): ObservedSessionRuntime {
124
+ const observed: ObservedSessionRuntime = {};
125
+ for (const entry of entries) {
126
+ if (entry.type === "model_change") {
127
+ if (typeof entry.provider === "string") observed.provider = entry.provider;
128
+ if (typeof entry.modelId === "string") observed.modelId = entry.modelId;
129
+ } else if (
130
+ entry.type === "thinking_level_change" &&
131
+ typeof entry.thinkingLevel === "string"
132
+ ) {
133
+ observed.thinking = entry.thinkingLevel;
134
+ }
135
+ }
136
+ return observed;
137
+ }
138
+
139
+ export function findLastAssistantMessage(entries: SessionEntry[]): string | null {
140
+ for (let i = entries.length - 1; i >= 0; i--) {
141
+ const entry = entries[i];
142
+ if (entry.type !== "message") continue;
143
+ const msg = entry as MessageEntry;
144
+ if (msg.message.role !== "assistant") continue;
145
+
146
+ const texts = msg.message.content
147
+ .filter(
148
+ (block) =>
149
+ block.type === "text" && typeof block.text === "string" && block.text.trim() !== "",
150
+ )
151
+ .map((block) => block.text as string);
152
+
153
+ if (texts.length > 0 && texts.join("").trim()) return texts.join("\n");
154
+
155
+ const stopReason = (msg.message as { stopReason?: unknown }).stopReason;
156
+ const errorMessage = (msg.message as { errorMessage?: unknown }).errorMessage;
157
+ if (
158
+ stopReason === "error" &&
159
+ typeof errorMessage === "string" &&
160
+ errorMessage.trim() !== ""
161
+ ) {
162
+ return `Subagent error: ${errorMessage.trim()}`;
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+
168
+ /**
169
+ * Append a branch_summary entry to the session file.
170
+ * Returns the new entry's id.
171
+ */
172
+ export function appendBranchSummary(
173
+ sessionFile: string,
174
+ branchPointId: string,
175
+ fromId: string | null,
176
+ summary: string,
177
+ ): string {
178
+ const id = randomBytes(4).toString("hex");
179
+ const entry = {
180
+ type: "branch_summary",
181
+ id,
182
+ parentId: branchPointId,
183
+ timestamp: new Date().toISOString(),
184
+ fromId: fromId ?? branchPointId,
185
+ summary,
186
+ };
187
+ appendFileSync(sessionFile, JSON.stringify(entry) + "\n", "utf8");
188
+ return id;
189
+ }
190
+
191
+ /**
192
+ * Copy the session file to destDir for parallel worker isolation.
193
+ * Returns the path of the copy.
194
+ */
195
+ export function copySessionFile(sessionFile: string, destDir: string): string {
196
+ const id = randomBytes(4).toString("hex");
197
+ const dest = join(destDir, `subagent-${id}.jsonl`);
198
+ copyFileSync(sessionFile, dest);
199
+ return dest;
200
+ }
201
+
202
+ /**
203
+ * Read new entries from sourceFile (after afterLine), append them to targetFile.
204
+ * Returns the appended entries.
205
+ */
206
+ export function mergeNewEntries(
207
+ sourceFile: string,
208
+ targetFile: string,
209
+ afterLine: number,
210
+ ): SessionEntry[] {
211
+ const entries = getNewEntries(sourceFile, afterLine);
212
+ for (const entry of entries) {
213
+ appendFileSync(targetFile, JSON.stringify(entry) + "\n", "utf8");
214
+ }
215
+ return entries;
216
+ }