pi-crew 0.5.5 → 0.5.6

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 (72) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +17 -1
  3. package/docs/architecture.md +2 -0
  4. package/docs/migration-v0.4-v0.5.md +19 -2
  5. package/docs/pi-crew-v0.5.5-audit-fix-plan.md +133 -0
  6. package/package.json +7 -5
  7. package/src/benchmark/benchmark-runner.ts +45 -0
  8. package/src/benchmark/feedback-loop.ts +5 -0
  9. package/src/config/config.ts +10 -0
  10. package/src/config/suggestions.ts +8 -0
  11. package/src/extension/async-notifier.ts +10 -1
  12. package/src/extension/cross-extension-rpc.ts +1 -1
  13. package/src/extension/notification-router.ts +18 -0
  14. package/src/extension/register.ts +13 -17
  15. package/src/extension/registration/subagent-tools.ts +1 -1
  16. package/src/extension/team-tool/anchor.ts +201 -0
  17. package/src/extension/team-tool/api.ts +2 -1
  18. package/src/extension/team-tool/auto-summarize.ts +154 -0
  19. package/src/extension/team-tool/run.ts +37 -2
  20. package/src/extension/team-tool.ts +44 -2
  21. package/src/hooks/registry.ts +1 -3
  22. package/src/observability/event-bus.ts +13 -4
  23. package/src/observability/event-to-metric.ts +0 -2
  24. package/src/runtime/anchor-manager.ts +473 -0
  25. package/src/runtime/async-runner.ts +8 -4
  26. package/src/runtime/auto-summarize.ts +350 -0
  27. package/src/runtime/background-runner.ts +2 -1
  28. package/src/runtime/budget-tracker.ts +354 -0
  29. package/src/runtime/chain-runner.ts +507 -0
  30. package/src/runtime/child-pi.ts +1 -1
  31. package/src/runtime/crash-recovery.ts +5 -4
  32. package/src/runtime/custom-tools/irc-tool.ts +13 -0
  33. package/src/runtime/custom-tools/submit-result-tool.ts +3 -2
  34. package/src/runtime/delivery-coordinator.ts +10 -3
  35. package/src/runtime/dynamic-script-runner.ts +482 -0
  36. package/src/runtime/handoff-manager.ts +589 -0
  37. package/src/runtime/hidden-handoff.ts +424 -0
  38. package/src/runtime/live-agent-manager.ts +20 -4
  39. package/src/runtime/live-session-runtime.ts +39 -4
  40. package/src/runtime/manifest-cache.ts +2 -1
  41. package/src/runtime/model-resolver.ts +16 -4
  42. package/src/runtime/phase-tracker.ts +373 -0
  43. package/src/runtime/pipeline-runner.ts +514 -0
  44. package/src/runtime/retry-runner.ts +354 -0
  45. package/src/runtime/sandbox.ts +252 -0
  46. package/src/runtime/scheduler.ts +7 -2
  47. package/src/runtime/subagent-manager.ts +1 -1
  48. package/src/runtime/task-graph.ts +11 -1
  49. package/src/runtime/task-runner.ts +1 -1
  50. package/src/runtime/team-runner.ts +4 -3
  51. package/src/schema/team-tool-schema.ts +30 -0
  52. package/src/skills/discover-skills.ts +5 -0
  53. package/src/state/active-run-registry.ts +9 -2
  54. package/src/state/contracts.ts +9 -0
  55. package/src/state/crew-init.ts +3 -3
  56. package/src/state/decision-ledger.ts +26 -32
  57. package/src/state/event-log-rotation.ts +2 -2
  58. package/src/state/event-log.ts +9 -1
  59. package/src/state/mailbox.ts +10 -0
  60. package/src/state/run-cache.ts +18 -8
  61. package/src/tools/safe-bash-extension.ts +1 -0
  62. package/src/tools/safe-bash.ts +152 -20
  63. package/src/ui/overlays/mailbox-detail-overlay.ts +13 -2
  64. package/src/ui/powerbar-publisher.ts +1 -0
  65. package/src/ui/transcript-cache.ts +13 -0
  66. package/src/utils/bm25-search.ts +16 -8
  67. package/src/utils/env-filter.ts +8 -5
  68. package/src/utils/redaction.ts +169 -15
  69. package/src/utils/sse-parser.ts +10 -1
  70. package/src/worktree/cleanup.ts +6 -1
  71. package/workflows/chain.workflow.md +252 -0
  72. package/workflows/pipeline.workflow.md +27 -0
@@ -20,6 +20,8 @@ export class MailboxDetailOverlay {
20
20
  private side: "inbox" | "outbox" = "inbox";
21
21
  private selected = 0;
22
22
  private expanded = false;
23
+ private lastRefreshedTaskCount = 0;
24
+ private needsRefresh = true;
23
25
 
24
26
  constructor(opts: { runId: string; cwd: string; done: (action: MailboxAction | undefined) => void; theme?: unknown }) {
25
27
  this.runId = opts.runId;
@@ -32,6 +34,12 @@ export class MailboxDetailOverlay {
32
34
  private refresh(): void {
33
35
  const loaded = loadRunManifestById(this.cwd, this.runId);
34
36
  if (!loaded) return;
37
+ // Track task count changes to trigger re-render
38
+ const taskCount = loaded.tasks.length;
39
+ if (taskCount !== this.lastRefreshedTaskCount) {
40
+ this.lastRefreshedTaskCount = taskCount;
41
+ this.needsRefresh = true;
42
+ }
35
43
  const delivery = readDeliveryState(loaded.manifest).messages;
36
44
  const applyDelivery = (message: MailboxMessage): MailboxMessage => ({ ...message, status: delivery[message.id] ?? message.status });
37
45
  const taskIds = loaded.tasks.map((task) => task.id);
@@ -49,11 +57,14 @@ export class MailboxDetailOverlay {
49
57
  }
50
58
 
51
59
  invalidate(): void {
52
- this.refresh();
60
+ this.needsRefresh = true;
53
61
  }
54
62
 
55
63
  render(width: number): string[] {
56
- this.refresh();
64
+ if (this.needsRefresh) {
65
+ this.refresh();
66
+ this.needsRefresh = false;
67
+ }
57
68
  const inner = Math.max(40, width - 4);
58
69
  const col = Math.max(18, Math.floor((inner - 3) / 2));
59
70
  const lines = [
@@ -294,6 +294,7 @@ export function requestPowerbarUpdate(
294
294
 
295
295
  /** Dispose the powerbar coalescer. Call during extension cleanup. */
296
296
  export function disposePowerbarCoalescer(): void {
297
+ powerbarCoalescer.flush();
297
298
  powerbarCoalescer.dispose();
298
299
  }
299
300
 
@@ -19,6 +19,7 @@ export interface TranscriptReadOptions {
19
19
 
20
20
  const TRANSCRIPT_CACHE_TTL_MS = 500;
21
21
  const DEFAULT_TAIL_BYTES = 256 * 1024;
22
+ const MAX_CACHE_SIZE = 100;
22
23
  const transcriptCache = new Map<string, TranscriptCacheEntry>();
23
24
 
24
25
  function cacheKey(path: string, options: Required<Pick<TranscriptReadOptions, "full">> & { maxTailBytes: number }): string {
@@ -85,6 +86,18 @@ export function readTranscriptLinesCached(path: string, parse: (text: string) =>
85
86
  truncated: read.truncated,
86
87
  };
87
88
  transcriptCache.set(key, entry);
89
+ // Evict oldest entry if cache exceeds max size
90
+ if (transcriptCache.size > MAX_CACHE_SIZE) {
91
+ let oldestKey: string | null = null;
92
+ let oldestParsedAt = Infinity;
93
+ for (const [k, v] of transcriptCache.entries()) {
94
+ if (v.parsedAt < oldestParsedAt) {
95
+ oldestParsedAt = v.parsedAt;
96
+ oldestKey = k;
97
+ }
98
+ }
99
+ if (oldestKey) transcriptCache.delete(oldestKey);
100
+ }
88
101
  return lines;
89
102
  } catch {
90
103
  return previous?.lines ?? [];
@@ -46,17 +46,17 @@ export class BM25Search<T extends SearchDocument> {
46
46
  }
47
47
 
48
48
  /**
49
- * Compute document frequency for a query term using substring matching,
50
- * consistent with the regex-based tf computation in search().
49
+ * Compute document frequency for a query term using indexOf for better performance.
50
+ * Uses linear-time substring matching instead of regex to avoid ReDoS.
51
51
  */
52
52
  private df(term: string): number {
53
- const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
54
- const re = new RegExp(escaped, "g");
53
+ const termLower = term.toLowerCase();
55
54
  let count = 0;
56
55
  for (const doc of this.documents) {
57
56
  for (const field of Object.keys(this.fieldWeights)) {
58
57
  const text = (doc.fields[field] ?? "").toLowerCase();
59
- if (re.test(text)) {
58
+ // Use indexOf for linear-time substring search
59
+ if (text.includes(termLower)) {
60
60
  count++;
61
61
  break;
62
62
  }
@@ -81,11 +81,19 @@ export class BM25Search<T extends SearchDocument> {
81
81
  let fieldScore = 0;
82
82
 
83
83
  for (const term of queryTerms) {
84
- const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
85
- const tf = (textLower.match(new RegExp(escaped, "g")) ?? []).length;
84
+ // Use indexOf for linear-time substring counting instead of regex
85
+ const termLower = term.toLowerCase();
86
+ let tf = 0;
87
+ let pos = 0;
88
+ while ((pos = textLower.indexOf(termLower, pos)) !== -1) {
89
+ tf++;
90
+ pos += termLower.length;
91
+ // Cap tf to prevent runaway on repeated patterns
92
+ if (tf > 100) break;
93
+ }
86
94
  if (tf === 0) continue;
87
95
 
88
- const df = this.df(term);
96
+ const df = this.df(termLower);
89
97
  if (df === 0) continue;
90
98
 
91
99
  const idf = Math.log((this.N - df + 0.5) / (df + 0.5) + 1);
@@ -1,4 +1,4 @@
1
- import { SECRET_KEY_PATTERN } from "./redaction.ts";
1
+ import { isSecretKey } from "./redaction.ts";
2
2
 
3
3
  export interface SanitizeEnvOptions {
4
4
  /** Allow-list of env var names to preserve. Supports trailing glob, e.g. `"PI_*"`. */
@@ -8,14 +8,17 @@ export interface SanitizeEnvOptions {
8
8
  /**
9
9
  * Strip env vars whose keys look like secrets before passing to child processes.
10
10
  *
11
- * Default mode (no allowList): deny-list using SECRET_KEY_PATTERN.
11
+ * Default mode (no allowList): deny-list using isSecretKey.
12
12
  * When allowList is provided, only keys matching the allow-list are preserved.
13
13
  */
14
14
  export function sanitizeEnvSecrets(env: NodeJS.ProcessEnv, options?: SanitizeEnvOptions): Record<string, string> {
15
15
  const filtered: Record<string, string> = {};
16
16
  if (options?.allowList && options.allowList.length > 0) {
17
17
  const matchers = options.allowList.map((p) => {
18
- if (p.endsWith("*")) return (k: string) => k.startsWith(p.slice(0, -1));
18
+ if (p.endsWith("*")) {
19
+ const prefix = p.slice(0, -1);
20
+ return (k: string) => k.startsWith(prefix) && k.length > prefix.length;
21
+ }
19
22
  return (k: string) => k === p;
20
23
  });
21
24
  for (const [key, value] of Object.entries(env)) {
@@ -24,7 +27,7 @@ export function sanitizeEnvSecrets(env: NodeJS.ProcessEnv, options?: SanitizeEnv
24
27
  return filtered;
25
28
  }
26
29
  for (const [key, value] of Object.entries(env)) {
27
- if (value !== undefined && !SECRET_KEY_PATTERN.test(key)) filtered[key] = value;
30
+ if (value !== undefined && !isSecretKey(key)) filtered[key] = value;
28
31
  }
29
32
  return filtered;
30
- }
33
+ }
@@ -1,26 +1,180 @@
1
- export const SECRET_KEY_PATTERN = /(?:^|[_.-])(token|api[-_]?key|password|passwd|secret|credential|authorization|private[-_]?key)(?:$|[_.-])/i;
2
- const INLINE_SECRET_PATTERN = /(^|[\s,{])(([A-Za-z0-9_.-]*(?:api[-_]?key|token|password|passwd|secret|credential|authorization|private[-_]?key)[A-Za-z0-9_.-]*)\s*[=:]\s*)([^\s,;"'}]+)/gi;
3
- const AUTH_HEADER_PATTERN = /\b(Authorization\s*:\s*(?:Bearer|Basic|Token)?\s*)([^\r\n]+)/gi;
4
- const BEARER_PATTERN = /\b(Bearer\s+)([A-Za-z0-9._~+/=-]{8,})\b/g;
5
- const PEM_PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]{0,65536}?-----END [A-Z ]*PRIVATE KEY-----/g;
1
+ /**
2
+ * ReDoS-resistant pattern matching for secret detection.
3
+ * Uses linear-time scan instead of complex regex to prevent catastrophic backtracking.
4
+ */
5
+
6
+ // Pattern for PEM private keys (possessive quantifier prevents backtracking)
7
+ export const PEM_PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+PRIVATE KEY-----/g;
8
+
9
+ // Linear-time secret key detection
10
+ export function isSecretKey(keyName: string): boolean {
11
+ // Fast path: common secret key names
12
+ const lower = keyName.toLowerCase();
13
+ if (/^(token|apikey|api_key|password|secret|credential|authorization|privatekey|private_key)$/.test(lower)) {
14
+ return true;
15
+ }
16
+ // Linear scan for prefix characters followed by keywords
17
+ const prefixes = "_.-";
18
+ const keywords = ["token", "api", "key", "password", "passwd", "secret", "credential", "authorization", "private"];
19
+
20
+ for (let i = 0; i < keyName.length; i++) {
21
+ if (prefixes.includes(keyName[i])) {
22
+ const remaining = keyName.substring(i + 1).toLowerCase();
23
+ for (const kw of keywords) {
24
+ if (remaining.startsWith(kw)) {
25
+ const afterKw = remaining.substring(kw.length);
26
+ if (afterKw === "" || prefixes.includes(afterKw[0]) || /[a-zA-Z0-9]/.test(afterKw[0])) {
27
+ return true;
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ return false;
34
+ }
35
+
36
+ // Linear-time Authorization header redaction
37
+ export function redactAuthHeader(line: string): string {
38
+ const lower = line.toLowerCase();
39
+ const authIdx = lower.indexOf("authorization:");
40
+ if (authIdx === -1) return line;
41
+
42
+ // Verify word boundary - must be at start of line or preceded by whitespace/comma/brace
43
+ if (authIdx > 0) {
44
+ const before = line[authIdx - 1];
45
+ if (before !== ' ' && before !== ',' && before !== '{' && before !== '[' && before !== '"' && before !== '\r' && before !== '\n') {
46
+ return line; // Not a word boundary
47
+ }
48
+ }
49
+
50
+ // Check if this is followed by Bearer token (don't redact Bearer tokens separately)
51
+ // Look for "Bearer" after "authorization:"
52
+ const afterAuth = lower.substring(authIdx + 14).trimStart();
53
+ if (!afterAuth.startsWith('bearer ')) {
54
+ // No Bearer token, this is a regular Authorization header - redact it
55
+ let end = authIdx + 14;
56
+ while (end < line.length && line[end] !== "\r" && line[end] !== "\n") {
57
+ end++;
58
+ }
59
+ return line.substring(0, end) + " ***" + (end < line.length ? line.substring(end) : "");
60
+ }
61
+
62
+ // It's a Bearer token format - don't redact here, let redactBearerTokens handle it
63
+ return line;
64
+ }
65
+
66
+ // Linear-time Bearer token redaction
67
+ export function redactBearerTokens(line: string): string {
68
+ const upper = line.toUpperCase();
69
+ const result: string[] = [];
70
+ let i = 0;
71
+
72
+ while (i < line.length) {
73
+ if (upper.startsWith("BEARER ", i)) {
74
+ // Check word boundary: preceded by start, space, comma, brace, or newline
75
+ if (i > 0) {
76
+ const before = line[i - 1];
77
+ if (before !== ' ' && before !== ',' && before !== '{' && before !== '[' && before !== '"' && before !== '\r' && before !== '\n') {
78
+ result.push(line[i]);
79
+ i++;
80
+ continue;
81
+ }
82
+ }
83
+
84
+ // Found "Bearer " - now find the token
85
+ const bearerPrefix = line.substring(i, i + 7); // "Bearer "
86
+ let j = i + 7;
87
+ let tokenLen = 0;
88
+ while (j < line.length && tokenLen < 200 && /[A-Za-z0-9._~+/-]/.test(line[j])) {
89
+ j++;
90
+ tokenLen++;
91
+ }
92
+
93
+ if (tokenLen >= 8) {
94
+ // Replace with Bearer + *** (redact the token)
95
+ result.push(bearerPrefix + "***");
96
+ i = j;
97
+ continue;
98
+ }
99
+ }
100
+ result.push(line[i]);
101
+ i++;
102
+ }
103
+
104
+ return result.join("");
105
+ }
6
106
 
7
107
  function isRecord(value: unknown): value is Record<string, unknown> {
8
108
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
9
- // Exclude built-in types whose Object.entries() would produce empty arrays.
10
109
  if (value instanceof Date || value instanceof RegExp || value instanceof Error || value instanceof Map || value instanceof Set) return false;
11
110
  return true;
12
111
  }
13
112
 
14
- function isSecretKey(keyName: string): boolean {
15
- return SECRET_KEY_PATTERN.test(keyName) || /^(token|apiKey|api_key|password|secret|credential|authorization|privateKey|private_key)$/i.test(keyName);
113
+ export function redactSecretString(value: string): string {
114
+ let result = value;
115
+
116
+ // Replace PEM private keys
117
+ result = result.replace(PEM_PRIVATE_KEY_PATTERN, "***");
118
+
119
+ // Replace Authorization headers (non-Bearer format)
120
+ result = redactAuthHeader(result);
121
+
122
+ // Replace Bearer tokens
123
+ result = redactBearerTokens(result);
124
+
125
+ // Replace inline secrets: key=value or key:value patterns
126
+ result = redactInlineSecrets(result);
127
+
128
+ return result;
16
129
  }
17
130
 
18
- export function redactSecretString(value: string): string {
19
- return value
20
- .replace(PEM_PRIVATE_KEY_PATTERN, "***")
21
- .replace(AUTH_HEADER_PATTERN, "$1***")
22
- .replace(BEARER_PATTERN, "$1***")
23
- .replace(INLINE_SECRET_PATTERN, "$1$2***");
131
+ // Linear-time inline secret redaction: token=xxx, api_key=xxx, etc.
132
+ function redactInlineSecrets(value: string): string {
133
+ const result: string[] = [];
134
+ let i = 0;
135
+
136
+ while (i < value.length) {
137
+ // Look for pattern: word_chars + = or : + non-whitespace_value
138
+ // Check for secret key followed by = or :
139
+ let j = i;
140
+ let keyLen = 0;
141
+
142
+ // Collect key characters (alphanumeric, underscore, hyphen)
143
+ while (j < value.length && /[a-zA-Z0-9_-]/.test(value[j])) {
144
+ j++;
145
+ keyLen++;
146
+ }
147
+
148
+ if (keyLen > 0 && j < value.length && (value[j] === '=' || value[j] === ':')) {
149
+ const key = value.substring(i, i + keyLen);
150
+
151
+ // Check if this is a secret key
152
+ if (isSecretKey(key)) {
153
+ // Find the value (everything after = or : until space, comma, or end)
154
+ const sep = value[j];
155
+ let k = j + 1;
156
+ let valLen = 0;
157
+ while (k < value.length && valLen < 500 && value[k] !== ' ' && value[k] !== ',' && value[k] !== ';' && value[k] !== '"' && value[k] !== '"' && value[k] !== '\r' && value[k] !== '\n') {
158
+ k++;
159
+ valLen++;
160
+ }
161
+
162
+ // Only redact if there's actual content
163
+ if (valLen > 0) {
164
+ result.push(key);
165
+ result.push(sep);
166
+ result.push("***");
167
+ i = k;
168
+ continue;
169
+ }
170
+ }
171
+ }
172
+
173
+ result.push(value[i]);
174
+ i++;
175
+ }
176
+
177
+ return result.join("");
24
178
  }
25
179
 
26
180
  export function redactSecrets(value: unknown, keyName = ""): unknown {
@@ -41,4 +195,4 @@ export function redactJsonLine(line: string): string {
41
195
  } catch {
42
196
  return redactSecretString(line);
43
197
  }
44
- }
198
+ }
@@ -8,6 +8,9 @@ export interface ServerSentEvent {
8
8
  /** L1: Maximum number of raw lines before discarding an oversized event. */
9
9
  const MAX_EVENT_LINES = 1000;
10
10
 
11
+ /** L2: Maximum data size per line to prevent unbounded memory usage. */
12
+ const MAX_DATA_SIZE = 100000; // 100KB per line
13
+
11
14
  /** Read newline-delimited lines from a text ReadableStream, buffering partial chunks. */
12
15
  async function* readLines(
13
16
  stream: ReadableStream<string>,
@@ -97,13 +100,19 @@ export async function* readSseEvents(
97
100
 
98
101
  currentRaw.push(line);
99
102
 
100
- // L1: Guard against unbounded memory growth
103
+ // L1: Guard against unbounded memory growth (line count)
101
104
  if (currentRaw.length > MAX_EVENT_LINES) {
102
105
  const evt = flush();
103
106
  if (evt) yield evt;
104
107
  continue;
105
108
  }
106
109
 
110
+ // L2: Guard against unbounded memory growth (data size per line)
111
+ if (value.length > MAX_DATA_SIZE) {
112
+ // Truncate oversized data to prevent memory issues
113
+ value = value.slice(0, MAX_DATA_SIZE);
114
+ }
115
+
107
116
  if (field === "event") {
108
117
  currentEvent = value;
109
118
  } else if (field === "data") {
@@ -59,7 +59,12 @@ export function cleanupRunWorktrees(manifest: TeamRunManifest, options: { force?
59
59
  // Commit changes to a branch instead of just preserving the worktree
60
60
  try {
61
61
  execFileSync("git", ["add", "-A"], { cwd: worktreePath, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" }, windowsHide: true });
62
- const safeDesc = entry.name.slice(0, 200);
62
+ let safeDesc = entry.name.slice(0, 200);
63
+ // SECURITY: Strip any newlines that could be injected via a malicious worktree name
64
+ // to prevent newline injection in git commit messages
65
+ if (safeDesc.includes("\n")) {
66
+ safeDesc = safeDesc.replace(/[\r\n]+/g, " ");
67
+ }
63
68
  execFileSync("git", ["commit", "-m", `pi-crew: ${safeDesc}`], { cwd: worktreePath, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" }, windowsHide: true });
64
69
  // Create branch in the main repo pointing to this worktree's HEAD
65
70
  try {
@@ -0,0 +1,252 @@
1
+ # Chain Workflow - Sequential execution with context passing
2
+
3
+ **Source:** `docs/pi-boomerang-integration-plan.md`
4
+ **Syntax:** `step1 -> step2 -> step3`
5
+ **Version:** 1.0.0
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ The Chain workflow enables sequential execution of multiple teams with automatic context passing between steps. Each step receives a handoff summary from the previous step, enabling informed execution without repeating context.
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ # Simple team chain
17
+ pi team run --chain "@research -> @implement -> @review"
18
+
19
+ # With model override
20
+ pi team run --chain "@research -> @implement --model claude-opus-3 -> @review"
21
+
22
+ # With inline goals
23
+ pi team run --chain '"Research AI trends" -> "Analyze findings" -> "Write report"'
24
+
25
+ # Global model override
26
+ pi team run --chain "@step1 -> @step2 -> @step3" --global-model claude-sonnet-4
27
+
28
+ # With timeout
29
+ pi team run --chain "@step1 --timeout 60 -> @step2"
30
+
31
+ # Continue on error
32
+ pi team run --chain "@step1 -> @step2" --continue-on-error
33
+ ```
34
+
35
+ ## Chain Syntax
36
+
37
+ ### Step References
38
+
39
+ | Syntax | Type | Example |
40
+ |--------|------|---------|
41
+ | `@teamName` | Team reference | `@research` |
42
+ | `workflow:name` | Workflow reference | `workflow:build` |
43
+ | `template:name` | Template reference | `template:planning` |
44
+ | `"goal text"` | Inline goal | `"Research AI trends"` |
45
+
46
+ ### Per-Step Overrides
47
+
48
+ | Flag | Description | Example |
49
+ |------|-------------|---------|
50
+ | `--model <model>` | Override model | `--model claude-opus-3` |
51
+ | `--skill <skill>` | Override skill | `--skill coding` |
52
+ | `--thinking <mode>` | Thinking mode | `--thinking deep` |
53
+ | `--timeout <seconds>` | Step timeout | `--timeout 60` |
54
+ | `--continue-on-error` | Continue chain on failure | `--continue-on-error` |
55
+
56
+ ### Global Overrides
57
+
58
+ | Flag | Description | Example |
59
+ |------|-------------|---------|
60
+ | `--global-model <model>` | Apply to all steps | `--global-model sonnet` |
61
+ | `--global-skill <skill>` | Apply to all steps | `--global-skill writing` |
62
+ | `--global-thinking <mode>` | Apply to all steps | `--global-thinking fast` |
63
+ | `--continue-on-error` | Continue on any step failure | `--continue-on-error` |
64
+
65
+ ## Workflow Definition
66
+
67
+ ```yaml
68
+ name: chain
69
+ description: Sequential execution with context passing
70
+ syntax: "step1 -> step2 -> step3"
71
+
72
+ steps:
73
+ - id: chain_executor
74
+ role: chain-executor
75
+ task: |
76
+ Execute the chain: {chain}
77
+
78
+ Each step receives context from previous steps.
79
+ Generate handoff summary after each step.
80
+
81
+ configuration:
82
+ # Chain parser settings
83
+ parser:
84
+ stepSeparator: "->"
85
+ trimWhitespace: true
86
+ validateReferences: true
87
+
88
+ # Handoff settings
89
+ handoff:
90
+ generateBetweenSteps: true
91
+ accumulateContext: true
92
+ maxHandoffHistory: 10
93
+
94
+ # Execution settings
95
+ execution:
96
+ sequential: true
97
+ stopOnFailure: true
98
+ timeoutPerStep: 300000 # 5 minutes
99
+ ```
100
+
101
+ ## Context Passing
102
+
103
+ When running a chain:
104
+
105
+ 1. **Initial context** is passed to the first step
106
+ 2. **Handoff summary** is generated after each step completes
107
+ 3. **Chain history** is appended to context for subsequent steps
108
+
109
+ ### Handoff Summary Structure
110
+
111
+ ```typescript
112
+ interface HandoffSummary {
113
+ taskId: string;
114
+ runId: string;
115
+ timestamp: number;
116
+
117
+ task: string;
118
+ outcome: "success" | "failure" | "partial";
119
+
120
+ filesCreated: string[];
121
+ filesModified: string[];
122
+ filesDeleted: string[];
123
+
124
+ decisions: Decision[];
125
+
126
+ blockers: string[];
127
+ nextSteps: string[];
128
+
129
+ metrics: {
130
+ tokensUsed: number;
131
+ duration: number;
132
+ iterations: number;
133
+ toolsUsed: string[];
134
+ };
135
+
136
+ contextSnapshot: string;
137
+ }
138
+ ```
139
+
140
+ ### Chain History Format
141
+
142
+ ```typescript
143
+ interface ChainHistoryEntry {
144
+ step: string;
145
+ outcome: string;
146
+ filesCreated: string[];
147
+ filesModified: string[];
148
+ decisions: string[];
149
+ nextSteps: string[];
150
+ }
151
+ ```
152
+
153
+ ## Examples
154
+
155
+ ### Research → Implement → Review
156
+
157
+ ```bash
158
+ pi team run \
159
+ --team implementation \
160
+ --workflow chain \
161
+ --chain "@research:gather -> @implement:build -> @review:verify" \
162
+ --goal "Build feature X with research, implementation, and review"
163
+ ```
164
+
165
+ ### Multi-Model Pipeline
166
+
167
+ ```bash
168
+ pi team run \
169
+ --chain "@fast-research --model haiku -> @deep-analysis --model opus -> @summary --model sonnet" \
170
+ --goal "Analyze codebase and produce documentation"
171
+ ```
172
+
173
+ ### Error-Tolerant Pipeline
174
+
175
+ ```bash
176
+ pi team run \
177
+ --chain "@step1 -> @step2 -> @step3" \
178
+ --continue-on-error \
179
+ --goal "Run data pipeline with graceful degradation"
180
+ ```
181
+
182
+ ## Integration Points
183
+
184
+ ### Retry Support
185
+
186
+ Chains can be combined with retry configuration:
187
+
188
+ ```typescript
189
+ interface ChainRetryConfig {
190
+ maxAttempts: number;
191
+ summaryBetweenAttempts: boolean;
192
+ stopOnSuccess: boolean;
193
+ backoffMs?: number;
194
+ }
195
+
196
+ // Example: Retry each step up to 2 times
197
+ const config: ChainRetryConfig = {
198
+ maxAttempts: 2,
199
+ summaryBetweenAttempts: true,
200
+ stopOnSuccess: true,
201
+ backoffMs: 1000,
202
+ };
203
+ ```
204
+
205
+ ### Budget Tracking
206
+
207
+ Chain execution supports budget tracking per step:
208
+
209
+ ```bash
210
+ pi team run \
211
+ --chain "@step1 -> @step2" \
212
+ --budget-total 100000 \
213
+ --budget-warning 80000
214
+ ```
215
+
216
+ ## Event Types
217
+
218
+ | Event | Description |
219
+ |-------|-------------|
220
+ | `chain.started` | Chain execution began |
221
+ | `chain.step_completed` | Step completed successfully |
222
+ | `chain.step_failed` | Step failed |
223
+ | `chain.completed` | All steps completed |
224
+ | `chain.failed` | Chain failed |
225
+
226
+ ## Error Handling
227
+
228
+ ### Default Behavior
229
+
230
+ - Chain stops on first failure
231
+ - Handoff from failed step includes error details
232
+ - Final result includes all attempted steps
233
+
234
+ ### Continue on Error
235
+
236
+ ```bash
237
+ pi team run --chain "@step1 -> @step2" --continue-on-error
238
+ ```
239
+
240
+ - All steps execute regardless of failures
241
+ - Each step receives context from previous (even failed) steps
242
+ - Final result indicates overall success/failure
243
+
244
+ ## Related Features
245
+
246
+ - **HandoffManager** (`src/runtime/handoff-manager.ts`) - Generates structured summaries
247
+ - **RetryRunner** (`src/runtime/retry-runner.ts`) - Retry with accumulated context
248
+ - **BudgetTracker** - Token budget tracking across steps
249
+
250
+ ---
251
+
252
+ *Generated from pi-boomerang integration plan. See `docs/pi-boomerang-integration-plan.md` for full specification.*
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: pipeline
3
+ description: Multi-stage pipeline with automatic fan-out for array inputs
4
+ ---
5
+
6
+ ## Stage 1: Research
7
+ role: explorer
8
+
9
+ Perform initial research on: {goal}. Gather relevant information, identify key concepts, and provide a structured summary.
10
+
11
+ ## Stage 2: Analysis
12
+ role: analyst
13
+ dependsOn: Stage 1
14
+
15
+ Analyze the research findings from Stage 1. Identify patterns, relationships, and insights. Provide structured analysis with supporting evidence.
16
+
17
+ ## Stage 3: Synthesis
18
+ role: analyst
19
+ dependsOn: Stage 2
20
+
21
+ Synthesize the analysis into actionable recommendations. Prioritize findings and provide clear next steps.
22
+
23
+ ## Stage 4: Documentation
24
+ role: writer
25
+ dependsOn: Synthesis
26
+
27
+ Document the complete findings in a clear, well-structured format. Include executive summary, detailed findings, and recommendations.