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.
- package/CHANGELOG.md +116 -0
- package/README.md +17 -1
- package/docs/architecture.md +2 -0
- package/docs/migration-v0.4-v0.5.md +19 -2
- package/docs/pi-crew-v0.5.5-audit-fix-plan.md +133 -0
- package/package.json +7 -5
- package/src/benchmark/benchmark-runner.ts +45 -0
- package/src/benchmark/feedback-loop.ts +5 -0
- package/src/config/config.ts +10 -0
- package/src/config/suggestions.ts +8 -0
- package/src/extension/async-notifier.ts +10 -1
- package/src/extension/cross-extension-rpc.ts +1 -1
- package/src/extension/notification-router.ts +18 -0
- package/src/extension/register.ts +13 -17
- package/src/extension/registration/subagent-tools.ts +1 -1
- package/src/extension/team-tool/anchor.ts +201 -0
- package/src/extension/team-tool/api.ts +2 -1
- package/src/extension/team-tool/auto-summarize.ts +154 -0
- package/src/extension/team-tool/run.ts +37 -2
- package/src/extension/team-tool.ts +44 -2
- package/src/hooks/registry.ts +1 -3
- package/src/observability/event-bus.ts +13 -4
- package/src/observability/event-to-metric.ts +0 -2
- package/src/runtime/anchor-manager.ts +473 -0
- package/src/runtime/async-runner.ts +8 -4
- package/src/runtime/auto-summarize.ts +350 -0
- package/src/runtime/background-runner.ts +2 -1
- package/src/runtime/budget-tracker.ts +354 -0
- package/src/runtime/chain-runner.ts +507 -0
- package/src/runtime/child-pi.ts +1 -1
- package/src/runtime/crash-recovery.ts +5 -4
- package/src/runtime/custom-tools/irc-tool.ts +13 -0
- package/src/runtime/custom-tools/submit-result-tool.ts +3 -2
- package/src/runtime/delivery-coordinator.ts +10 -3
- package/src/runtime/dynamic-script-runner.ts +482 -0
- package/src/runtime/handoff-manager.ts +589 -0
- package/src/runtime/hidden-handoff.ts +424 -0
- package/src/runtime/live-agent-manager.ts +20 -4
- package/src/runtime/live-session-runtime.ts +39 -4
- package/src/runtime/manifest-cache.ts +2 -1
- package/src/runtime/model-resolver.ts +16 -4
- package/src/runtime/phase-tracker.ts +373 -0
- package/src/runtime/pipeline-runner.ts +514 -0
- package/src/runtime/retry-runner.ts +354 -0
- package/src/runtime/sandbox.ts +252 -0
- package/src/runtime/scheduler.ts +7 -2
- package/src/runtime/subagent-manager.ts +1 -1
- package/src/runtime/task-graph.ts +11 -1
- package/src/runtime/task-runner.ts +1 -1
- package/src/runtime/team-runner.ts +4 -3
- package/src/schema/team-tool-schema.ts +30 -0
- package/src/skills/discover-skills.ts +5 -0
- package/src/state/active-run-registry.ts +9 -2
- package/src/state/contracts.ts +9 -0
- package/src/state/crew-init.ts +3 -3
- package/src/state/decision-ledger.ts +26 -32
- package/src/state/event-log-rotation.ts +2 -2
- package/src/state/event-log.ts +9 -1
- package/src/state/mailbox.ts +10 -0
- package/src/state/run-cache.ts +18 -8
- package/src/tools/safe-bash-extension.ts +1 -0
- package/src/tools/safe-bash.ts +152 -20
- package/src/ui/overlays/mailbox-detail-overlay.ts +13 -2
- package/src/ui/powerbar-publisher.ts +1 -0
- package/src/ui/transcript-cache.ts +13 -0
- package/src/utils/bm25-search.ts +16 -8
- package/src/utils/env-filter.ts +8 -5
- package/src/utils/redaction.ts +169 -15
- package/src/utils/sse-parser.ts +10 -1
- package/src/worktree/cleanup.ts +6 -1
- package/workflows/chain.workflow.md +252 -0
- 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.
|
|
60
|
+
this.needsRefresh = true;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
render(width: number): string[] {
|
|
56
|
-
this.
|
|
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 = [
|
|
@@ -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 ?? [];
|
package/src/utils/bm25-search.ts
CHANGED
|
@@ -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
|
|
50
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
const
|
|
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(
|
|
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);
|
package/src/utils/env-filter.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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("*"))
|
|
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 && !
|
|
30
|
+
if (value !== undefined && !isSecretKey(key)) filtered[key] = value;
|
|
28
31
|
}
|
|
29
32
|
return filtered;
|
|
30
|
-
}
|
|
33
|
+
}
|
package/src/utils/redaction.ts
CHANGED
|
@@ -1,26 +1,180 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
}
|
package/src/utils/sse-parser.ts
CHANGED
|
@@ -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") {
|
package/src/worktree/cleanup.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|