pi-subagents 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/README.md +54 -1879
- package/agents/oracle.md +1 -0
- package/inspector-runner.mjs +11 -0
- package/package.json +13 -5
- package/prompts/parallel-context-build.md +1 -1
- package/prompts/parallel-handoff-plan.md +1 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +9 -7
- package/skills/pi-subagents/references/constraints-and-recipes.md +24 -32
- package/skills/pi-subagents/references/execution-controls.md +99 -97
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +23 -30
- package/src/agents/agent-management.ts +155 -65
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +154 -71
- package/src/agents/chain-serializer.ts +10 -7
- package/src/agents/frontmatter.ts +5 -3
- package/src/agents/identity.ts +1 -1
- package/src/agents/proactive-skills.ts +13 -10
- package/src/agents/skills.ts +23 -6
- package/src/api/control-channel.ts +4 -0
- package/src/api/delegation.ts +26 -194
- package/src/api/external-runs.ts +129 -0
- package/src/api/intercom-bridge.ts +3 -0
- package/src/api/pi-args.ts +5 -0
- package/src/api/preflight.ts +3 -3
- package/src/api/shared-types.ts +19 -0
- package/src/extension/config.ts +10 -0
- package/src/extension/control-notices.ts +5 -39
- package/src/extension/doctor.ts +10 -9
- package/src/extension/fanout-child.ts +7 -4
- package/src/extension/index.ts +232 -68
- package/src/extension/rpc.ts +18 -12
- package/src/extension/schemas.ts +48 -37
- package/src/extension/tool-description.ts +36 -86
- package/src/inspectors/herdr/actions.ts +229 -0
- package/src/inspectors/herdr/client.ts +130 -0
- package/src/inspectors/herdr/inspector-runner.ts +141 -0
- package/src/inspectors/herdr/project-panes.ts +154 -0
- package/src/integrations/herdr-status.ts +330 -0
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/result-intercom.ts +5 -1
- package/src/missions/actions.ts +374 -0
- package/src/missions/lifecycle.ts +314 -0
- package/src/missions/store.ts +442 -0
- package/src/missions/types.ts +135 -0
- package/src/policy/authority.ts +46 -0
- package/src/profiles/profiles.ts +29 -3
- package/src/runs/background/async-execution.ts +110 -50
- package/src/runs/background/async-job-tracker.ts +27 -4
- package/src/runs/background/async-resume.ts +6 -6
- package/src/runs/background/async-status.ts +29 -1
- package/src/runs/background/auto-drain.ts +3 -3
- package/src/runs/background/chain-append.ts +3 -2
- package/src/runs/background/control-channel.ts +9 -7
- package/src/runs/background/fleet-view.ts +3 -4
- package/src/runs/background/notify.ts +2 -1
- package/src/runs/background/process-terminal.ts +5 -5
- package/src/runs/background/result-watcher.ts +13 -5
- package/src/runs/background/run-id-resolver.ts +3 -3
- package/src/runs/background/run-status.ts +35 -8
- package/src/runs/background/scheduled-runs.ts +602 -375
- package/src/runs/background/stale-run-reconciler.ts +3 -3
- package/src/runs/background/subagent-runner.ts +608 -445
- package/src/runs/background/subagent-wait.ts +50 -9
- package/src/runs/background/wait-subscriptions.ts +253 -0
- package/src/runs/background/wait-tool.ts +12 -4
- package/src/runs/foreground/async-steering-action.ts +3 -3
- package/src/runs/foreground/chain-clarify.ts +8 -4
- package/src/runs/foreground/chain-execution.ts +56 -30
- package/src/runs/foreground/execution.ts +34 -9
- package/src/runs/foreground/subagent-executor.ts +1036 -274
- package/src/runs/shared/acceptance.ts +28 -6
- package/src/runs/shared/child-protocol.ts +302 -22
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/external-cli-runner.ts +130 -0
- package/src/runs/shared/long-running-guard.ts +42 -1
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -0
- package/src/runs/shared/nested-events.ts +59 -5
- package/src/runs/shared/nested-render.ts +9 -4
- package/src/runs/shared/parallel-handoff.ts +86 -2
- package/src/runs/shared/parallel-utils.ts +11 -2
- package/src/runs/shared/permissions.ts +95 -0
- package/src/runs/shared/pi-args.ts +468 -109
- package/src/runs/shared/pi-spawn.ts +11 -1
- package/src/runs/shared/run-history.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
- package/src/runs/shared/subagent-startup-retry.ts +5 -2
- package/src/runs/shared/turn-budget.ts +6 -6
- package/src/runs/shared/worktree.ts +122 -12
- package/src/shared/accessible-dir.ts +29 -7
- package/src/shared/artifacts.ts +143 -1
- package/src/shared/fork-context.ts +3 -2
- package/src/shared/launch-contract.ts +1 -0
- package/src/shared/settings.ts +10 -0
- package/src/shared/types.ts +156 -14
- package/src/shared/utils.ts +8 -6
- package/src/slash/delegation-adapters.ts +32 -194
- package/src/slash/delegation-request.ts +43 -126
- package/src/slash/prompt-template-bridge.ts +158 -205
- package/src/slash/prompt-workflows.ts +21 -57
- package/src/slash/slash-bridge.ts +14 -0
- package/src/slash/slash-commands.ts +31 -632
- package/src/slash/subagents-admin.ts +18 -14
- package/src/tui/fleet-status.ts +156 -21
- package/src/tui/fleet-transcript.ts +110 -5
- package/src/tui/fleet.ts +57 -25
- package/src/tui/render.ts +368 -152
- package/src/types/pi-runtime-compat.d.ts +8 -0
- package/src/watchdog/lsp-diagnostics.ts +12 -7
- package/src/watchdog/model-selection.ts +2 -2
- package/src/watchdog/permission-arbiter.ts +145 -0
- package/src/watchdog/register-child.ts +1 -1
- package/src/watchdog/register-main.ts +1 -1
- package/src/watchdog/review.ts +4 -1
- package/src/watchdog/runtime.ts +3 -2
- package/src/workflows/chat-progress.ts +140 -0
- package/src/workflows/scripted-workflow.ts +415 -0
- package/agents/advisor.md +0 -73
- package/src/extension/chain-validation.ts +0 -181
|
@@ -169,6 +169,7 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
169
169
|
if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
170
170
|
else if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
|
|
171
171
|
else if (input === "none") errors.push(`${pathLabel} level "none" requires a reason; use { level: "none", reason: "..." }.`);
|
|
172
|
+
else if (input === "verified") errors.push(`${pathLabel} level "verified" requires object form with at least one runtime verify command. Use level "checked" or provide a non-empty acceptance.verify array.`);
|
|
172
173
|
return errors;
|
|
173
174
|
}
|
|
174
175
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
@@ -232,7 +233,11 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
232
233
|
} else if (value.evidence !== undefined) {
|
|
233
234
|
errors.push(`${pathLabel}.evidence must be an array. ${ACCEPTANCE_EVIDENCE_HELP}`);
|
|
234
235
|
}
|
|
235
|
-
if (value.
|
|
236
|
+
if (value.level === "verified" && (!Array.isArray(value.verify) || value.verify.length === 0)) {
|
|
237
|
+
errors.push(`${pathLabel}.verify must contain at least one runtime command when level is verified. Use level "checked" or provide a non-empty acceptance.verify array.`);
|
|
238
|
+
} else if (value.verify !== undefined && !Array.isArray(value.verify)) {
|
|
239
|
+
errors.push(`${pathLabel}.verify must be an array.`);
|
|
240
|
+
}
|
|
236
241
|
if (Array.isArray(value.verify)) {
|
|
237
242
|
for (const [index, command] of value.verify.entries()) {
|
|
238
243
|
if (!command || typeof command !== "object" || Array.isArray(command)) {
|
|
@@ -311,7 +316,7 @@ export function validateExecutionAcceptance(input: {
|
|
|
311
316
|
}
|
|
312
317
|
|
|
313
318
|
function normalizeCriteria(criteria: Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }> | undefined, evidence: AcceptanceEvidenceKind[]): ResolvedAcceptanceGate[] {
|
|
314
|
-
return (criteria ?? []).map((criterion, index) => {
|
|
319
|
+
return (criteria ?? []).map((criterion, index): ResolvedAcceptanceGate => {
|
|
315
320
|
if (typeof criterion === "string") {
|
|
316
321
|
return { id: `criterion-${index + 1}`, must: criterion, evidence, severity: "required" };
|
|
317
322
|
}
|
|
@@ -405,7 +410,7 @@ export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig, opt
|
|
|
405
410
|
lines.push("", "Runtime verification commands configured by parent:");
|
|
406
411
|
for (const command of acceptance.verify) lines.push(`- ${command.id}: ${command.command}`);
|
|
407
412
|
}
|
|
408
|
-
if (acceptance.review
|
|
413
|
+
if (acceptance.review) {
|
|
409
414
|
lines.push("", `Review gate: ${acceptance.review.required === false ? "optional" : "required"}${acceptance.review.agent ? ` by ${acceptance.review.agent}` : ""}.`);
|
|
410
415
|
if (acceptance.review.focus) lines.push(`Review focus: ${acceptance.review.focus}`);
|
|
411
416
|
}
|
|
@@ -650,6 +655,21 @@ function parseAcceptanceReportBody(body: string): { report?: AcceptanceReport; e
|
|
|
650
655
|
return validateAcceptanceReport(parseReportJson(body));
|
|
651
656
|
}
|
|
652
657
|
|
|
658
|
+
function parseUnterminatedAcceptanceReportFence(output: string): { report?: AcceptanceReport; error?: string } {
|
|
659
|
+
const opener = /```acceptance[-_]report\b[^\n]*\n/gi.exec(output);
|
|
660
|
+
if (!opener) return {};
|
|
661
|
+
const bodyStart = opener.index + opener[0].length;
|
|
662
|
+
if (output.indexOf("```", bodyStart) !== -1) return {};
|
|
663
|
+
try {
|
|
664
|
+
const validation = validateAcceptanceReport(JSON.parse(output.slice(bodyStart).trim()) as unknown);
|
|
665
|
+
return validation.report
|
|
666
|
+
? { report: validation.report }
|
|
667
|
+
: { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}` };
|
|
668
|
+
} catch (error) {
|
|
669
|
+
return { error: `Failed to parse acceptance-report: ${error instanceof Error ? error.message : String(error)}` };
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
653
673
|
function parseGenericJsonAcceptanceReportBody(body: string): { report?: AcceptanceReport; error?: string } {
|
|
654
674
|
const parsed = parseReportJson(body);
|
|
655
675
|
const normalized = normalizeAcceptanceReportValue(parsed);
|
|
@@ -681,6 +701,8 @@ export function parseAcceptanceReport(output: string): { report?: AcceptanceRepo
|
|
|
681
701
|
}
|
|
682
702
|
if (parseErrors.length > 0) return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}` };
|
|
683
703
|
if (explicitFencePresent) {
|
|
704
|
+
const recovered = parseUnterminatedAcceptanceReportFence(output);
|
|
705
|
+
if (recovered.report || recovered.error) return recovered;
|
|
684
706
|
return { error: "Failed to parse acceptance-report: Empty or unterminated acceptance-report fence." };
|
|
685
707
|
}
|
|
686
708
|
for (const body of fencedBlocks(output, "(?:json|jsonc|json5)")) {
|
|
@@ -942,7 +964,7 @@ function uniqueStrings(items: Array<string | undefined>): string[] {
|
|
|
942
964
|
}
|
|
943
965
|
|
|
944
966
|
export function aggregateAcceptanceReport(input: {
|
|
945
|
-
results: Array<Pick<SingleResult, "agent" | "acceptance" | "error" |
|
|
967
|
+
results: Array<Pick<SingleResult, "agent" | "acceptance" | "error"> & { exitCode: number | null }>;
|
|
946
968
|
notes?: string;
|
|
947
969
|
}): AcceptanceReport {
|
|
948
970
|
const childReports = input.results.map((result) => result.acceptance?.childReport).filter((report): report is AcceptanceReport => Boolean(report));
|
|
@@ -952,7 +974,7 @@ export function aggregateAcceptanceReport(input: {
|
|
|
952
974
|
criteriaSatisfied: [
|
|
953
975
|
{ id: "criterion-1", status: successfulChildren ? "satisfied" : "not-satisfied", evidence: successfulChildren ? `All ${input.results.length} dynamic child run(s) completed without child or acceptance blockers.` : "Dynamic fanout produced no accepted child evidence." },
|
|
954
976
|
{ id: "criterion-2", status: successfulChildren ? "satisfied" : "not-satisfied", evidence: successfulChildren ? "Collected child acceptance evidence for aggregate review." : "Dynamic fanout produced no aggregate review evidence." },
|
|
955
|
-
...input.results.map((result, index) => ({
|
|
977
|
+
...input.results.map((result, index): { id?: string; status: "satisfied" | "not-satisfied" | "not-applicable"; evidence: string } => ({
|
|
956
978
|
id: `child-${index + 1}`,
|
|
957
979
|
status: result.exitCode === 0 && result.acceptance?.status !== "rejected" ? "satisfied" : "not-satisfied",
|
|
958
980
|
evidence: `${result.agent}: acceptance ${result.acceptance?.status ?? "unreported"}${result.error ? ` (${result.error})` : ""}`,
|
|
@@ -1148,7 +1170,7 @@ export async function evaluateAcceptance(input: {
|
|
|
1148
1170
|
ledger.evidenceStatus = ledger.status;
|
|
1149
1171
|
}
|
|
1150
1172
|
|
|
1151
|
-
if (acceptance.review
|
|
1173
|
+
if (acceptance.review) {
|
|
1152
1174
|
if (input.reviewResult?.status === "reviewed") {
|
|
1153
1175
|
ledger.reviewResult = input.reviewResult;
|
|
1154
1176
|
ledger.status = "reviewed";
|
|
@@ -6,6 +6,236 @@ export type { ProtocolOutputLimit } from "../../shared/types.ts";
|
|
|
6
6
|
export const MAX_CHILD_PENDING_LINE_BYTES = 16 * 1024 * 1024;
|
|
7
7
|
export const MAX_CHILD_STDERR_BYTES = 128 * 1024;
|
|
8
8
|
const MAX_PROTOCOL_DIAGNOSTIC_BYTES = 4096;
|
|
9
|
+
const MAX_PROJECTED_JSON_DEPTH = 256;
|
|
10
|
+
|
|
11
|
+
export interface OversizedLineProjection {
|
|
12
|
+
push(chunk: Buffer): boolean;
|
|
13
|
+
finish(): string | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface OversizedLineProjector {
|
|
17
|
+
accepts(prefix: string): boolean;
|
|
18
|
+
create(): OversizedLineProjection;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type JsonContainer =
|
|
22
|
+
| { type: "array"; state: "value-or-end" | "value" | "comma-or-end" }
|
|
23
|
+
| { type: "object"; state: "key-or-end" | "key" | "colon" | "value" | "comma-or-end"; key?: string };
|
|
24
|
+
|
|
25
|
+
type JsonToken =
|
|
26
|
+
| { type: "string"; role: "key" | "value"; value: string; capture: boolean; escape: boolean; unicodeDigits: number; unicodeValue: string }
|
|
27
|
+
| { type: "literal"; expected: string; index: number; value: boolean | null }
|
|
28
|
+
| { type: "number"; phase: "minus" | "zero" | "int" | "dot" | "frac" | "exp" | "exp-sign" | "exp-digits" };
|
|
29
|
+
|
|
30
|
+
function createPiAggregateProjection(): OversizedLineProjection {
|
|
31
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
32
|
+
const stack: JsonContainer[] = [];
|
|
33
|
+
let rootState: "value" | "end" = "value";
|
|
34
|
+
let token: JsonToken | undefined;
|
|
35
|
+
let valid = true;
|
|
36
|
+
let eventType: string | undefined;
|
|
37
|
+
let willRetry: boolean | undefined;
|
|
38
|
+
|
|
39
|
+
const parent = (): JsonContainer | undefined => stack.at(-1);
|
|
40
|
+
const isTopLevelField = (key: string | undefined): boolean => stack.length === 1 && parent()?.type === "object" && (key === "type" || key === "willRetry");
|
|
41
|
+
|
|
42
|
+
const completeValue = (value?: string | boolean | null): void => {
|
|
43
|
+
const container = parent();
|
|
44
|
+
if (!container) {
|
|
45
|
+
rootState = "end";
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (container.type === "object") {
|
|
49
|
+
if (stack.length === 1 && container.key === "type" && typeof value === "string") eventType = value;
|
|
50
|
+
if (stack.length === 1 && container.key === "willRetry" && typeof value === "boolean") willRetry = value;
|
|
51
|
+
container.key = undefined;
|
|
52
|
+
container.state = "comma-or-end";
|
|
53
|
+
} else container.state = "comma-or-end";
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const startValue = (char: string): boolean => {
|
|
57
|
+
const container = parent();
|
|
58
|
+
const key = container?.type === "object" ? container.key : undefined;
|
|
59
|
+
if (isTopLevelField(key)) {
|
|
60
|
+
if (key === "type") eventType = undefined;
|
|
61
|
+
else willRetry = undefined;
|
|
62
|
+
}
|
|
63
|
+
if (char === "{" || char === "[") {
|
|
64
|
+
if (stack.length >= MAX_PROJECTED_JSON_DEPTH) return false;
|
|
65
|
+
stack.push(char === "{" ? { type: "object", state: "key-or-end" } : { type: "array", state: "value-or-end" });
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (char === '"') {
|
|
69
|
+
token = { type: "string", role: "value", value: "", capture: key === "type" && stack.length === 1, escape: false, unicodeDigits: 0, unicodeValue: "" };
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (char === "t") token = { type: "literal", expected: "true", index: 1, value: true };
|
|
73
|
+
else if (char === "f") token = { type: "literal", expected: "false", index: 1, value: false };
|
|
74
|
+
else if (char === "n") token = { type: "literal", expected: "null", index: 1, value: null };
|
|
75
|
+
else if (char === "-") token = { type: "number", phase: "minus" };
|
|
76
|
+
else if (char === "0") token = { type: "number", phase: "zero" };
|
|
77
|
+
else if (char >= "1" && char <= "9") token = { type: "number", phase: "int" };
|
|
78
|
+
else return false;
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const closeContainer = (): true => {
|
|
83
|
+
stack.pop();
|
|
84
|
+
completeValue();
|
|
85
|
+
return true;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const processChar = (char: string): boolean => {
|
|
89
|
+
if (token?.type === "string") {
|
|
90
|
+
if (token.unicodeDigits > 0) {
|
|
91
|
+
if (!/[0-9a-fA-F]/.test(char)) return false;
|
|
92
|
+
token.unicodeValue += char;
|
|
93
|
+
token.unicodeDigits--;
|
|
94
|
+
if (token.unicodeDigits === 0 && token.capture) {
|
|
95
|
+
if (token.value.length >= 64) return false;
|
|
96
|
+
token.value += String.fromCharCode(Number.parseInt(token.unicodeValue, 16));
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (token.escape) {
|
|
101
|
+
token.escape = false;
|
|
102
|
+
if (char === "u") { token.unicodeDigits = 4; token.unicodeValue = ""; return true; }
|
|
103
|
+
if (!'"\\/bfnrt'.includes(char)) return false;
|
|
104
|
+
if (token.capture) {
|
|
105
|
+
if (token.value.length >= 64) return false;
|
|
106
|
+
token.value += ({ b: "\b", f: "\f", n: "\n", r: "\r", t: "\t" } as Record<string, string>)[char] ?? char;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (char === "\\") { token.escape = true; return true; }
|
|
111
|
+
if (char === '"') {
|
|
112
|
+
const finished = token;
|
|
113
|
+
token = undefined;
|
|
114
|
+
if (finished.role === "key") {
|
|
115
|
+
const container = parent();
|
|
116
|
+
if (!container || container.type !== "object") return false;
|
|
117
|
+
container.key = finished.value;
|
|
118
|
+
container.state = "colon";
|
|
119
|
+
} else completeValue(finished.capture ? finished.value : undefined);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
if (char.charCodeAt(0) < 0x20) return false;
|
|
123
|
+
if (token.capture) {
|
|
124
|
+
if (token.value.length >= 64) return false;
|
|
125
|
+
token.value += char;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (token?.type === "literal") {
|
|
130
|
+
if (char !== token.expected[token.index]) return false;
|
|
131
|
+
token.index++;
|
|
132
|
+
if (token.index === token.expected.length) {
|
|
133
|
+
const value = token.value;
|
|
134
|
+
token = undefined;
|
|
135
|
+
completeValue(value);
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (token?.type === "number") {
|
|
140
|
+
const phase = token.phase;
|
|
141
|
+
if (phase === "minus") {
|
|
142
|
+
if (char === "0") token.phase = "zero";
|
|
143
|
+
else if (char >= "1" && char <= "9") token.phase = "int";
|
|
144
|
+
else return false;
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (phase === "zero" || phase === "int") {
|
|
148
|
+
if (char >= "0" && char <= "9") { if (phase === "zero") return false; return true; }
|
|
149
|
+
if (char === ".") { token.phase = "dot"; return true; }
|
|
150
|
+
if (char === "e" || char === "E") { token.phase = "exp"; return true; }
|
|
151
|
+
} else if (phase === "dot") {
|
|
152
|
+
if (char >= "0" && char <= "9") { token.phase = "frac"; return true; }
|
|
153
|
+
return false;
|
|
154
|
+
} else if (phase === "frac") {
|
|
155
|
+
if (char >= "0" && char <= "9") return true;
|
|
156
|
+
if (char === "e" || char === "E") { token.phase = "exp"; return true; }
|
|
157
|
+
} else if (phase === "exp") {
|
|
158
|
+
if (char === "+" || char === "-") { token.phase = "exp-sign"; return true; }
|
|
159
|
+
if (char >= "0" && char <= "9") { token.phase = "exp-digits"; return true; }
|
|
160
|
+
return false;
|
|
161
|
+
} else if (phase === "exp-sign") {
|
|
162
|
+
if (char >= "0" && char <= "9") { token.phase = "exp-digits"; return true; }
|
|
163
|
+
return false;
|
|
164
|
+
} else if (phase === "exp-digits" && char >= "0" && char <= "9") return true;
|
|
165
|
+
if (!["zero", "int", "frac", "exp-digits"].includes(phase)) return false;
|
|
166
|
+
token = undefined;
|
|
167
|
+
completeValue();
|
|
168
|
+
return processChar(char);
|
|
169
|
+
}
|
|
170
|
+
if (char === " " || char === "\t" || char === "\r" || char === "\n") return true;
|
|
171
|
+
const container = parent();
|
|
172
|
+
if (!container) return rootState === "value" ? startValue(char) : false;
|
|
173
|
+
if (container.type === "object") {
|
|
174
|
+
if (container.state === "key-or-end" || container.state === "key") {
|
|
175
|
+
if (char === "}" && container.state === "key-or-end") return closeContainer();
|
|
176
|
+
if (char !== '"') return false;
|
|
177
|
+
token = { type: "string", role: "key", value: "", capture: stack.length === 1, escape: false, unicodeDigits: 0, unicodeValue: "" };
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
if (container.state === "colon") {
|
|
181
|
+
if (char !== ":") return false;
|
|
182
|
+
container.state = "value";
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
if (container.state === "value") return startValue(char);
|
|
186
|
+
if (char === ",") { container.state = "key"; return true; }
|
|
187
|
+
if (char === "}") return closeContainer();
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
if (container.state === "value-or-end" || container.state === "value") {
|
|
191
|
+
if (char === "]" && container.state === "value-or-end") return closeContainer();
|
|
192
|
+
return startValue(char);
|
|
193
|
+
}
|
|
194
|
+
if (char === ",") { container.state = "value"; return true; }
|
|
195
|
+
if (char === "]") return closeContainer();
|
|
196
|
+
return false;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const processText = (text: string): boolean => {
|
|
200
|
+
for (const char of text) if (!processChar(char)) return false;
|
|
201
|
+
return true;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
push(chunk) {
|
|
206
|
+
if (!valid) return false;
|
|
207
|
+
try { valid = processText(decoder.decode(chunk, { stream: true })); }
|
|
208
|
+
catch { valid = false; }
|
|
209
|
+
return valid;
|
|
210
|
+
},
|
|
211
|
+
finish() {
|
|
212
|
+
try { valid = valid && processText(decoder.decode()); }
|
|
213
|
+
catch { valid = false; }
|
|
214
|
+
if (token?.type === "number" && ["zero", "int", "frac", "exp-digits"].includes(token.phase)) {
|
|
215
|
+
token = undefined;
|
|
216
|
+
completeValue();
|
|
217
|
+
}
|
|
218
|
+
if (!valid || token || stack.length !== 0 || rootState !== "end") return undefined;
|
|
219
|
+
if (eventType === "turn_end") return '{"type":"turn_end"}';
|
|
220
|
+
if (eventType === "agent_end" && typeof willRetry === "boolean") return JSON.stringify({ type: "agent_end", willRetry });
|
|
221
|
+
return undefined;
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Pi JSON mode emits granular message/tool events followed by aggregate
|
|
228
|
+
* `turn_end` and `agent_end` events that duplicate those payloads. Parallel
|
|
229
|
+
* image reads can make one aggregate record exceed the child line limit even
|
|
230
|
+
* though every granular event was valid. Replace only syntactically valid,
|
|
231
|
+
* redundant records with the lifecycle fields the runners consume.
|
|
232
|
+
*/
|
|
233
|
+
export const PI_AGGREGATE_EVENT_PROJECTOR: OversizedLineProjector = {
|
|
234
|
+
accepts(prefix) {
|
|
235
|
+
return prefix.startsWith('{"type":"turn_end"') || prefix.startsWith('{"type":"agent_end"');
|
|
236
|
+
},
|
|
237
|
+
create: createPiAggregateProjection,
|
|
238
|
+
};
|
|
9
239
|
|
|
10
240
|
export function formatProtocolOutputLimit(limit: ProtocolOutputLimit): string {
|
|
11
241
|
return `${limit.code}: child ${limit.stream} line exceeded ${limit.limitBytes} bytes (observed at least ${limit.observedBytes} bytes without a newline).`;
|
|
@@ -14,6 +244,7 @@ export function formatProtocolOutputLimit(limit: ProtocolOutputLimit): string {
|
|
|
14
244
|
export function createBoundedLineReader(options: {
|
|
15
245
|
stream?: "stdout" | "stderr";
|
|
16
246
|
maxPendingLineBytes?: number;
|
|
247
|
+
oversizedLineProjector?: OversizedLineProjector;
|
|
17
248
|
onLine: (line: string) => void;
|
|
18
249
|
onLimit: (limit: ProtocolOutputLimit) => void;
|
|
19
250
|
}): {
|
|
@@ -27,17 +258,67 @@ export function createBoundedLineReader(options: {
|
|
|
27
258
|
}
|
|
28
259
|
let pending: Buffer[] = [];
|
|
29
260
|
let pendingBytes = 0;
|
|
261
|
+
let projectedPrefix: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
|
262
|
+
let projectedTail: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
|
263
|
+
let projectedBytes = 0;
|
|
264
|
+
let projection: OversizedLineProjection | undefined;
|
|
265
|
+
let projectingOversizedLine = false;
|
|
30
266
|
let limitExceeded = false;
|
|
31
267
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
268
|
+
const diagnosticTail = (prior: Buffer, segment: Buffer): Buffer => {
|
|
269
|
+
const tailFromSegment = segment.subarray(Math.max(0, segment.length - MAX_PROTOCOL_DIAGNOSTIC_BYTES));
|
|
270
|
+
return tailFromSegment.length === MAX_PROTOCOL_DIAGNOSTIC_BYTES
|
|
271
|
+
? tailFromSegment
|
|
272
|
+
: Buffer.concat([prior.subarray(Math.max(0, prior.length - (MAX_PROTOCOL_DIAGNOSTIC_BYTES - tailFromSegment.length))), tailFromSegment]);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const failLimit = (observedBytes: number, prefix: Buffer, tail: Buffer): false => {
|
|
276
|
+
limitExceeded = true;
|
|
277
|
+
pending = [];
|
|
278
|
+
pendingBytes = 0;
|
|
279
|
+
projectingOversizedLine = false;
|
|
280
|
+
projection = undefined;
|
|
281
|
+
projectedPrefix = Buffer.alloc(0);
|
|
282
|
+
projectedTail = Buffer.alloc(0);
|
|
283
|
+
projectedBytes = 0;
|
|
284
|
+
options.onLimit({
|
|
285
|
+
code: "protocol_output_limit",
|
|
286
|
+
stream: options.stream ?? "stdout",
|
|
287
|
+
limitBytes: maxPendingLineBytes,
|
|
288
|
+
observedBytes,
|
|
289
|
+
diagnosticPrefix: prefix.toString("utf8"),
|
|
290
|
+
diagnosticTail: tail.toString("utf8"),
|
|
291
|
+
});
|
|
292
|
+
return false;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const finishLine = (): void => {
|
|
296
|
+
if (projectingOversizedLine) {
|
|
297
|
+
const projected = projection?.finish();
|
|
298
|
+
if (projected === undefined) {
|
|
299
|
+
failLimit(projectedBytes, projectedPrefix, projectedTail);
|
|
300
|
+
} else {
|
|
301
|
+
options.onLine(projected);
|
|
302
|
+
}
|
|
303
|
+
} else if (pendingBytes > 0) {
|
|
304
|
+
options.onLine(Buffer.concat(pending, pendingBytes).toString("utf8"));
|
|
305
|
+
}
|
|
35
306
|
pending = [];
|
|
36
307
|
pendingBytes = 0;
|
|
308
|
+
projectingOversizedLine = false;
|
|
309
|
+
projection = undefined;
|
|
310
|
+
projectedPrefix = Buffer.alloc(0);
|
|
311
|
+
projectedTail = Buffer.alloc(0);
|
|
312
|
+
projectedBytes = 0;
|
|
37
313
|
};
|
|
38
314
|
|
|
39
315
|
const append = (segment: Buffer): boolean => {
|
|
40
316
|
if (segment.length === 0) return true;
|
|
317
|
+
if (projectingOversizedLine) {
|
|
318
|
+
projectedBytes += segment.length;
|
|
319
|
+
projectedTail = diagnosticTail(projectedTail, segment);
|
|
320
|
+
return projection?.push(segment) === true || failLimit(projectedBytes, projectedPrefix, projectedTail);
|
|
321
|
+
}
|
|
41
322
|
const observedBytes = pendingBytes + segment.length;
|
|
42
323
|
if (observedBytes > maxPendingLineBytes) {
|
|
43
324
|
const prior = pendingBytes > 0 ? Buffer.concat(pending, pendingBytes) : Buffer.alloc(0);
|
|
@@ -45,22 +326,20 @@ export function createBoundedLineReader(options: {
|
|
|
45
326
|
const prefix = prefixFromPrior.length === MAX_PROTOCOL_DIAGNOSTIC_BYTES
|
|
46
327
|
? prefixFromPrior
|
|
47
328
|
: Buffer.concat([prefixFromPrior, segment.subarray(0, MAX_PROTOCOL_DIAGNOSTIC_BYTES - prefixFromPrior.length)]);
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
return false;
|
|
329
|
+
const tail = diagnosticTail(prior, segment);
|
|
330
|
+
if (options.oversizedLineProjector?.accepts(prefix.toString("utf8"))) {
|
|
331
|
+
const candidate = options.oversizedLineProjector.create();
|
|
332
|
+
if (!candidate.push(prior) || !candidate.push(segment)) return failLimit(observedBytes, prefix, tail);
|
|
333
|
+
pending = [];
|
|
334
|
+
pendingBytes = 0;
|
|
335
|
+
projectingOversizedLine = true;
|
|
336
|
+
projection = candidate;
|
|
337
|
+
projectedPrefix = prefix;
|
|
338
|
+
projectedTail = tail;
|
|
339
|
+
projectedBytes = observedBytes;
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
return failLimit(observedBytes, prefix, tail);
|
|
64
343
|
}
|
|
65
344
|
pending.push(segment);
|
|
66
345
|
pendingBytes = observedBytes;
|
|
@@ -75,13 +354,14 @@ export function createBoundedLineReader(options: {
|
|
|
75
354
|
for (let index = 0; index < bytes.length; index++) {
|
|
76
355
|
if (bytes[index] !== 0x0a) continue;
|
|
77
356
|
if (!append(bytes.subarray(start, index))) return;
|
|
78
|
-
|
|
357
|
+
finishLine();
|
|
358
|
+
if (limitExceeded) return;
|
|
79
359
|
start = index + 1;
|
|
80
360
|
}
|
|
81
361
|
append(bytes.subarray(start));
|
|
82
362
|
},
|
|
83
363
|
end() {
|
|
84
|
-
if (!limitExceeded)
|
|
364
|
+
if (!limitExceeded) finishLine();
|
|
85
365
|
},
|
|
86
366
|
exceeded: () => limitExceeded,
|
|
87
367
|
};
|
|
@@ -100,7 +380,7 @@ export function createBoundedByteTail(maxBytes = MAX_CHILD_STDERR_BYTES): {
|
|
|
100
380
|
byteLength(): number;
|
|
101
381
|
} {
|
|
102
382
|
if (!Number.isInteger(maxBytes) || maxBytes < 1) throw new Error("maxBytes must be a positive integer.");
|
|
103
|
-
let tail = Buffer.alloc(0);
|
|
383
|
+
let tail: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
|
104
384
|
return {
|
|
105
385
|
push(chunk) {
|
|
106
386
|
const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
@@ -264,7 +264,7 @@ export function materializeDynamicParallelStep(step: DynamicParallelStep, output
|
|
|
264
264
|
export function collectDynamicResults(
|
|
265
265
|
step: DynamicParallelStep,
|
|
266
266
|
items: DynamicMaterializedItem[],
|
|
267
|
-
results: Array<Pick<SingleResult, "agent" | "
|
|
267
|
+
results: Array<Pick<SingleResult, "agent" | "error" | "timedOut" | "stopped" | "structuredOutput" | "artifactPaths" | "savedOutputPath"> & { exitCode: number | null; output?: string; finalOutput?: string }>,
|
|
268
268
|
): DynamicCollectedResult[] {
|
|
269
269
|
return items.map((entry, index) => {
|
|
270
270
|
const result = results[index];
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { finished } from "node:stream/promises";
|
|
5
|
+
import { trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
6
|
+
import type { ExternalProcessStatus } from "../../shared/types.ts";
|
|
7
|
+
|
|
8
|
+
const HARD_KILL_DELAY_MS = 2_000;
|
|
9
|
+
const MAX_OUTPUT_TAIL_BYTES = 64 * 1024;
|
|
10
|
+
const MAX_ERROR_TAIL_BYTES = 64 * 1024;
|
|
11
|
+
|
|
12
|
+
export function buildExternalCliPrompt(systemInstructions: string, task: string): string {
|
|
13
|
+
return `<System instructions>\n${systemInstructions.trim()}\n\n<Task>\n${task}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ExternalCliRunResult {
|
|
17
|
+
output: string;
|
|
18
|
+
exitCode: number | null;
|
|
19
|
+
error?: string;
|
|
20
|
+
timedOut?: boolean;
|
|
21
|
+
stopped?: boolean;
|
|
22
|
+
processSignal?: string | null;
|
|
23
|
+
externalProcess: ExternalProcessStatus;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function runExternalCli(input: {
|
|
27
|
+
command: string;
|
|
28
|
+
args?: string[];
|
|
29
|
+
cwd: string;
|
|
30
|
+
prompt: string;
|
|
31
|
+
asyncDir: string;
|
|
32
|
+
stepIndex: number;
|
|
33
|
+
registerTimeout?: (stop: (() => void) | undefined) => void;
|
|
34
|
+
registerStop?: (stop: (() => void) | undefined) => void;
|
|
35
|
+
timeoutMessage?: string;
|
|
36
|
+
stopMessage?: string;
|
|
37
|
+
onProcess?: (process: ExternalProcessStatus) => void;
|
|
38
|
+
}): Promise<ExternalCliRunResult> {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const startedAt = Date.now();
|
|
41
|
+
const stdoutPath = path.join(input.asyncDir, `external-${input.stepIndex}.stdout.log`);
|
|
42
|
+
const stderrPath = path.join(input.asyncDir, `external-${input.stepIndex}.stderr.log`);
|
|
43
|
+
fs.mkdirSync(input.asyncDir, { recursive: true });
|
|
44
|
+
const stdoutStream = fs.createWriteStream(stdoutPath, { flags: "w" });
|
|
45
|
+
const stderrStream = fs.createWriteStream(stderrPath, { flags: "w" });
|
|
46
|
+
const streamsFinished = Promise.allSettled([finished(stdoutStream), finished(stderrStream)]);
|
|
47
|
+
let stdoutTail = Buffer.alloc(0);
|
|
48
|
+
let stderrTail = Buffer.alloc(0);
|
|
49
|
+
let timedOut = false;
|
|
50
|
+
let stopped = false;
|
|
51
|
+
let settled = false;
|
|
52
|
+
let hardKillTimer: NodeJS.Timeout | undefined;
|
|
53
|
+
const child = spawn(input.command, input.args ?? [], {
|
|
54
|
+
cwd: input.cwd,
|
|
55
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
56
|
+
shell: false,
|
|
57
|
+
windowsHide: true,
|
|
58
|
+
});
|
|
59
|
+
const initialProcess: ExternalProcessStatus = {
|
|
60
|
+
...(typeof child.pid === "number" ? { pid: child.pid } : {}),
|
|
61
|
+
startedAt,
|
|
62
|
+
stdoutPath,
|
|
63
|
+
stderrPath,
|
|
64
|
+
};
|
|
65
|
+
input.onProcess?.(initialProcess);
|
|
66
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
67
|
+
stdoutStream.write(chunk);
|
|
68
|
+
stdoutTail = Buffer.concat([stdoutTail, chunk]);
|
|
69
|
+
if (stdoutTail.length > MAX_OUTPUT_TAIL_BYTES) stdoutTail = stdoutTail.subarray(stdoutTail.length - MAX_OUTPUT_TAIL_BYTES);
|
|
70
|
+
});
|
|
71
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
72
|
+
stderrStream.write(chunk);
|
|
73
|
+
stderrTail = Buffer.concat([stderrTail, chunk]);
|
|
74
|
+
if (stderrTail.length > MAX_ERROR_TAIL_BYTES) stderrTail = stderrTail.subarray(stderrTail.length - MAX_ERROR_TAIL_BYTES);
|
|
75
|
+
});
|
|
76
|
+
const terminate = (reason: "timeout" | "stop") => {
|
|
77
|
+
if (settled || timedOut || stopped) return;
|
|
78
|
+
timedOut = reason === "timeout";
|
|
79
|
+
stopped = reason === "stop";
|
|
80
|
+
trySignalChild(child, "SIGTERM");
|
|
81
|
+
hardKillTimer = setTimeout(() => {
|
|
82
|
+
if (!settled) trySignalChild(child, "SIGKILL");
|
|
83
|
+
}, HARD_KILL_DELAY_MS);
|
|
84
|
+
hardKillTimer.unref?.();
|
|
85
|
+
};
|
|
86
|
+
input.registerTimeout?.(() => terminate("timeout"));
|
|
87
|
+
input.registerStop?.(() => terminate("stop"));
|
|
88
|
+
child.stdin.on("error", () => {});
|
|
89
|
+
child.stdin.end(input.prompt);
|
|
90
|
+
let spawnError: Error | undefined;
|
|
91
|
+
child.once("error", (error) => { spawnError = error; });
|
|
92
|
+
child.once("close", (exitCode, signal) => {
|
|
93
|
+
settled = true;
|
|
94
|
+
if (hardKillTimer) clearTimeout(hardKillTimer);
|
|
95
|
+
input.registerTimeout?.(undefined);
|
|
96
|
+
input.registerStop?.(undefined);
|
|
97
|
+
const endedAt = Date.now();
|
|
98
|
+
const externalProcess: ExternalProcessStatus = {
|
|
99
|
+
...initialProcess,
|
|
100
|
+
endedAt,
|
|
101
|
+
durationMs: endedAt - startedAt,
|
|
102
|
+
exitCode,
|
|
103
|
+
processSignal: signal,
|
|
104
|
+
};
|
|
105
|
+
input.onProcess?.(externalProcess);
|
|
106
|
+
const stderr = stderrTail.toString("utf-8").trim();
|
|
107
|
+
const error = stopped
|
|
108
|
+
? input.stopMessage ?? "Subagent stopped by user."
|
|
109
|
+
: timedOut
|
|
110
|
+
? input.timeoutMessage ?? "Subagent timed out."
|
|
111
|
+
: spawnError?.message ?? (exitCode === 0 ? undefined : stderr || `External CLI exited with code ${exitCode}.`);
|
|
112
|
+
const result: ExternalCliRunResult = {
|
|
113
|
+
output: stdoutTail.toString("utf-8").trim(),
|
|
114
|
+
exitCode: timedOut || stopped || spawnError ? 1 : exitCode,
|
|
115
|
+
...(error ? { error } : {}),
|
|
116
|
+
...(timedOut ? { timedOut: true } : {}),
|
|
117
|
+
...(stopped ? { stopped: true } : {}),
|
|
118
|
+
processSignal: signal,
|
|
119
|
+
externalProcess,
|
|
120
|
+
};
|
|
121
|
+
stdoutStream.end();
|
|
122
|
+
stderrStream.end();
|
|
123
|
+
void streamsFinished.then((streamResults) => {
|
|
124
|
+
const streamFailure = streamResults.find((streamResult) => streamResult.status === "rejected");
|
|
125
|
+
if (streamFailure?.status === "rejected") reject(streamFailure.reason);
|
|
126
|
+
else resolve(result);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -96,8 +96,49 @@ function hasUnquotedFileRedirection(command: string): boolean {
|
|
|
96
96
|
return false;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
function unquotedShellText(command: string): string {
|
|
100
|
+
let inSingle = false;
|
|
101
|
+
let inDouble = false;
|
|
102
|
+
let escaped = false;
|
|
103
|
+
let result = "";
|
|
104
|
+
for (const char of command) {
|
|
105
|
+
if (escaped) {
|
|
106
|
+
result += inSingle || inDouble ? "_" : char;
|
|
107
|
+
escaped = false;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (char === "\\" && !inSingle) {
|
|
111
|
+
escaped = true;
|
|
112
|
+
result += inDouble ? "_" : char;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (char === "'" && !inDouble) {
|
|
116
|
+
inSingle = !inSingle;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (char === '"' && !inSingle) {
|
|
120
|
+
inDouble = !inDouble;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
result += inSingle || inDouble ? "_" : char;
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function hasMutatingGitCommand(command: string): boolean {
|
|
129
|
+
const unquoted = unquotedShellText(command);
|
|
130
|
+
for (const segment of unquoted.split(/(?:&&|\|\||[;|()\n])/)) {
|
|
131
|
+
const trimmed = segment.trim();
|
|
132
|
+
if (!trimmed || /^(?:echo|printf)\b/.test(trimmed)) continue;
|
|
133
|
+
if (/^git\s+(?:(?:(?:-C|--git-dir|--work-tree)\s+\S+|(?:--git-dir|--work-tree)=\S+|--paginate)\s+)*(?:add|commit|push)\b/.test(trimmed)) return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
99
138
|
export function isMutatingBashCommand(command: string): boolean {
|
|
100
|
-
return hasUnquotedFileRedirection(command)
|
|
139
|
+
return hasUnquotedFileRedirection(command)
|
|
140
|
+
|| hasMutatingGitCommand(command)
|
|
141
|
+
|| MUTATING_BASH_PATTERNS.some((pattern) => pattern.test(command));
|
|
101
142
|
}
|
|
102
143
|
|
|
103
144
|
export function isMutatingTool(toolName: string | undefined, args: Record<string, unknown> | undefined): boolean {
|