pi-crew 0.5.2 → 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 +183 -0
- package/README.md +17 -1
- package/docs/architecture.md +2 -0
- package/docs/bugs/cross-session-notification-leakage.md +82 -0
- package/docs/coding-agent-optimization.md +268 -0
- package/docs/deep-review-report.md +384 -0
- package/docs/distillation/cybersecurity-patterns.md +294 -0
- package/docs/migration-v0.4-v0.5.md +208 -0
- package/docs/optimization-plan.md +642 -0
- package/docs/pi-crew-v0.5.5-audit-fix-plan.md +133 -0
- package/docs/pi-mono-opportunities.md +969 -0
- package/docs/pi-mono-review.md +291 -0
- package/docs/skills/REFERENCE.md +144 -0
- package/package.json +12 -9
- package/skills/artifact-analysis-loop/SKILL.md +302 -0
- package/skills/async-worker-recovery/SKILL.md +19 -1
- package/skills/child-pi-spawning/SKILL.md +19 -6
- package/skills/context-artifact-hygiene/SKILL.md +19 -2
- package/skills/delegation-patterns/SKILL.md +68 -3
- package/skills/detection-pipeline-design/SKILL.md +285 -0
- package/skills/event-log-tracing/SKILL.md +20 -6
- package/skills/git-master/SKILL.md +20 -6
- package/skills/hunting-investigation-loop/SKILL.md +401 -0
- package/skills/incident-playbook-construction/SKILL.md +383 -0
- package/skills/live-agent-lifecycle/SKILL.md +20 -6
- package/skills/mailbox-interactive/SKILL.md +19 -6
- package/skills/model-routing-context/SKILL.md +19 -1
- package/skills/multi-perspective-review/SKILL.md +19 -4
- package/skills/observability-reliability/SKILL.md +19 -2
- package/skills/orchestration/SKILL.md +20 -2
- package/skills/ownership-session-security/SKILL.md +20 -2
- package/skills/pi-extension-lifecycle/SKILL.md +20 -2
- package/skills/post-mortem/SKILL.md +7 -2
- package/skills/read-only-explorer/SKILL.md +20 -6
- package/skills/requirements-to-task-packet/SKILL.md +23 -3
- package/skills/resource-discovery-config/SKILL.md +20 -2
- package/skills/runtime-state-reader/SKILL.md +20 -2
- package/skills/safe-bash/SKILL.md +21 -6
- package/skills/scrutinize/SKILL.md +20 -2
- package/skills/secure-agent-orchestration-review/SKILL.md +29 -2
- package/skills/security-review/SKILL.md +560 -0
- package/skills/state-mutation-locking/SKILL.md +22 -2
- package/skills/systematic-debugging/SKILL.md +8 -6
- package/skills/threat-hypothesis-framework/SKILL.md +175 -0
- package/skills/ui-render-performance/SKILL.md +20 -2
- package/skills/verification-before-done/SKILL.md +17 -2
- package/skills/widget-rendering/SKILL.md +21 -6
- package/skills/workspace-isolation/SKILL.md +20 -6
- package/skills/worktree-isolation/SKILL.md +20 -6
- package/src/agents/agent-config.ts +40 -1
- package/src/benchmark/benchmark-runner.ts +45 -0
- package/src/benchmark/feedback-loop.ts +5 -0
- package/src/config/config.ts +32 -5
- package/src/config/role-tools.ts +82 -0
- package/src/config/suggestions.ts +8 -0
- package/src/config/types.ts +4 -0
- package/src/extension/async-notifier.ts +10 -1
- package/src/extension/crew-cleanup.ts +114 -0
- package/src/extension/cross-extension-rpc.ts +1 -1
- package/src/extension/notification-router.ts +18 -0
- package/src/extension/register.ts +27 -19
- 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 +42 -7
- package/src/extension/team-tool.ts +44 -2
- package/src/hooks/registry.ts +1 -3
- package/src/observability/event-bus.ts +69 -0
- 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 +10 -3
- package/src/runtime/budget-tracker.ts +354 -0
- package/src/runtime/chain-runner.ts +507 -0
- package/src/runtime/child-pi.ts +123 -35
- package/src/runtime/crash-recovery.ts +5 -4
- package/src/runtime/crew-agent-runtime.ts +1 -0
- 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/foreground-control.ts +87 -17
- 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/pi-args.ts +11 -1
- package/src/runtime/pi-json-output.ts +31 -0
- package/src/runtime/pipeline-runner.ts +514 -0
- package/src/runtime/progress-tracker.ts +124 -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/skill-effectiveness.ts +473 -0
- package/src/runtime/skill-instructions.ts +37 -3
- package/src/runtime/subagent-manager.ts +1 -1
- package/src/runtime/task-graph.ts +11 -1
- package/src/runtime/task-runner.ts +92 -18
- package/src/runtime/team-runner.ts +13 -12
- package/src/runtime/tool-progress.ts +10 -3
- package/src/runtime/verification-gates.ts +367 -0
- package/src/schema/team-tool-schema.ts +37 -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 +98 -55
- package/src/state/event-log-rotation.ts +2 -2
- package/src/state/event-log.ts +144 -10
- package/src/state/hook-instinct-bridge.ts +5 -5
- package/src/state/mailbox.ts +10 -0
- package/src/state/run-cache.ts +18 -8
- package/src/state/state-store.ts +3 -1
- package/src/state/types.ts +4 -0
- package/src/tools/safe-bash-extension.ts +1 -0
- package/src/tools/safe-bash.ts +152 -20
- package/src/types/new-api-types.ts +34 -0
- package/src/ui/agent-management-overlay.ts +5 -1
- package/src/ui/crew-widget.ts +29 -15
- package/src/ui/overlays/mailbox-detail-overlay.ts +13 -2
- package/src/ui/powerbar-publisher.ts +101 -7
- package/src/ui/tool-render.ts +15 -15
- 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/session-utils.ts +52 -0
- package/src/utils/sse-parser.ts +10 -1
- package/src/worktree/cleanup.ts +6 -1
- package/src/worktree/worktree-manager.ts +32 -13
- package/workflows/chain.workflow.md +252 -0
- package/workflows/pipeline.workflow.md +27 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChainRunner - Execute sequential chains with `->` syntax support.
|
|
3
|
+
*
|
|
4
|
+
* Based on pi-boomerang's parseChain pattern:
|
|
5
|
+
* - Parses "teamA -> teamB -> teamC" syntax
|
|
6
|
+
* - Supports per-step overrides for model, skill, thinking
|
|
7
|
+
* - Accumulates handoffs between steps
|
|
8
|
+
* - Executes steps sequentially with context passing
|
|
9
|
+
*
|
|
10
|
+
* @see docs/pi-boomerang-integration-plan.md
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { HandoffSummary, HandoffManager, TaskPacket, TaskResult } from "./handoff-manager.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Single step in a chain.
|
|
17
|
+
*/
|
|
18
|
+
export interface ChainStep {
|
|
19
|
+
/** Step name/identifier */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Team to execute (if using team reference) */
|
|
22
|
+
team?: string;
|
|
23
|
+
/** Workflow to execute (if using workflow reference) */
|
|
24
|
+
workflow?: string;
|
|
25
|
+
/** Template to execute (if using template reference) */
|
|
26
|
+
template?: string;
|
|
27
|
+
/** Inline goal text (for literal goals) */
|
|
28
|
+
inlineGoal?: string;
|
|
29
|
+
|
|
30
|
+
/** Per-step model override */
|
|
31
|
+
model?: string;
|
|
32
|
+
/** Per-step skill override */
|
|
33
|
+
skill?: string;
|
|
34
|
+
/** Thinking mode */
|
|
35
|
+
thinking?: "fast" | "standard" | "deep";
|
|
36
|
+
|
|
37
|
+
/** Step-specific context */
|
|
38
|
+
context?: Record<string, unknown>;
|
|
39
|
+
/** Step timeout in milliseconds */
|
|
40
|
+
timeout?: number;
|
|
41
|
+
|
|
42
|
+
/** Whether to continue chain on failure */
|
|
43
|
+
continueOnError?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parsed chain specification.
|
|
48
|
+
*/
|
|
49
|
+
export interface ChainSpec {
|
|
50
|
+
/** Ordered steps in the chain */
|
|
51
|
+
steps: ChainStep[];
|
|
52
|
+
/** Global arguments applied to all steps */
|
|
53
|
+
globalArgs?: Record<string, unknown>;
|
|
54
|
+
/** Global model override */
|
|
55
|
+
globalModel?: string;
|
|
56
|
+
/** Global skill override */
|
|
57
|
+
globalSkill?: string;
|
|
58
|
+
/** Global thinking mode */
|
|
59
|
+
globalThinking?: "fast" | "standard" | "deep";
|
|
60
|
+
/** Continue chain on step failure */
|
|
61
|
+
continueOnError?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Result of a single chain step execution.
|
|
66
|
+
*/
|
|
67
|
+
export interface ChainStepResult {
|
|
68
|
+
step: number;
|
|
69
|
+
name: string;
|
|
70
|
+
outcome: "success" | "failure" | "skipped" | "partial";
|
|
71
|
+
result?: TaskResult;
|
|
72
|
+
handoff?: HandoffSummary;
|
|
73
|
+
duration: number;
|
|
74
|
+
error?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Final chain execution result.
|
|
79
|
+
*/
|
|
80
|
+
export interface ChainResult {
|
|
81
|
+
steps: ChainStepResult[];
|
|
82
|
+
totalDuration: number;
|
|
83
|
+
success: boolean;
|
|
84
|
+
/** Total tokens used across all steps */
|
|
85
|
+
totalTokens?: number;
|
|
86
|
+
/** All handoffs generated during chain */
|
|
87
|
+
totalHandoffs: HandoffSummary[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Task runner interface for chain execution.
|
|
92
|
+
*/
|
|
93
|
+
export interface ChainTaskRunner {
|
|
94
|
+
runTask(packet: TaskPacket): Promise<TaskResult>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* ChainRunner executes sequential chains with context passing.
|
|
99
|
+
*/
|
|
100
|
+
export class ChainRunner {
|
|
101
|
+
/** Maximum number of chain history entries to prevent memory leaks */
|
|
102
|
+
private static readonly MAX_CHAIN_HISTORY_SIZE = 100;
|
|
103
|
+
|
|
104
|
+
/** Maximum size per handoff entry to prevent memory issues from large artifacts */
|
|
105
|
+
private static readonly MAX_HANDOFF_ENTRY_SIZE = 5000; // bytes per entry
|
|
106
|
+
|
|
107
|
+
constructor(
|
|
108
|
+
private taskRunner: ChainTaskRunner,
|
|
109
|
+
private handoffManager: HandoffManager,
|
|
110
|
+
) {}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parse chain syntax: step1 -> step2 -> step3
|
|
114
|
+
*
|
|
115
|
+
* Supports multiple syntaxes:
|
|
116
|
+
* - Team reference: @teamName
|
|
117
|
+
* - Workflow reference: workflow:name
|
|
118
|
+
* - Template reference: template:name
|
|
119
|
+
* - Inline goal: "goal description"
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* parseChain("@research -> @implement -> @review")
|
|
123
|
+
* parseChain('"Research AI trends" -> "Analyze findings"')
|
|
124
|
+
* parseChain("@step1 --model claude-opus-3 -> @step2")
|
|
125
|
+
*
|
|
126
|
+
* @param chainString - The chain string to parse
|
|
127
|
+
* @returns Parsed chain specification
|
|
128
|
+
*/
|
|
129
|
+
parseChain(chainString: string): ChainSpec {
|
|
130
|
+
const stepStrings = chainString.split("->").map(s => s.trim());
|
|
131
|
+
|
|
132
|
+
const steps: ChainStep[] = stepStrings.map((step, index) => {
|
|
133
|
+
return this.parseStep(step, index);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Extract global overrides
|
|
137
|
+
const globalModel = this.extractGlobalFlag(chainString, "global-model");
|
|
138
|
+
const globalSkill = this.extractGlobalFlag(chainString, "global-skill");
|
|
139
|
+
const globalThinking = this.extractGlobalFlag(chainString, "global-thinking") as "fast" | "standard" | "deep" | undefined;
|
|
140
|
+
const continueOnError = this.extractGlobalFlag(chainString, "continue-on-error") === "true";
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
steps,
|
|
144
|
+
globalModel,
|
|
145
|
+
globalSkill,
|
|
146
|
+
globalThinking,
|
|
147
|
+
continueOnError,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Execute chain sequentially.
|
|
153
|
+
* Each step receives handoff from previous step.
|
|
154
|
+
*
|
|
155
|
+
* @param spec - Parsed chain specification
|
|
156
|
+
* @param initialContext - Initial context for the chain
|
|
157
|
+
* @param eventsPath - Optional event log path for events
|
|
158
|
+
* @returns Final chain result
|
|
159
|
+
*/
|
|
160
|
+
async runChain(
|
|
161
|
+
spec: ChainSpec,
|
|
162
|
+
initialContext: Record<string, unknown> = {},
|
|
163
|
+
eventsPath?: string
|
|
164
|
+
): Promise<ChainResult> {
|
|
165
|
+
const stepResults: ChainStepResult[] = [];
|
|
166
|
+
let accumulatedContext = { ...initialContext };
|
|
167
|
+
const startTime = Date.now();
|
|
168
|
+
let totalTokens = 0;
|
|
169
|
+
const allHandoffs: HandoffSummary[] = [];
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < spec.steps.length; i++) {
|
|
172
|
+
const step = spec.steps[i];
|
|
173
|
+
const stepStart = Date.now();
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
// Resolve effective config (step overrides global)
|
|
177
|
+
const effectiveConfig = this.getEffectiveConfig(step, spec);
|
|
178
|
+
|
|
179
|
+
// Enrich context with previous handoffs
|
|
180
|
+
const stepContext = this.enrichContextFromHandoffs(
|
|
181
|
+
accumulatedContext,
|
|
182
|
+
stepResults
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Execute step
|
|
186
|
+
const result = await this.executeStep(effectiveConfig, stepContext);
|
|
187
|
+
|
|
188
|
+
// Track tokens
|
|
189
|
+
if (result.usage?.totalTokens) {
|
|
190
|
+
totalTokens += result.usage.totalTokens;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Generate handoff for next step
|
|
194
|
+
const handoff = await this.handoffManager.generateSummary(
|
|
195
|
+
this.createMinimalPacket(step, i),
|
|
196
|
+
result
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
stepResults.push({
|
|
200
|
+
step: i + 1,
|
|
201
|
+
name: step.name,
|
|
202
|
+
outcome: result.outcome,
|
|
203
|
+
result,
|
|
204
|
+
handoff,
|
|
205
|
+
duration: Date.now() - stepStart,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (handoff !== null) { allHandoffs.push(handoff); }
|
|
209
|
+
|
|
210
|
+
// Update accumulated context on success
|
|
211
|
+
if (result.outcome === "success") {
|
|
212
|
+
accumulatedContext = {
|
|
213
|
+
...accumulatedContext,
|
|
214
|
+
[`step_${i}_result`]: result,
|
|
215
|
+
[`step_${i}_handoff`]: handoff,
|
|
216
|
+
};
|
|
217
|
+
} else {
|
|
218
|
+
// Stop chain on step failure unless configured to continue
|
|
219
|
+
if (!spec.continueOnError && !step.continueOnError) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Emit progress event if eventsPath provided
|
|
225
|
+
if (eventsPath) {
|
|
226
|
+
const { appendEventAsync } = await import("../state/event-log.ts");
|
|
227
|
+
await appendEventAsync(eventsPath, {
|
|
228
|
+
type: "chain.step_completed",
|
|
229
|
+
runId: "chain",
|
|
230
|
+
taskId: `step-${i + 1}`,
|
|
231
|
+
data: {
|
|
232
|
+
step: i + 1,
|
|
233
|
+
name: step.name,
|
|
234
|
+
outcome: result.outcome,
|
|
235
|
+
duration: Date.now() - stepStart,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
242
|
+
|
|
243
|
+
stepResults.push({
|
|
244
|
+
step: i + 1,
|
|
245
|
+
name: step.name,
|
|
246
|
+
outcome: "failure",
|
|
247
|
+
duration: Date.now() - stepStart,
|
|
248
|
+
error: errorMessage,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Stop chain on failure unless configured to continue
|
|
252
|
+
if (!spec.continueOnError && !step.continueOnError) {
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
steps: stepResults,
|
|
260
|
+
totalDuration: Date.now() - startTime,
|
|
261
|
+
success: stepResults.every(s => s.outcome !== "failure"),
|
|
262
|
+
totalTokens: totalTokens > 0 ? totalTokens : undefined,
|
|
263
|
+
totalHandoffs: allHandoffs,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Parse a single step from the chain string.
|
|
269
|
+
* Includes type safety checks for ChainStep parsing (H3).
|
|
270
|
+
*/
|
|
271
|
+
private parseStep(step: string, index: number): ChainStep {
|
|
272
|
+
// Parse team reference: @teamName
|
|
273
|
+
const teamMatch = step.match(/^@([a-zA-Z][a-zA-Z0-9_]*)/);
|
|
274
|
+
|
|
275
|
+
// Parse workflow reference: workflow:name
|
|
276
|
+
const workflowMatch = step.match(/^workflow:([a-zA-Z][a-zA-Z0-9_]*)/);
|
|
277
|
+
|
|
278
|
+
// Parse template reference: template:name
|
|
279
|
+
const templateMatch = step.match(/^template:([a-zA-Z][a-zA-Z0-9_]*)/);
|
|
280
|
+
|
|
281
|
+
// Parse inline goal: "goal description" (can follow other patterns)
|
|
282
|
+
const inlineMatch = step.match(/"([^"]{1,10000})"/);
|
|
283
|
+
|
|
284
|
+
const nameParts = step.split(/\s+/);
|
|
285
|
+
const name = (nameParts[0] && nameParts[0].length > 0 && nameParts[0].length <= 100)
|
|
286
|
+
? nameParts[0]
|
|
287
|
+
: `step-${index}`;
|
|
288
|
+
|
|
289
|
+
const parsed: ChainStep = {
|
|
290
|
+
name,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Set step type based on matching pattern with type safety
|
|
294
|
+
if (teamMatch && teamMatch[1]) {
|
|
295
|
+
parsed.team = this.sanitizeIdentifier(teamMatch[1]);
|
|
296
|
+
}
|
|
297
|
+
if (workflowMatch && workflowMatch[1]) {
|
|
298
|
+
parsed.workflow = this.sanitizeIdentifier(workflowMatch[1]);
|
|
299
|
+
}
|
|
300
|
+
if (templateMatch && templateMatch[1]) {
|
|
301
|
+
parsed.template = this.sanitizeIdentifier(templateMatch[1]);
|
|
302
|
+
}
|
|
303
|
+
if (inlineMatch && inlineMatch[1]) {
|
|
304
|
+
parsed.inlineGoal = this.sanitizeInlineGoal(inlineMatch[1]);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Parse per-step overrides with type safety
|
|
308
|
+
const modelVal = this.extractFlag(step, "model");
|
|
309
|
+
if (modelVal && this.isValidModelName(modelVal)) {
|
|
310
|
+
parsed.model = modelVal;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const skillVal = this.extractFlag(step, "skill");
|
|
314
|
+
if (skillVal && this.isValidIdentifier(skillVal)) {
|
|
315
|
+
parsed.skill = skillVal;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const thinkingVal = this.extractFlag(step, "thinking");
|
|
319
|
+
if (thinkingVal && this.isValidThinkingMode(thinkingVal)) {
|
|
320
|
+
parsed.thinking = thinkingVal;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Parse step timeout
|
|
324
|
+
const timeoutStr = this.extractFlag(step, "timeout");
|
|
325
|
+
if (timeoutStr) {
|
|
326
|
+
const timeoutMs = parseInt(timeoutStr, 10);
|
|
327
|
+
if (!isNaN(timeoutMs) && timeoutMs > 0 && timeoutMs <= 86400000) {
|
|
328
|
+
parsed.timeout = timeoutMs * 1000; // Convert seconds to ms
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Parse continueOnError for step
|
|
333
|
+
if (this.extractFlag(step, "continue-on-error") === "true") {
|
|
334
|
+
parsed.continueOnError = true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return parsed;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Sanitize identifier to prevent injection.
|
|
342
|
+
*/
|
|
343
|
+
private sanitizeIdentifier(value: string): string {
|
|
344
|
+
return value.replace(/[^a-zA-Z0-9_]/g, '_').substring(0, 100);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Sanitize inline goal to prevent injection.
|
|
349
|
+
*/
|
|
350
|
+
private sanitizeInlineGoal(value: string): string {
|
|
351
|
+
// Remove control characters and limit length
|
|
352
|
+
return value.replace(/[\x00-\x1F\x7F]/g, '').substring(0, 10000);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Validate model name format.
|
|
357
|
+
*/
|
|
358
|
+
private isValidModelName(value: string): boolean {
|
|
359
|
+
return /^[a-zA-Z][a-zA-Z0-9_-]{0,50}$/.test(value);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Validate identifier format.
|
|
364
|
+
*/
|
|
365
|
+
private isValidIdentifier(value: string): boolean {
|
|
366
|
+
return /^[a-zA-Z][a-zA-Z0-9_]{0,50}$/.test(value);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Validate thinking mode value.
|
|
371
|
+
*/
|
|
372
|
+
private isValidThinkingMode(value: string): value is "fast" | "standard" | "deep" {
|
|
373
|
+
return ["fast", "standard", "deep"].includes(value);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Extract a flag from step string.
|
|
378
|
+
* Uses escaped flag name to prevent regex injection.
|
|
379
|
+
*/
|
|
380
|
+
private extractFlag(input: string, flag: string): string | undefined {
|
|
381
|
+
// Escape regex special characters in flag name to prevent injection
|
|
382
|
+
const escapedFlag = flag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
383
|
+
const match = input.match(new RegExp(`--${escapedFlag}\\s+(\\S+)`));
|
|
384
|
+
return match?.[1];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Extract a global flag from the chain string.
|
|
389
|
+
* Global flags can appear anywhere in the chain string.
|
|
390
|
+
* Uses escaped flag name to prevent regex injection.
|
|
391
|
+
*/
|
|
392
|
+
private extractGlobalFlag(input: string, flag: string): string | undefined {
|
|
393
|
+
// Escape regex special characters in flag name to prevent injection
|
|
394
|
+
const escapedFlag = flag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
395
|
+
const patternEq = '--' + escapedFlag + '=\\s*(\\S+)';
|
|
396
|
+
const match = input.match(new RegExp(patternEq, 'i'));
|
|
397
|
+
if (match) return match[1];
|
|
398
|
+
|
|
399
|
+
const patternNoEq = '--' + escapedFlag + '\\s+(\\S+)';
|
|
400
|
+
const matchNoEq = input.match(new RegExp(patternNoEq, 'i'));
|
|
401
|
+
if (matchNoEq) return matchNoEq[1];
|
|
402
|
+
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get effective config with step overrides global.
|
|
408
|
+
*/
|
|
409
|
+
private getEffectiveConfig(step: ChainStep, spec: ChainSpec): ChainStep {
|
|
410
|
+
return {
|
|
411
|
+
...step,
|
|
412
|
+
model: step.model ?? spec.globalModel,
|
|
413
|
+
skill: step.skill ?? spec.globalSkill,
|
|
414
|
+
thinking: step.thinking ?? spec.globalThinking,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Enrich context with previous handoffs.
|
|
420
|
+
* Limits history size to prevent memory leaks.
|
|
421
|
+
*/
|
|
422
|
+
private enrichContextFromHandoffs(
|
|
423
|
+
context: Record<string, unknown>,
|
|
424
|
+
previousResults: ChainStepResult[]
|
|
425
|
+
): Record<string, unknown> {
|
|
426
|
+
const handoffs = previousResults
|
|
427
|
+
.filter(r => r.handoff)
|
|
428
|
+
.map(r => r.handoff!);
|
|
429
|
+
|
|
430
|
+
if (handoffs.length === 0) {
|
|
431
|
+
return context;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Limit history size to prevent memory leak (H2)
|
|
435
|
+
const limitedHandoffs = handoffs.slice(-ChainRunner.MAX_CHAIN_HISTORY_SIZE);
|
|
436
|
+
|
|
437
|
+
// Limit per-entry size to prevent memory issues from large artifacts
|
|
438
|
+
const filteredHandoffs = limitedHandoffs.filter(h => {
|
|
439
|
+
const size = JSON.stringify(h).length;
|
|
440
|
+
return size <= ChainRunner.MAX_HANDOFF_ENTRY_SIZE;
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
...context,
|
|
445
|
+
__chainHistory: filteredHandoffs.map(h => ({
|
|
446
|
+
step: h.taskId,
|
|
447
|
+
outcome: h.outcome,
|
|
448
|
+
filesCreated: h.filesCreated?.slice(0, 50), // Limit array size
|
|
449
|
+
filesModified: h.filesModified?.slice(0, 50), // Limit array size
|
|
450
|
+
decisions: h.decisions?.slice(0, 20), // Limit array size
|
|
451
|
+
nextSteps: h.nextSteps?.slice(0, 20), // Limit array size
|
|
452
|
+
})),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Execute a single step.
|
|
458
|
+
*/
|
|
459
|
+
private async executeStep(
|
|
460
|
+
config: ChainStep,
|
|
461
|
+
context: Record<string, unknown>
|
|
462
|
+
): Promise<TaskResult> {
|
|
463
|
+
const packet: TaskPacket = {
|
|
464
|
+
taskId: `chain-${Date.now()}-${config.name}`,
|
|
465
|
+
runId: "chain",
|
|
466
|
+
goal: config.inlineGoal ?? config.name,
|
|
467
|
+
summarizeThreshold: 3000,
|
|
468
|
+
collapseContext: true,
|
|
469
|
+
context,
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
return this.taskRunner.runTask(packet);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Create minimal packet for handoff generation.
|
|
477
|
+
*/
|
|
478
|
+
private createMinimalPacket(step: ChainStep, index: number): TaskPacket {
|
|
479
|
+
return {
|
|
480
|
+
taskId: `chain-step-${index}`,
|
|
481
|
+
runId: "chain",
|
|
482
|
+
sessionId: "chain",
|
|
483
|
+
goal: step.inlineGoal ?? step.name,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Create a ChainRunner with default dependencies.
|
|
490
|
+
*/
|
|
491
|
+
export function createChainRunner(
|
|
492
|
+
taskRunner: ChainTaskRunner,
|
|
493
|
+
handoffManager: HandoffManager
|
|
494
|
+
): ChainRunner {
|
|
495
|
+
return new ChainRunner(taskRunner, handoffManager);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Parse chain from string shorthand.
|
|
500
|
+
*/
|
|
501
|
+
export function parseChainString(chainString: string): ChainSpec {
|
|
502
|
+
const runner = new ChainRunner(
|
|
503
|
+
{ runTask: () => Promise.reject(new Error("Not initialized")) } as ChainTaskRunner,
|
|
504
|
+
{} as HandoffManager
|
|
505
|
+
);
|
|
506
|
+
return runner.parseChain(chainString);
|
|
507
|
+
}
|