@dreki-gg/pi-subagent 0.6.0 → 0.8.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 (40) hide show
  1. package/.fallowrc.json +6 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +82 -70
  4. package/docs/orchestration-principles.md +132 -0
  5. package/docs/plans/01_structured-handoffs-and-synthesis.plan.md +376 -0
  6. package/docs/plans/02_clean-context-review-loop.plan.md +240 -0
  7. package/docs/plans/03_parallel-recon-single-writer-docs.plan.md +199 -0
  8. package/docs/plans/04_manager-workflow.plan.md +248 -0
  9. package/docs/plans/05_advisor-routing.plan.md +244 -0
  10. package/extensions/subagent/agent-result-utils.ts +12 -0
  11. package/extensions/subagent/agent-runner-types.ts +23 -0
  12. package/extensions/subagent/agent-runner.ts +90 -0
  13. package/extensions/subagent/agents.ts +31 -77
  14. package/extensions/subagent/handoffs.ts +273 -0
  15. package/extensions/subagent/index.ts +359 -581
  16. package/extensions/subagent/run-agent-args.ts +90 -0
  17. package/extensions/subagent/{delegate-executor.ts → spawn-utils.ts} +64 -87
  18. package/extensions/subagent/synthesis.ts +1 -51
  19. package/package.json +7 -10
  20. package/prompts/advisor.md +37 -0
  21. package/prompts/bug-prover.md +42 -0
  22. package/{agents → prompts}/docs-scout.md +2 -2
  23. package/prompts/manager.md +52 -0
  24. package/{agents → prompts}/planner.md +16 -1
  25. package/prompts/reviewer.md +93 -0
  26. package/{agents → prompts}/scout.md +5 -1
  27. package/prompts/validator.md +42 -0
  28. package/prompts/worker.md +61 -0
  29. package/skills/spawn-subagents/SKILL.md +80 -8
  30. package/skills/write-an-agent/SKILL.md +5 -5
  31. package/agents/reviewer.md +0 -37
  32. package/agents/worker.md +0 -26
  33. package/extensions/subagent/delegate-args.ts +0 -145
  34. package/extensions/subagent/delegate-types.ts +0 -62
  35. package/extensions/subagent/delegate-ui.ts +0 -132
  36. package/extensions/subagent/workflows.ts +0 -150
  37. package/prompts/implement-and-review.md +0 -10
  38. package/prompts/implement.md +0 -10
  39. package/prompts/scout-and-plan.md +0 -9
  40. /package/{agents → prompts}/ux-designer.md +0 -0
@@ -0,0 +1,273 @@
1
+ export interface HandoffFileRef {
2
+ path: string;
3
+ notes?: string;
4
+ }
5
+
6
+ export interface HandoffEnvelope {
7
+ version: 'subagent-handoff/v1';
8
+ sourceAgent: string;
9
+ sourceStep?: number;
10
+ task: string;
11
+ summary: string;
12
+ goal?: string;
13
+ decisions: string[];
14
+ constraints: string[];
15
+ files: HandoffFileRef[];
16
+ symbols: string[];
17
+ openQuestions: string[];
18
+ rawOutput: string;
19
+ }
20
+
21
+ export interface RenderHandoffOptions {
22
+ includeRawOutput?: boolean;
23
+ maxRawChars?: number;
24
+ }
25
+
26
+ const DEFAULT_MAX_RAW_CHARS = 4000;
27
+ const MAX_SUMMARY_ITEMS = 3;
28
+ const SECTION_RE = /^##\s+(.+)$/gm;
29
+ const CODE_SYMBOL_RE =
30
+ /^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)|^(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*[:=]|^(?:export\s+)?(?:interface|type|class|enum)\s+([A-Za-z_$][\w$]*)/gm;
31
+ const BULLET_SYMBOL_RE =
32
+ /^\s*(?:[-*]|\d+\.)\s+`?([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)`?(?=\s*(?:-|:|\())/gm;
33
+ const PATH_RE = /\b(?:\.?\.?\/)?(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+(?:\.[A-Za-z0-9_.-]+)?\b/g;
34
+ const CODE_PATH_RE = /`([^`\n]+(?:\/[A-Za-z0-9_.-]+)+[^`\n]*)`/g;
35
+
36
+ interface SectionEntry {
37
+ title: string;
38
+ body: string;
39
+ }
40
+
41
+ function normalizeWhitespace(value: string): string {
42
+ return value.replace(/\r/g, '').replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
43
+ }
44
+
45
+ function normalizeSectionTitle(title: string): string {
46
+ return title.trim().toLowerCase().replace(/[\s/]+/g, ' ').replace(/[():]/g, '');
47
+ }
48
+
49
+ function splitMarkdownSections(markdown: string): { preamble: string; sections: SectionEntry[] } {
50
+ const matches = Array.from(markdown.matchAll(SECTION_RE));
51
+ if (matches.length === 0) {
52
+ return { preamble: normalizeWhitespace(markdown), sections: [] };
53
+ }
54
+
55
+ const sections: SectionEntry[] = [];
56
+ const preamble = normalizeWhitespace(markdown.slice(0, matches[0]?.index ?? 0));
57
+
58
+ for (let i = 0; i < matches.length; i++) {
59
+ const current = matches[i];
60
+ const next = matches[i + 1];
61
+ const title = current[1]?.trim() ?? '';
62
+ const start = (current.index ?? 0) + current[0].length;
63
+ const end = next?.index ?? markdown.length;
64
+ const body = normalizeWhitespace(markdown.slice(start, end));
65
+ sections.push({ title, body });
66
+ }
67
+
68
+ return { preamble, sections };
69
+ }
70
+
71
+ function getSectionBodies(sections: SectionEntry[], titles: string[]): string[] {
72
+ const wanted = new Set(titles.map(normalizeSectionTitle));
73
+ return sections
74
+ .filter((section) => wanted.has(normalizeSectionTitle(section.title)))
75
+ .map((section) => section.body)
76
+ .filter(Boolean);
77
+ }
78
+
79
+ function getFirstSectionBody(sections: SectionEntry[], titles: string[]): string | undefined {
80
+ return getSectionBodies(sections, titles)[0];
81
+ }
82
+
83
+ function extractListItems(section: string | undefined): string[] {
84
+ if (!section) return [];
85
+
86
+ const matches = Array.from(section.matchAll(/^\s*(?:[-*]|\d+\.)\s+(.+)$/gm));
87
+ if (matches.length === 0) return [];
88
+
89
+ return matches
90
+ .map((match) => normalizeWhitespace(match[1] ?? ''))
91
+ .map((item) => item.replace(/^[`*_]+|[`*_]+$/g, '').trim())
92
+ .filter(Boolean);
93
+ }
94
+
95
+ function extractParagraph(section: string | undefined): string | undefined {
96
+ if (!section) return undefined;
97
+ const paragraph = normalizeWhitespace(section.split(/\n\n+/)[0] ?? '');
98
+ const normalized = paragraph.replace(/^[-*]\s+/, '').trim();
99
+ return normalized && !isSentinelItem(normalized) ? paragraph : undefined;
100
+ }
101
+
102
+ function truncate(value: string, maxChars: number): string {
103
+ if (value.length <= maxChars) return value;
104
+ return `${value.slice(0, Math.max(0, maxChars - 14)).trimEnd()}\n… (truncated)`;
105
+ }
106
+
107
+ function isSentinelItem(value: string): boolean {
108
+ return /^(?:none|n\/a|not applicable)$/i.test(value.trim());
109
+ }
110
+
111
+ function uniq(values: string[]): string[] {
112
+ return Array.from(
113
+ new Set(values.map((value) => value.trim()).filter((value) => value && !isSentinelItem(value))),
114
+ );
115
+ }
116
+
117
+ function summarizeBullets(items: string[]): string | undefined {
118
+ const filtered = items.filter((item) => !isSentinelItem(item));
119
+ if (filtered.length === 0) return undefined;
120
+ return filtered.slice(0, MAX_SUMMARY_ITEMS).join('; ');
121
+ }
122
+
123
+ function summarizeSection(section: string | undefined): string | undefined {
124
+ if (!section) return undefined;
125
+ return summarizeBullets(extractListItems(section)) ?? extractParagraph(section);
126
+ }
127
+
128
+ function buildSummary(preamble: string, sections: SectionEntry[]): string {
129
+ const parts = [
130
+ summarizeSection(getFirstSectionBody(sections, ['Goal', 'Completed'])),
131
+ summarizeSection(getFirstSectionBody(sections, ['Plan', 'Architecture', 'Integration Notes'])),
132
+ summarizeSection(getFirstSectionBody(sections, ['Recommended Next Step', 'Notes', 'Risks'])),
133
+ extractParagraph(preamble),
134
+ ].filter((part): part is string => Boolean(part));
135
+
136
+ return parts.length > 0 ? parts.slice(0, MAX_SUMMARY_ITEMS).join(' ') : '(no summary available)';
137
+ }
138
+
139
+ function extractPaths(text: string): HandoffFileRef[] {
140
+ const seen = new Set<string>();
141
+ const files: HandoffFileRef[] = [];
142
+
143
+ const addPath = (rawPath: string, notes?: string) => {
144
+ const path = rawPath.trim().replace(/^`|`$/g, '').replace(/[),.:;]+$/g, '');
145
+ if (!path.includes('/')) return;
146
+ if (path.startsWith('http://') || path.startsWith('https://')) return;
147
+ if (seen.has(path)) return;
148
+ seen.add(path);
149
+ files.push({ path, notes });
150
+ };
151
+
152
+ for (const match of text.matchAll(CODE_PATH_RE)) {
153
+ addPath(match[1] ?? '');
154
+ }
155
+
156
+ for (const match of text.matchAll(PATH_RE)) {
157
+ addPath(match[0] ?? '');
158
+ }
159
+
160
+ return files;
161
+ }
162
+
163
+ function extractSymbols(text: string): string[] {
164
+ const symbols: string[] = [];
165
+
166
+ for (const match of text.matchAll(CODE_SYMBOL_RE)) {
167
+ const symbol = match[1] ?? match[2] ?? match[3];
168
+ if (symbol) symbols.push(symbol);
169
+ }
170
+
171
+ for (const match of text.matchAll(BULLET_SYMBOL_RE)) {
172
+ const symbol = match[1];
173
+ if (symbol && !symbol.includes('/')) symbols.push(symbol);
174
+ }
175
+
176
+ return uniq(symbols);
177
+ }
178
+
179
+ function toQuestions(items: string[]): string[] {
180
+ return items.filter(
181
+ (item) =>
182
+ /\?$/.test(item) || /\b(?:unknown|unclear|unresolved|question|follow up|todo)\b/i.test(item),
183
+ );
184
+ }
185
+
186
+ function renderList(title: string, items: string[]): string[] {
187
+ if (items.length === 0) return [];
188
+ return [`## ${title}`, ...items.map((item) => `- ${item}`), ''];
189
+ }
190
+
191
+ export function buildHandoffFromResult(input: {
192
+ agent: string;
193
+ step?: number;
194
+ task: string;
195
+ output: string;
196
+ }): HandoffEnvelope {
197
+ const output = input.output.trim() || '(no output)';
198
+ const { preamble, sections } = splitMarkdownSections(output);
199
+ const decisions = uniq(extractListItems(getFirstSectionBody(sections, ['Decisions'])));
200
+ const explicitConstraints = uniq(
201
+ extractListItems(getFirstSectionBody(sections, ['Constraints', 'Constraints or Unknowns'])),
202
+ );
203
+ const riskItems = uniq(extractListItems(getFirstSectionBody(sections, ['Risks'])));
204
+ const openQuestions = uniq([
205
+ ...extractListItems(getFirstSectionBody(sections, ['Open Questions'])),
206
+ ...toQuestions(extractListItems(getFirstSectionBody(sections, ['Notes', 'Constraints or Unknowns']))),
207
+ ]);
208
+ const goal =
209
+ extractParagraph(getFirstSectionBody(sections, ['Goal'])) ??
210
+ extractParagraph(getFirstSectionBody(sections, ['Completed'])) ??
211
+ undefined;
212
+
213
+ return {
214
+ version: 'subagent-handoff/v1',
215
+ sourceAgent: input.agent,
216
+ sourceStep: input.step,
217
+ task: input.task,
218
+ summary: buildSummary(preamble, sections),
219
+ goal,
220
+ decisions,
221
+ constraints: uniq([...explicitConstraints, ...riskItems]),
222
+ files: extractPaths(output),
223
+ symbols: extractSymbols(output),
224
+ openQuestions,
225
+ rawOutput: output,
226
+ };
227
+ }
228
+
229
+ export function renderHandoffForPrompt(
230
+ handoff: HandoffEnvelope,
231
+ options: RenderHandoffOptions = {},
232
+ ): string {
233
+ const includeRawOutput = options.includeRawOutput ?? true;
234
+ const maxRawChars = options.maxRawChars ?? DEFAULT_MAX_RAW_CHARS;
235
+ const lines: string[] = [
236
+ '## Previous Agent Handoff',
237
+ `- Source Agent: ${handoff.sourceAgent}`,
238
+ ...(handoff.sourceStep ? [`- Step: ${handoff.sourceStep}`] : []),
239
+ `- Task: ${truncate(handoff.task, 240)}`,
240
+ `- Summary: ${handoff.summary}`,
241
+ '',
242
+ ];
243
+
244
+ if (handoff.goal) {
245
+ lines.push('## Goal', handoff.goal, '');
246
+ }
247
+
248
+ lines.push(...renderList('Decisions', handoff.decisions));
249
+ lines.push(...renderList('Constraints', handoff.constraints));
250
+
251
+ if (handoff.files.length > 0) {
252
+ lines.push('## Files');
253
+ for (const file of handoff.files) {
254
+ lines.push(file.notes ? `- \`${file.path}\` - ${file.notes}` : `- \`${file.path}\``);
255
+ }
256
+ lines.push('');
257
+ }
258
+
259
+ lines.push(...renderList('Symbols', handoff.symbols));
260
+ lines.push(...renderList('Open Questions', handoff.openQuestions));
261
+
262
+ if (includeRawOutput) {
263
+ lines.push(
264
+ '## Raw Output (truncated)',
265
+ '```markdown',
266
+ truncate(handoff.rawOutput, maxRawChars),
267
+ '```',
268
+ '',
269
+ );
270
+ }
271
+
272
+ return lines.join('\n').trim();
273
+ }