pi-crew 0.9.35 → 0.9.37
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 +48 -0
- package/dist/build-meta.json +54 -16
- package/dist/index.mjs +308 -9
- package/dist/index.mjs.map +4 -4
- package/package.json +1 -1
- package/src/extension/register.ts +6 -0
- package/src/extension/registration/subagent-tools.ts +18 -1
- package/src/extension/team-tool/context.ts +60 -4
- package/src/extension/team-tool/handle-settings.ts +1 -1
- package/src/runtime/async-runner.ts +1 -1
- package/src/runtime/progress-tracker.ts +140 -0
- package/src/runtime/verification-gates.ts +9 -4
- package/src/state/instinct-store.ts +5 -0
- package/src/ui/settings-overlay.ts +1 -1
- package/src/workflows/intermediate-store.ts +5 -0
package/package.json
CHANGED
|
@@ -130,6 +130,7 @@ function purgeStaleActiveRunIndexSyncIfLoaded(): void {
|
|
|
130
130
|
|
|
131
131
|
import { pruneFinishedRuns, pruneUserLevelRuns } from "../extension/run-maintenance.ts";
|
|
132
132
|
import { initI18n } from "../i18n.ts";
|
|
133
|
+
import { globalProgressTracker } from "../runtime/progress-tracker.ts";
|
|
133
134
|
// H3-L2 split: DeliveryCoordinator + OverflowRecoveryTracker moved to registration/lifecycle.ts.
|
|
134
135
|
import { tryRegisterSessionCleanup } from "../runtime/session-resources.ts";
|
|
135
136
|
import { createSessionSnapshot } from "../runtime/session-snapshot.ts";
|
|
@@ -1474,6 +1475,11 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1474
1475
|
const record = event as Record<string, unknown>;
|
|
1475
1476
|
const eventType = typeof record.type === "string" ? record.type : undefined;
|
|
1476
1477
|
if (eventType) lifecycleState.overflowTracker?.feedEvent(taskId, runId, eventType);
|
|
1478
|
+
// Phase 2: forward worker events to progress tracker for real-time
|
|
1479
|
+
// widget display (tool calls, assistant text) on child-process runtime.
|
|
1480
|
+
if (record && typeof record === "object") {
|
|
1481
|
+
globalProgressTracker.handleWorkerEvent(taskId, runId, record);
|
|
1482
|
+
}
|
|
1477
1483
|
},
|
|
1478
1484
|
});
|
|
1479
1485
|
registerSubagentTools(pi, subagentManager, {
|
|
@@ -23,6 +23,7 @@ import { loadConfig } from "../../config/config.ts";
|
|
|
23
23
|
import { t } from "../../i18n.ts";
|
|
24
24
|
import type { BatchBarrier } from "../../runtime/batch-barrier.ts";
|
|
25
25
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
26
|
+
import { globalProgressTracker } from "../../runtime/progress-tracker.ts";
|
|
26
27
|
import { checkSubagentSpawnPermission, currentCrewRole } from "../../runtime/role-permission.ts";
|
|
27
28
|
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
28
29
|
import {
|
|
@@ -444,7 +445,23 @@ function startAgentToolProgress(cwd: string, agentRecordId: string, onUpdate: On
|
|
|
444
445
|
agents,
|
|
445
446
|
error: record.error,
|
|
446
447
|
});
|
|
447
|
-
|
|
448
|
+
// Phase 2C: enrich with real-time worker progress (tool calls, partial text)
|
|
449
|
+
// from globalProgressTracker. For foreground agents, find the task ID from
|
|
450
|
+
// the manifest and look up streaming progress.
|
|
451
|
+
let progressLine = text;
|
|
452
|
+
if (tasks && tasks.length > 0) {
|
|
453
|
+
const wp = globalProgressTracker.getWorkerProgress(tasks[0].id);
|
|
454
|
+
if (wp) {
|
|
455
|
+
const toolLine = wp.currentTool ? `\n ⚡ tool: ${wp.currentTool}` : "";
|
|
456
|
+
const tokenLine =
|
|
457
|
+
wp.tokens.input + wp.tokens.output > 0
|
|
458
|
+
? `\n 📊 tokens: ${(wp.tokens.input / 1000).toFixed(1)}K in, ${(wp.tokens.output / 1000).toFixed(1)}K out`
|
|
459
|
+
: "";
|
|
460
|
+
const textLine = wp.partialText ? `\n 💬 ${wp.partialText.slice(-150).replace(/\n/g, " ")}` : "";
|
|
461
|
+
progressLine = `${text}${toolLine}${tokenLine}${textLine}`;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
onUpdate({ content: [{ type: "text", text: progressLine }] });
|
|
448
465
|
} catch (error) {
|
|
449
466
|
logInternalError("subagent-tools.progress", error, `agentId=${agentRecordId}`);
|
|
450
467
|
}
|
|
@@ -47,6 +47,40 @@ function extractTextContent(content: unknown): string {
|
|
|
47
47
|
.join("\n");
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Maximum character budget for inherited parent context (~3K tokens).
|
|
52
|
+
* When exceeded, oldest messages are dropped first (most-recent-first retention).
|
|
53
|
+
*/
|
|
54
|
+
export const MAX_PARENT_CONTEXT_CHARS = 12_000;
|
|
55
|
+
|
|
56
|
+
/** Maximum chars for a single assistant message before truncation. */
|
|
57
|
+
const MAX_ASSISTANT_MSG_CHARS = 500;
|
|
58
|
+
|
|
59
|
+
/** Truncated length for oversized assistant messages. */
|
|
60
|
+
const TRUNCATED_ASSISTANT_CHARS = 200;
|
|
61
|
+
|
|
62
|
+
/** Messages starting with these patterns and exceeding this size are likely
|
|
63
|
+
* file dumps / bash output — skip them to keep context relevant and compact. */
|
|
64
|
+
const NOISY_THRESHOLD = 1_000;
|
|
65
|
+
const NOISY_PREFIXES = ["```", "total ", "drwx", "-rw", "import ", "export "];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if a message looks like noisy file/tool output (code dumps, ls output,
|
|
69
|
+
* import lists) that bloats context without helping the subagent.
|
|
70
|
+
*/
|
|
71
|
+
function isNoisyContent(text: string): boolean {
|
|
72
|
+
if (text.length < NOISY_THRESHOLD) return false;
|
|
73
|
+
return NOISY_PREFIXES.some((prefix) => text.startsWith(prefix));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build a compact parent conversation context for subagent inheritance.
|
|
78
|
+
*
|
|
79
|
+
* Extracts recent user messages, assistant reasoning, and compaction summaries
|
|
80
|
+
* from the parent session. Applies a character budget (most-recent-first
|
|
81
|
+
* retention) and filters noisy content (file dumps, long bash output) to keep
|
|
82
|
+
* the inherited context relevant without bloating the subagent's token budget.
|
|
83
|
+
*/
|
|
50
84
|
export function buildParentContext(ctx: TeamContext): string | undefined {
|
|
51
85
|
const branch = ctx.sessionManager?.getBranch?.();
|
|
52
86
|
if (!Array.isArray(branch) || branch.length === 0) return undefined;
|
|
@@ -58,21 +92,43 @@ export function buildParentContext(ctx: TeamContext): string | undefined {
|
|
|
58
92
|
message?: unknown;
|
|
59
93
|
summary?: unknown;
|
|
60
94
|
};
|
|
61
|
-
|
|
95
|
+
// Compaction summaries are always valuable — keep in full.
|
|
96
|
+
if (record.type === "compaction" && typeof record.summary === "string") {
|
|
97
|
+
parts.push(`[Summary]: ${record.summary}`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
62
100
|
const message =
|
|
63
101
|
record.message && typeof record.message === "object" && !Array.isArray(record.message)
|
|
64
102
|
? (record.message as { role?: unknown; content?: unknown })
|
|
65
103
|
: undefined;
|
|
66
104
|
if (!message || (message.role !== "user" && message.role !== "assistant")) continue;
|
|
67
|
-
|
|
68
|
-
if (text)
|
|
105
|
+
let text = extractTextContent(message.content).trim();
|
|
106
|
+
if (!text) continue;
|
|
107
|
+
// Filter: skip noisy content (file dumps, long bash output).
|
|
108
|
+
if (isNoisyContent(text)) continue;
|
|
109
|
+
// Truncate: long assistant messages get shortened to key points.
|
|
110
|
+
if (message.role === "assistant" && text.length > MAX_ASSISTANT_MSG_CHARS) {
|
|
111
|
+
text = `${text.slice(0, TRUNCATED_ASSISTANT_CHARS)}…`;
|
|
112
|
+
}
|
|
113
|
+
parts.push(`[${message.role === "user" ? "User" : "Assistant"}]: ${text}`);
|
|
69
114
|
}
|
|
70
115
|
if (!parts.length) return undefined;
|
|
116
|
+
|
|
117
|
+
// Budget: keep most-recent messages first, drop oldest when over budget.
|
|
118
|
+
let totalChars = 0;
|
|
119
|
+
const budgeted: string[] = [];
|
|
120
|
+
for (const part of [...parts].reverse()) {
|
|
121
|
+
if (totalChars + part.length > MAX_PARENT_CONTEXT_CHARS) break;
|
|
122
|
+
budgeted.unshift(part);
|
|
123
|
+
totalChars += part.length;
|
|
124
|
+
}
|
|
125
|
+
if (!budgeted.length) return undefined;
|
|
126
|
+
|
|
71
127
|
return [
|
|
72
128
|
`# Parent Conversation Context`,
|
|
73
129
|
"The following context was inherited from the parent Pi session. Treat it as reference-only.",
|
|
74
130
|
"",
|
|
75
|
-
|
|
131
|
+
budgeted.join("\n\n"),
|
|
76
132
|
].join("\n");
|
|
77
133
|
}
|
|
78
134
|
|
|
@@ -14,7 +14,7 @@ const EFFECTIVE_DEFAULTS: Record<string, unknown> = {
|
|
|
14
14
|
"runtime.mode": "auto",
|
|
15
15
|
"runtime.maxTurns": 10000,
|
|
16
16
|
"runtime.graceTurns": 5,
|
|
17
|
-
"runtime.inheritContext":
|
|
17
|
+
"runtime.inheritContext": true,
|
|
18
18
|
"runtime.promptMode": "replace",
|
|
19
19
|
"runtime.completionMutationGuard": "warn",
|
|
20
20
|
"runtime.isolationPolicy": undefined,
|
|
@@ -210,7 +210,7 @@ export const BACKGROUND_RUNNER_ENV_ALLOWLIST: string[] = [
|
|
|
210
210
|
// Phase 1.5: worker-thread atomic writer opt-in (RFC 15).
|
|
211
211
|
"PI_CREW_WORKER_ATOMIC_WRITER",
|
|
212
212
|
"PI_TEAMS_WORKER_ATOMIC_WRITER",
|
|
213
|
-
// Phase 1.5 #1: verification env sanitization opt-
|
|
213
|
+
// Phase 1.5 #1: verification env sanitization opt-out (RFC 13 §6).
|
|
214
214
|
"PI_CREW_VERIFICATION_SANITIZE_ENV",
|
|
215
215
|
"PI_TEAMS_VERIFICATION_SANITIZE_ENV",
|
|
216
216
|
"PI_CREW_VERIFICATION_PRESERVE_ENV",
|
|
@@ -9,6 +9,8 @@ export interface AgentProgress {
|
|
|
9
9
|
turns: number;
|
|
10
10
|
tokens: { input: number; output: number };
|
|
11
11
|
status: "idle" | "running" | "completed" | "error";
|
|
12
|
+
/** Most recent partial assistant text (for live streaming display). */
|
|
13
|
+
partialText?: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export class ProgressTracker {
|
|
@@ -118,7 +120,145 @@ export class ProgressTracker {
|
|
|
118
120
|
getProgress(agentId: string): AgentProgress | undefined {
|
|
119
121
|
return this.sessions.get(agentId)?.progress;
|
|
120
122
|
}
|
|
123
|
+
|
|
124
|
+
// ── Child-process worker event bridge ──────────────────────────────────
|
|
125
|
+
//
|
|
126
|
+
// For child-process runtime, events arrive as raw JSON from the child pi
|
|
127
|
+
// process's stdout (via onJsonEvent). These methods bridge those events into
|
|
128
|
+
// the same crewEventBus stream that live-session uses, so the widget shows
|
|
129
|
+
// real-time tool calls and assistant text for BOTH runtimes.
|
|
130
|
+
|
|
131
|
+
private workerProgress = new Map<string, AgentProgress>();
|
|
132
|
+
|
|
133
|
+
/** Throttle: don't emit more than 1 progress event per 500ms per worker. */
|
|
134
|
+
private lastEmitTs = new Map<string, number>();
|
|
135
|
+
private static readonly EMIT_THROTTLE_MS = 500;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handle a raw child-process JSON event (from onJsonEvent callback).
|
|
139
|
+
* Processes tool_execution_start/end, agent_start/end, and assistant text.
|
|
140
|
+
*/
|
|
141
|
+
handleWorkerEvent(taskId: string, runId: string, event: Record<string, unknown>): void {
|
|
142
|
+
let progress = this.workerProgress.get(taskId);
|
|
143
|
+
if (!progress) {
|
|
144
|
+
progress = {
|
|
145
|
+
toolCalls: 0,
|
|
146
|
+
currentTool: null,
|
|
147
|
+
toolStartTime: null,
|
|
148
|
+
errors: [],
|
|
149
|
+
turns: 0,
|
|
150
|
+
tokens: { input: 0, output: 0 },
|
|
151
|
+
status: "running",
|
|
152
|
+
};
|
|
153
|
+
this.workerProgress.set(taskId, progress);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const eventType = typeof event.type === "string" ? event.type : undefined;
|
|
157
|
+
|
|
158
|
+
switch (eventType) {
|
|
159
|
+
case "tool_execution_start": {
|
|
160
|
+
progress.toolCalls++;
|
|
161
|
+
progress.currentTool = typeof event.toolName === "string" ? event.toolName : "unknown";
|
|
162
|
+
progress.toolStartTime = Date.now();
|
|
163
|
+
this.emitThrottled(taskId, runId, progress);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case "tool_execution_end": {
|
|
167
|
+
progress.currentTool = null;
|
|
168
|
+
progress.toolStartTime = null;
|
|
169
|
+
if (event.isError) {
|
|
170
|
+
progress.errors.push(String(event.result ?? "Unknown error"));
|
|
171
|
+
crewEventBus.emit({
|
|
172
|
+
type: "agent:error",
|
|
173
|
+
runId,
|
|
174
|
+
agentId: taskId,
|
|
175
|
+
payload: String(event.result ?? "Unknown error"),
|
|
176
|
+
timestamp: Date.now(),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
this.emitThrottled(taskId, runId, progress);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case "turn_start":
|
|
183
|
+
case "turn_end":
|
|
184
|
+
progress.turns++;
|
|
185
|
+
break;
|
|
186
|
+
case "agent_start":
|
|
187
|
+
progress.status = "running";
|
|
188
|
+
break;
|
|
189
|
+
case "agent_end":
|
|
190
|
+
case "agent_settled":
|
|
191
|
+
progress.status = "completed";
|
|
192
|
+
this.emitThrottled(taskId, runId, progress);
|
|
193
|
+
break;
|
|
194
|
+
case "message":
|
|
195
|
+
case "message_end": {
|
|
196
|
+
// Extract assistant text for streaming display.
|
|
197
|
+
const message = event.message as { role?: string; content?: unknown } | undefined;
|
|
198
|
+
if (message?.role === "assistant") {
|
|
199
|
+
const text = extractWorkerText(message.content);
|
|
200
|
+
if (text) {
|
|
201
|
+
progress.partialText = text.slice(-2000); // keep last 2K chars
|
|
202
|
+
this.emitThrottled(taskId, runId, progress);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Track usage from message_end events.
|
|
206
|
+
if (eventType === "message_end" && event.usage && typeof event.usage === "object") {
|
|
207
|
+
const usage = event.usage as { input?: number; output?: number };
|
|
208
|
+
if (typeof usage.input === "number") progress.tokens.input += usage.input;
|
|
209
|
+
if (typeof usage.output === "number") progress.tokens.output += usage.output;
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Get the accumulated progress for a child-process worker task. */
|
|
217
|
+
getWorkerProgress(taskId: string): AgentProgress | undefined {
|
|
218
|
+
return this.workerProgress.get(taskId);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Remove a worker from tracking after completion. */
|
|
222
|
+
untrackWorker(taskId: string): void {
|
|
223
|
+
this.workerProgress.delete(taskId);
|
|
224
|
+
this.lastEmitTs.delete(taskId);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Emit a progress event to crewEventBus, throttled to avoid flooding
|
|
229
|
+
* the widget with re-renders (max 1 event per EMIT_THROTTLE_MS per worker).
|
|
230
|
+
*/
|
|
231
|
+
private emitThrottled(taskId: string, runId: string, progress: AgentProgress): void {
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
const last = this.lastEmitTs.get(taskId) ?? 0;
|
|
234
|
+
if (now - last < ProgressTracker.EMIT_THROTTLE_MS) return;
|
|
235
|
+
this.lastEmitTs.set(taskId, now);
|
|
236
|
+
crewEventBus.emit({
|
|
237
|
+
type: "agent:progress",
|
|
238
|
+
runId,
|
|
239
|
+
agentId: taskId,
|
|
240
|
+
payload: { ...progress },
|
|
241
|
+
timestamp: now,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
121
244
|
}
|
|
122
245
|
|
|
123
246
|
// Export singleton instance
|
|
124
247
|
export const globalProgressTracker = new ProgressTracker();
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Extract text content from a worker message's content field.
|
|
251
|
+
* Handles string content and array-of-parts (like extractTextContent in context.ts).
|
|
252
|
+
*/
|
|
253
|
+
function extractWorkerText(content: unknown): string {
|
|
254
|
+
if (typeof content === "string") return content;
|
|
255
|
+
if (!Array.isArray(content)) return "";
|
|
256
|
+
return content
|
|
257
|
+
.map((part) =>
|
|
258
|
+
part && typeof part === "object" && !Array.isArray(part) && typeof (part as { text?: unknown }).text === "string"
|
|
259
|
+
? (part as { text: string }).text
|
|
260
|
+
: "",
|
|
261
|
+
)
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
.join("\n");
|
|
264
|
+
}
|
|
@@ -25,8 +25,9 @@ import { redactSecretString } from "../utils/redaction.ts";
|
|
|
25
25
|
* against adversarial workers; this kills the leak at the source by never
|
|
26
26
|
* giving the verification process the secret in the first place.
|
|
27
27
|
*
|
|
28
|
-
* Opt-
|
|
29
|
-
*
|
|
28
|
+
* Opt-out via `PI_CREW_VERIFICATION_SANITIZE_ENV=0` or
|
|
29
|
+
* `PI_TEAMS_VERIFICATION_SANITIZE_ENV=0` to disable sanitization (e.g. for
|
|
30
|
+
* tests that legitimately need API access). Escape hatch:
|
|
30
31
|
* `PI_CREW_VERIFICATION_PRESERVE_ENV=KEY1,KEY2,...` lets users explicitly opt
|
|
31
32
|
* specific secrets back in (audited via the allowlist validator).
|
|
32
33
|
*/
|
|
@@ -62,9 +63,13 @@ const VERIFICATION_ENV_ALLOWLIST: readonly string[] = [
|
|
|
62
63
|
"NPM_CONFIG_GLOBALCONFIG",
|
|
63
64
|
];
|
|
64
65
|
|
|
65
|
-
/** Whether env sanitization for verification is enabled (
|
|
66
|
+
/** Whether env sanitization for verification is enabled (opt-out: enabled by default). */
|
|
66
67
|
export function isVerificationEnvSanitizeEnabled(): boolean {
|
|
67
|
-
|
|
68
|
+
// Opt-out: explicitly disabled via env var set to '0'.
|
|
69
|
+
if (process.env.PI_CREW_VERIFICATION_SANITIZE_ENV === "0" || process.env.PI_TEAMS_VERIFICATION_SANITIZE_ENV === "0") {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { assertSafePathId } from "../utils/safe-paths.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Represents a learned instinct that guides agent behavior.
|
|
@@ -120,6 +121,7 @@ export class InstinctStore {
|
|
|
120
121
|
if (!savedInstinct.projectId) {
|
|
121
122
|
throw new Error("Project-scoped instinct requires a projectId");
|
|
122
123
|
}
|
|
124
|
+
assertSafePathId("projectId", savedInstinct.projectId);
|
|
123
125
|
this.appendInstinctToFile(this.getProjectInstinctPath(savedInstinct.projectId), savedInstinct);
|
|
124
126
|
}
|
|
125
127
|
|
|
@@ -163,6 +165,7 @@ export class InstinctStore {
|
|
|
163
165
|
* @returns Array of instincts for the project
|
|
164
166
|
*/
|
|
165
167
|
getProjectInstincts(projectId: string): Instinct[] {
|
|
168
|
+
assertSafePathId("projectId", projectId);
|
|
166
169
|
const projectInstincts = this.readInstinctsFromFile(this.getProjectInstinctPath(projectId));
|
|
167
170
|
const globalInstincts = this.readInstinctsFromFile(this.getGlobalInstinctPath());
|
|
168
171
|
return [...projectInstincts, ...globalInstincts];
|
|
@@ -183,6 +186,7 @@ export class InstinctStore {
|
|
|
183
186
|
.readdirSync(projectsDir, { withFileTypes: true })
|
|
184
187
|
.filter((e) => e.isDirectory())
|
|
185
188
|
.map((e) => e.name)) {
|
|
189
|
+
assertSafePathId("projectId", projectId);
|
|
186
190
|
const filePath = path.join(projectsDir, projectId, INSTINCT_FILE);
|
|
187
191
|
const instincts = this.readInstinctsFromFile(filePath);
|
|
188
192
|
const index = instincts.findIndex((i) => i.id === instinctId);
|
|
@@ -239,6 +243,7 @@ export class InstinctStore {
|
|
|
239
243
|
.readdirSync(projectsDir, { withFileTypes: true })
|
|
240
244
|
.filter((e) => e.isDirectory())
|
|
241
245
|
.map((e) => e.name)) {
|
|
246
|
+
assertSafePathId("projectId", projectId);
|
|
242
247
|
const filePath = path.join(projectsDir, projectId, INSTINCT_FILE);
|
|
243
248
|
instincts = this.readInstinctsFromFile(filePath);
|
|
244
249
|
index = instincts.findIndex((i) => i.id === instinctId);
|
|
@@ -327,7 +327,7 @@ const EFFECTIVE_DEFAULTS: Record<string, unknown> = {
|
|
|
327
327
|
"runtime.mode": "auto",
|
|
328
328
|
"runtime.maxTurns": 10000,
|
|
329
329
|
"runtime.graceTurns": 5,
|
|
330
|
-
"runtime.inheritContext":
|
|
330
|
+
"runtime.inheritContext": true,
|
|
331
331
|
"runtime.promptMode": "replace",
|
|
332
332
|
"runtime.completionMutationGuard": "warn",
|
|
333
333
|
"runtime.isolationPolicy": undefined,
|
|
@@ -56,6 +56,11 @@ export function ensureIntermediateDir(config: Partial<IntermediateStoreConfig> =
|
|
|
56
56
|
* @returns Path to the written file
|
|
57
57
|
*/
|
|
58
58
|
export function writeIntermediate(config: Partial<IntermediateStoreConfig>, phase: string, stepId: string, data: unknown): string {
|
|
59
|
+
// Security: validate phase/stepId before building the filename to prevent
|
|
60
|
+
// path traversal via poisoned phase or stepId.
|
|
61
|
+
if (!isSafePathId(phase) || !isSafePathId(stepId)) {
|
|
62
|
+
throw new Error("Invalid phase or stepId for intermediate store");
|
|
63
|
+
}
|
|
59
64
|
const dir = ensureIntermediateDir(config);
|
|
60
65
|
const filename = `${phase}-${stepId}.json`;
|
|
61
66
|
const filePath = path.join(dir, filename);
|