pi-subagents 0.35.0 → 0.36.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 +59 -0
- package/README.md +132 -24
- package/agents/advisor.md +73 -0
- package/package.json +8 -12
- package/skills/pi-subagents/SKILL.md +22 -9
- package/src/agents/agents.ts +22 -5
- package/src/api/delegation.ts +125 -0
- package/src/extension/config.ts +7 -1
- package/src/extension/index.ts +50 -38
- package/src/extension/rpc.ts +27 -2
- package/src/extension/schemas.ts +22 -2
- package/src/extension/tool-description.ts +2 -2
- package/src/intercom/intercom-bridge.ts +1 -1
- package/src/intercom/native-supervisor-channel.ts +45 -6
- package/src/intercom/result-intercom.ts +7 -0
- package/src/runs/background/async-execution.ts +34 -4
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +27 -5
- package/src/runs/background/async-status.ts +76 -3
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/completion-batcher.ts +6 -4
- package/src/runs/background/completion-dedupe.ts +2 -11
- package/src/runs/background/fleet-view.ts +9 -4
- package/src/runs/background/notify.ts +132 -120
- package/src/runs/background/result-watcher.ts +138 -78
- package/src/runs/background/run-status.ts +3 -1
- package/src/runs/background/subagent-runner.ts +225 -43
- package/src/runs/background/subagent-wait.ts +130 -4
- package/src/runs/background/wait-tool.ts +2 -2
- package/src/runs/foreground/chain-execution.ts +176 -111
- package/src/runs/foreground/execution.ts +90 -36
- package/src/runs/foreground/foreground-control.ts +90 -0
- package/src/runs/foreground/subagent-executor.ts +394 -163
- package/src/runs/shared/acceptance.ts +55 -13
- package/src/runs/shared/agent-contract.ts +38 -0
- package/src/runs/shared/child-protocol.ts +1 -1
- package/src/runs/shared/completion-guard.ts +36 -5
- package/src/runs/shared/context-mode.ts +44 -0
- package/src/runs/shared/dynamic-fanout.ts +4 -4
- package/src/runs/shared/long-running-guard.ts +4 -0
- package/src/runs/shared/nested-events.ts +27 -2
- package/src/runs/shared/parallel-handoff.ts +154 -0
- package/src/runs/shared/parallel-utils.ts +6 -0
- package/src/runs/shared/pi-args.ts +23 -14
- package/src/runs/shared/run-history.ts +90 -5
- package/src/runs/shared/structured-output.ts +112 -7
- package/src/runs/shared/subagent-control.ts +4 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
- package/src/runs/shared/task-intent.ts +10 -5
- package/src/runs/shared/tool-availability.ts +3 -1
- package/src/runs/shared/tool-budget.ts +11 -5
- package/src/runs/shared/turn-budget.ts +2 -1
- package/src/runs/shared/worktree.ts +63 -14
- package/src/shared/accessible-dir.ts +25 -0
- package/src/shared/artifacts.ts +37 -7
- package/src/shared/atomic-json.ts +14 -42
- package/src/shared/child-transcript.ts +52 -0
- package/src/shared/file-system-retry.ts +47 -0
- package/src/shared/settings.ts +9 -1
- package/src/shared/types.ts +211 -22
- package/src/slash/delegation-adapters.ts +152 -5
- package/src/slash/delegation-json.ts +108 -0
- package/src/slash/delegation-request.ts +182 -36
- package/src/slash/prompt-template-bridge.ts +222 -37
- package/src/slash/selector.ts +147 -0
- package/src/slash/slash-commands.ts +14 -5
- package/src/slash/slash-live-state.ts +2 -2
- package/src/slash/subagents-admin.ts +42 -42
- package/src/tui/fleet-status.ts +362 -0
- package/src/tui/fleet-transcript.ts +472 -0
- package/src/tui/fleet.ts +318 -59
- package/src/tui/render.ts +25 -15
- package/src/watchdog/change-signature.ts +105 -12
- package/src/watchdog/review.ts +7 -2
- package/src/watchdog/runtime.ts +5 -3
- package/src/slash/subagents-editor.ts +0 -86
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
AcceptanceConfig,
|
|
7
7
|
AcceptanceEvidenceKind,
|
|
8
8
|
AcceptanceInput,
|
|
9
|
+
AgentContract,
|
|
9
10
|
AcceptanceLedger,
|
|
10
11
|
AcceptanceLevel,
|
|
11
12
|
AcceptanceReport,
|
|
@@ -20,6 +21,7 @@ import type {
|
|
|
20
21
|
SingleResult,
|
|
21
22
|
SubagentRunMode,
|
|
22
23
|
} from "../../shared/types.ts";
|
|
24
|
+
import { isAgentContractV1 } from "./agent-contract.ts";
|
|
23
25
|
import { classifyTaskMutationIntent, taskMayMutate } from "./task-intent.ts";
|
|
24
26
|
|
|
25
27
|
const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
|
|
@@ -90,9 +92,9 @@ function inferLevel(input: {
|
|
|
90
92
|
&& intent.kind !== "read-only"
|
|
91
93
|
&& !/\b(?:do not|don't|must not)\s+patch\b/.test(task)
|
|
92
94
|
&& /\bpatch\s+(?:(?:\.{0,2}[\\/])?(?:[\w.-]+[\\/])+[\w.-]+|[\w.-]+\.[a-z0-9]+\b|(?:the\s+)?parser\b)/.test(task);
|
|
93
|
-
const taskMayWrite = taskMayMutate(input.task ?? "") || intent.kind === "implementation" || rolePatchTask;
|
|
95
|
+
const taskMayWrite = readOnlyTask ? false : taskMayMutate(input.task ?? "") || intent.kind === "implementation" || rolePatchTask;
|
|
94
96
|
const readOnlyAgent = input.acceptanceRole === "read-only"
|
|
95
|
-
|| (input.acceptanceRole === undefined && /\b(?:reviewer|scout|context-builder|researcher|analyst)\b/.test(agent));
|
|
97
|
+
|| (input.acceptanceRole === undefined && /\b(?:reviewer|oracle|scout|context-builder|researcher|analyst)\b/.test(agent));
|
|
96
98
|
const writeTask = taskMayWrite
|
|
97
99
|
|| (input.acceptanceRole === "writer" && !readOnlyTask)
|
|
98
100
|
|| (input.acceptanceRole === undefined && /\bworker\b/.test(agent) && !readOnlyTask);
|
|
@@ -324,10 +326,32 @@ export function resolveEffectiveAcceptance(input: {
|
|
|
324
326
|
async?: boolean;
|
|
325
327
|
dynamic?: boolean;
|
|
326
328
|
dynamicGroup?: boolean;
|
|
329
|
+
agentContract?: AgentContract;
|
|
327
330
|
}): ResolvedAcceptanceConfig {
|
|
328
331
|
const explicit = normalizeAcceptanceInput(input.explicit);
|
|
329
|
-
const inferred = inferLevel(input);
|
|
330
332
|
const explicitLevel = normalizeLevel(explicit.level);
|
|
333
|
+
if (isAgentContractV1(input.agentContract)) {
|
|
334
|
+
const level = explicitAcceptanceCanDisable(explicit) || explicitLevel === "auto"
|
|
335
|
+
? "none"
|
|
336
|
+
: explicitLevel;
|
|
337
|
+
const evidence = unique(explicit.evidence ?? []);
|
|
338
|
+
const criteria = normalizeCriteria(
|
|
339
|
+
explicit.criteria as Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }> | undefined,
|
|
340
|
+
evidence,
|
|
341
|
+
);
|
|
342
|
+
return {
|
|
343
|
+
level,
|
|
344
|
+
explicit: input.explicit !== undefined,
|
|
345
|
+
inferredReason: [],
|
|
346
|
+
criteria,
|
|
347
|
+
evidence,
|
|
348
|
+
verify: explicit.verify ?? [],
|
|
349
|
+
review: explicit.review,
|
|
350
|
+
stopRules: explicit.stopRules ?? [],
|
|
351
|
+
reason: explicit.reason,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const inferred = inferLevel(input);
|
|
331
355
|
const level = explicitAcceptanceCanDisable(explicit)
|
|
332
356
|
? "none"
|
|
333
357
|
: explicitLevel === "auto"
|
|
@@ -355,8 +379,13 @@ export function resolveEffectiveAcceptance(input: {
|
|
|
355
379
|
};
|
|
356
380
|
}
|
|
357
381
|
|
|
358
|
-
|
|
382
|
+
function acceptanceRequiresChildReport(acceptance: ResolvedAcceptanceConfig): boolean {
|
|
383
|
+
return acceptance.criteria.length > 0 || acceptance.evidence.length > 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig, options: { reportOptional?: boolean } = {}): string {
|
|
359
387
|
if (acceptance.level === "none") return "";
|
|
388
|
+
if (options.reportOptional && !acceptanceRequiresChildReport(acceptance)) return "";
|
|
360
389
|
const lines = [
|
|
361
390
|
"",
|
|
362
391
|
"## Acceptance Contract",
|
|
@@ -1029,6 +1058,7 @@ export async function evaluateAcceptance(input: {
|
|
|
1029
1058
|
reviewResult?: AcceptanceReviewResult;
|
|
1030
1059
|
signal?: AbortSignal;
|
|
1031
1060
|
abortMessage?: string;
|
|
1061
|
+
reportOptional?: boolean;
|
|
1032
1062
|
}): Promise<AcceptanceLedger> {
|
|
1033
1063
|
const acceptance = input.acceptance;
|
|
1034
1064
|
const ledger: AcceptanceLedger = {
|
|
@@ -1050,26 +1080,30 @@ export async function evaluateAcceptance(input: {
|
|
|
1050
1080
|
: { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}` };
|
|
1051
1081
|
})()
|
|
1052
1082
|
: parseAcceptanceReportSources(input.output, input.fileOutput);
|
|
1083
|
+
const needsReport = acceptanceRequiresChildReport(acceptance);
|
|
1053
1084
|
if (parsed.report) {
|
|
1054
1085
|
ledger.childReport = parsed.report;
|
|
1055
1086
|
ledger.status = "attested";
|
|
1056
|
-
} else {
|
|
1087
|
+
} else if (!input.reportOptional || needsReport || parsed.error !== ACCEPTANCE_REPORT_NOT_FOUND) {
|
|
1057
1088
|
ledger.childReportParseError = parsed.error;
|
|
1058
1089
|
ledger.runtimeChecks.push({ id: "attestation", status: "failed", message: parsed.error ?? "Structured acceptance report missing." });
|
|
1059
|
-
|
|
1060
|
-
|
|
1090
|
+
if (!input.reportOptional) {
|
|
1091
|
+
ledger.status = "rejected";
|
|
1092
|
+
return ledger;
|
|
1093
|
+
}
|
|
1094
|
+
} else {
|
|
1095
|
+
ledger.childReportParseError = parsed.error;
|
|
1061
1096
|
}
|
|
1062
1097
|
|
|
1063
|
-
if (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked) {
|
|
1098
|
+
if (parsed.report && LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked) {
|
|
1064
1099
|
ledger.runtimeChecks = [
|
|
1100
|
+
...ledger.runtimeChecks,
|
|
1065
1101
|
...checkCriteriaSatisfied(acceptance.criteria, parsed.report),
|
|
1066
1102
|
...runStructuralChecks(acceptance, parsed.report, input.cwd),
|
|
1067
1103
|
];
|
|
1068
|
-
if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1069
|
-
ledger.status = "
|
|
1070
|
-
return ledger;
|
|
1104
|
+
if (!ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1105
|
+
ledger.status = "checked";
|
|
1071
1106
|
}
|
|
1072
|
-
ledger.status = "checked";
|
|
1073
1107
|
}
|
|
1074
1108
|
|
|
1075
1109
|
if (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.verified && (acceptance.level === "verified" || acceptance.verify.length > 0)) {
|
|
@@ -1087,7 +1121,15 @@ export async function evaluateAcceptance(input: {
|
|
|
1087
1121
|
ledger.status = "rejected";
|
|
1088
1122
|
return ledger;
|
|
1089
1123
|
}
|
|
1090
|
-
ledger.status = "verified";
|
|
1124
|
+
if (!ledger.runtimeChecks.some((check) => check.status === "failed")) ledger.status = "verified";
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
1128
|
+
ledger.status = "rejected";
|
|
1129
|
+
return ledger;
|
|
1130
|
+
}
|
|
1131
|
+
if (ledger.status === "claimed" && acceptance.level !== "reviewed") {
|
|
1132
|
+
ledger.status = acceptance.level === "verified" ? "verified" : acceptance.level;
|
|
1091
1133
|
}
|
|
1092
1134
|
|
|
1093
1135
|
if (acceptance.level === "reviewed") {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AgentContract, EffectsProjection, ExecutionProjection, ReviewProjection, SingleResult } from "../../shared/types.ts";
|
|
2
|
+
|
|
3
|
+
export function isAgentContractV1(contract: AgentContract | undefined): boolean {
|
|
4
|
+
return contract?.version === 1;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function buildExecutionProjection(result: Pick<SingleResult, "exitCode" | "error" | "interrupted" | "timedOut" | "stopped" | "detached">): ExecutionProjection {
|
|
8
|
+
if (result.detached) {
|
|
9
|
+
return { status: "detached", success: false, exitCode: result.exitCode, detached: true, ...(result.error ? { error: result.error } : {}) };
|
|
10
|
+
}
|
|
11
|
+
if (result.stopped) {
|
|
12
|
+
return { status: "stopped", success: false, exitCode: result.exitCode, stopped: true, ...(result.error ? { error: result.error } : {}) };
|
|
13
|
+
}
|
|
14
|
+
if (result.interrupted) {
|
|
15
|
+
return { status: "paused", success: true, exitCode: result.exitCode, interrupted: true, ...(result.error ? { error: result.error } : {}) };
|
|
16
|
+
}
|
|
17
|
+
const success = result.exitCode === 0 && !result.error && !result.timedOut;
|
|
18
|
+
return {
|
|
19
|
+
status: success ? "completed" : "failed",
|
|
20
|
+
success,
|
|
21
|
+
exitCode: result.exitCode,
|
|
22
|
+
...(result.error ? { error: result.error } : {}),
|
|
23
|
+
...(result.timedOut ? { timedOut: true } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildReviewProjection(result: Pick<SingleResult, "acceptance">): ReviewProjection {
|
|
28
|
+
const review = result.acceptance?.reviewResult;
|
|
29
|
+
if (!review) return { status: "not-requested" };
|
|
30
|
+
return { status: review.status, findings: review.findings };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function attachContractProjections<T extends SingleResult>(result: T): T {
|
|
34
|
+
result.execution = buildExecutionProjection(result);
|
|
35
|
+
result.review = buildReviewProjection(result);
|
|
36
|
+
if (!result.effects) result.effects = {} satisfies EffectsProjection;
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
@@ -3,7 +3,7 @@ import type { ProtocolOutputLimit } from "../../shared/types.ts";
|
|
|
3
3
|
|
|
4
4
|
export type { ProtocolOutputLimit } from "../../shared/types.ts";
|
|
5
5
|
|
|
6
|
-
export const MAX_CHILD_PENDING_LINE_BYTES =
|
|
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
9
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
-
import {
|
|
2
|
+
import { isMutatingTool } from "./long-running-guard.ts";
|
|
3
3
|
import { expectsImplementationMutation } from "./task-intent.ts";
|
|
4
4
|
|
|
5
5
|
export { expectsImplementationMutation };
|
|
@@ -16,6 +16,12 @@ const READ_ONLY_BUILTIN_TOOLS = new Set([
|
|
|
16
16
|
"contact_supervisor",
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
|
+
// Cursor native edit/write often land as thinking traces (inactive_trace /
|
|
20
|
+
// transcript_trace) rather than toolCall parts when native tool replay is off
|
|
21
|
+
// or the tool is inactive in context. Match pi-cursor-sdk display labels.
|
|
22
|
+
const CURSOR_FILE_MUTATION_THINKING =
|
|
23
|
+
/(?:^|\n)\s*Cursor (?:edit|write)\s*:/i;
|
|
24
|
+
|
|
19
25
|
interface CompletionMutationGuardInput {
|
|
20
26
|
agent: string;
|
|
21
27
|
task: string;
|
|
@@ -31,21 +37,46 @@ interface CompletionMutationGuardResult {
|
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export function hasMutationToolCapability(tools: string[] | undefined, mcpDirectTools: string[] | undefined): boolean {
|
|
34
|
-
if (
|
|
40
|
+
if ((mcpDirectTools?.length ?? 0) > 0) return true;
|
|
41
|
+
if (tools === undefined) return true;
|
|
35
42
|
return !tools.every((tool) => READ_ONLY_BUILTIN_TOOLS.has(tool));
|
|
36
43
|
}
|
|
37
44
|
|
|
45
|
+
function hasCheckpointMutationEvidence(message: Message): boolean {
|
|
46
|
+
const record = message as unknown as {
|
|
47
|
+
role?: string;
|
|
48
|
+
type?: string;
|
|
49
|
+
customType?: unknown;
|
|
50
|
+
data?: unknown;
|
|
51
|
+
details?: unknown;
|
|
52
|
+
};
|
|
53
|
+
if ((record.role !== "custom" && record.type !== "custom") || record.customType !== "pi-checkpoint") return false;
|
|
54
|
+
const details = typeof record.details === "object" && record.details !== null && !Array.isArray(record.details)
|
|
55
|
+
? record.details as Record<string, unknown>
|
|
56
|
+
: undefined;
|
|
57
|
+
const data = typeof record.data === "object" && record.data !== null && !Array.isArray(record.data)
|
|
58
|
+
? record.data as Record<string, unknown>
|
|
59
|
+
: typeof details?.beforeCommit === "string" || typeof details?.afterCommit === "string"
|
|
60
|
+
? details
|
|
61
|
+
: details?.data && typeof details.data === "object" && !Array.isArray(details.data)
|
|
62
|
+
? details.data as Record<string, unknown>
|
|
63
|
+
: undefined;
|
|
64
|
+
return typeof data?.beforeCommit === "string"
|
|
65
|
+
&& typeof data.afterCommit === "string"
|
|
66
|
+
&& data.beforeCommit !== data.afterCommit;
|
|
67
|
+
}
|
|
68
|
+
|
|
38
69
|
export function hasMutationToolCall(messages: Message[]): boolean {
|
|
39
70
|
for (const message of messages) {
|
|
71
|
+
if (hasCheckpointMutationEvidence(message)) return true;
|
|
40
72
|
if (message.role !== "assistant") continue;
|
|
41
73
|
for (const part of message.content) {
|
|
74
|
+
if (part.type === "thinking" && CURSOR_FILE_MUTATION_THINKING.test(part.thinking)) return true;
|
|
42
75
|
if (part.type !== "toolCall") continue;
|
|
43
|
-
if (part.name === "edit" || part.name === "write") return true;
|
|
44
|
-
if (part.name !== "bash") continue;
|
|
45
76
|
const args = typeof part.arguments === "object" && part.arguments !== null && !Array.isArray(part.arguments)
|
|
46
77
|
? part.arguments as Record<string, unknown>
|
|
47
78
|
: {};
|
|
48
|
-
if (
|
|
79
|
+
if (isMutatingTool(part.name, args)) return true;
|
|
49
80
|
}
|
|
50
81
|
}
|
|
51
82
|
return false;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type ContextMode = "fresh" | "fork";
|
|
2
|
+
export type ContextSummary = ContextMode | "mixed";
|
|
3
|
+
|
|
4
|
+
export function isContextMode(value: unknown): value is ContextMode {
|
|
5
|
+
return value === "fresh" || value === "fork";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isContextSummary(value: unknown): value is ContextSummary {
|
|
9
|
+
return isContextMode(value) || value === "mixed";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function summarizeContextModes(modes: Array<ContextMode | undefined>): ContextSummary | undefined {
|
|
13
|
+
const resolved = modes.filter(isContextMode);
|
|
14
|
+
if (resolved.length === 0) return undefined;
|
|
15
|
+
const first = resolved[0]!;
|
|
16
|
+
return resolved.every((mode) => mode === first) ? first : "mixed";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function contextModeLabel(mode: ContextMode | ContextSummary | undefined): string {
|
|
20
|
+
if (mode === "fork") return "[fork]";
|
|
21
|
+
if (mode === "fresh") return "[fresh]";
|
|
22
|
+
if (mode === "mixed") return "[mixed]";
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function contextModeBadge(
|
|
27
|
+
theme: { fg(name: string, text: string): string },
|
|
28
|
+
mode: ContextMode | ContextSummary | undefined,
|
|
29
|
+
): string {
|
|
30
|
+
const label = contextModeLabel(mode);
|
|
31
|
+
if (!label) return "";
|
|
32
|
+
if (mode === "fork") return theme.fg("warning", ` ${label}`);
|
|
33
|
+
return theme.fg("dim", ` ${label}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function contextModePrefix(
|
|
37
|
+
theme: { fg(name: string, text: string): string },
|
|
38
|
+
mode: ContextMode | ContextSummary | undefined,
|
|
39
|
+
): string {
|
|
40
|
+
const label = contextModeLabel(mode);
|
|
41
|
+
if (!label) return "";
|
|
42
|
+
if (mode === "fork") return `${theme.fg("warning", label)} `;
|
|
43
|
+
return `${theme.fg("dim", label)} `;
|
|
44
|
+
}
|
|
@@ -42,11 +42,11 @@ const SAFE_OUTPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
42
42
|
const ITEM_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
43
43
|
const ITEM_REF_PATTERN = /\{([A-Za-z_][A-Za-z0-9_]*)(?:\.([^{}]+))?\}/g;
|
|
44
44
|
const RESERVED_TEMPLATE_NAMES = new Set(["task", "previous", "chain_dir", "outputs"]);
|
|
45
|
-
const DYNAMIC_STEP_KEYS = new Set(["expand", "parallel", "collect", "concurrency", "failFast", "phase", "label", "acceptance"]);
|
|
45
|
+
const DYNAMIC_STEP_KEYS = new Set(["expand", "parallel", "collect", "concurrency", "failFast", "phase", "label", "acceptance", "agentContract", "gateOn"]);
|
|
46
46
|
const RUNNER_DYNAMIC_STEP_KEYS = new Set([...DYNAMIC_STEP_KEYS, "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "sessionFiles", "thinkingOverrides"]);
|
|
47
47
|
const DYNAMIC_EXPAND_KEYS = new Set(["from", "item", "key", "maxItems", "onEmpty"]);
|
|
48
48
|
const DYNAMIC_EXPAND_FROM_KEYS = new Set(["output", "path"]);
|
|
49
|
-
const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "output", "outputMode", "reads", "progress", "skill", "model", "toolBudget", "acceptance"]);
|
|
49
|
+
const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "output", "outputMode", "reads", "progress", "skill", "model", "toolBudget", "acceptance", "agentContract", "gateOn"]);
|
|
50
50
|
const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
51
51
|
...DYNAMIC_PARALLEL_KEYS,
|
|
52
52
|
"outputName", "structured", "inheritProjectContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "waitToolEnabled",
|
|
@@ -288,9 +288,9 @@ export function collectDynamicResults(
|
|
|
288
288
|
});
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
export function validateDynamicCollection(schema: JsonSchemaObject | undefined, value: DynamicCollectedResult[]): void {
|
|
291
|
+
export async function validateDynamicCollection(schema: JsonSchemaObject | undefined, value: DynamicCollectedResult[]): Promise<void> {
|
|
292
292
|
if (!schema) return;
|
|
293
|
-
const validation = validateStructuredOutputValue(schema, value);
|
|
293
|
+
const validation = await validateStructuredOutputValue(schema, value);
|
|
294
294
|
if (validation.status === "invalid") {
|
|
295
295
|
throw new DynamicFanoutError(`Collected output validation failed: ${validation.message}`);
|
|
296
296
|
}
|
|
@@ -103,6 +103,10 @@ export function isMutatingBashCommand(command: string): boolean {
|
|
|
103
103
|
export function isMutatingTool(toolName: string | undefined, args: Record<string, unknown> | undefined): boolean {
|
|
104
104
|
if (!toolName) return false;
|
|
105
105
|
if (toolName === "edit" || toolName === "write") return true;
|
|
106
|
+
if (toolName === "cursor") {
|
|
107
|
+
const activityTitle = typeof args?.activityTitle === "string" ? args.activityTitle : "";
|
|
108
|
+
return /^Cursor (?:edit|write)\b/i.test(activityTitle);
|
|
109
|
+
}
|
|
106
110
|
if (toolName !== "bash") return false;
|
|
107
111
|
const command = typeof args?.command === "string" ? args.command : "";
|
|
108
112
|
if (!command.trim()) return false;
|
|
@@ -612,7 +612,9 @@ export function projectNestedEvents(route: NestedRoute): NestedRegistry {
|
|
|
612
612
|
} catch {
|
|
613
613
|
continue;
|
|
614
614
|
}
|
|
615
|
-
|
|
615
|
+
const records = parseNestedEventRecords(content, route);
|
|
616
|
+
if (records.length === 0) continue;
|
|
617
|
+
for (const event of records) {
|
|
616
618
|
registry = applyNestedEvent(registry, event);
|
|
617
619
|
changed = true;
|
|
618
620
|
}
|
|
@@ -620,11 +622,34 @@ export function projectNestedEvents(route: NestedRoute): NestedRegistry {
|
|
|
620
622
|
changed = true;
|
|
621
623
|
}
|
|
622
624
|
if (changed) {
|
|
623
|
-
|
|
625
|
+
const processedEvents = [...seen];
|
|
626
|
+
const retainedEvents = processedEvents.slice(-1000);
|
|
627
|
+
const evictedEvents = processedEvents.slice(0, -1000);
|
|
628
|
+
registry = { ...registry, processedEvents: retainedEvents };
|
|
624
629
|
// Parent projection is the only writer to this sidecar registry. Child and
|
|
625
630
|
// runner processes only create immutable event files, so parent status.json
|
|
626
631
|
// remains owned by the existing runner writer and is never rewritten here.
|
|
627
632
|
writeAtomicJson(registryPath(route), registry);
|
|
633
|
+
|
|
634
|
+
// The registry retains only a bounded filename cursor. Remove the old
|
|
635
|
+
// immutable status records after the cursor is durable; otherwise an evicted
|
|
636
|
+
// filename is rediscovered on the next projection and replayed forever.
|
|
637
|
+
for (const entry of evictedEvents) {
|
|
638
|
+
const eventPath = path.join(route.eventSink, entry);
|
|
639
|
+
if (!containedPath(route.eventSink, eventPath)) continue;
|
|
640
|
+
try {
|
|
641
|
+
const stat = fs.statSync(eventPath);
|
|
642
|
+
if (!stat.isFile() || stat.size > MAX_EVENT_BYTES) continue;
|
|
643
|
+
const content = fs.readFileSync(eventPath, "utf-8");
|
|
644
|
+
const hasStatusRecord = parseNestedEventRecords(content, route).length > 0;
|
|
645
|
+
const hasControlResult = content
|
|
646
|
+
.split("\n")
|
|
647
|
+
.some((line) => line.trim() && parseControlResult(line.trim(), route));
|
|
648
|
+
if (hasStatusRecord && !hasControlResult) fs.unlinkSync(eventPath);
|
|
649
|
+
} catch {
|
|
650
|
+
// A cleanup failure must not make a successfully projected status fail.
|
|
651
|
+
}
|
|
652
|
+
}
|
|
628
653
|
}
|
|
629
654
|
return registry;
|
|
630
655
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
|
+
import type {
|
|
5
|
+
ParallelHandoffGroup,
|
|
6
|
+
ParallelHandoffManifest,
|
|
7
|
+
ParallelHandoffReference,
|
|
8
|
+
SubagentResultStatus,
|
|
9
|
+
} from "../../shared/types.ts";
|
|
10
|
+
import type {
|
|
11
|
+
WorktreeCleanupReport,
|
|
12
|
+
WorktreeDiff,
|
|
13
|
+
WorktreeSetup,
|
|
14
|
+
} from "./worktree.ts";
|
|
15
|
+
|
|
16
|
+
export interface ParallelHandoffResult {
|
|
17
|
+
agent: string;
|
|
18
|
+
status: SubagentResultStatus;
|
|
19
|
+
summary: string;
|
|
20
|
+
outputPath?: string;
|
|
21
|
+
structuredOutput?: unknown;
|
|
22
|
+
structuredOutputPath?: string;
|
|
23
|
+
sessionPath?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readManifest(manifestPath: string): ParallelHandoffManifest | undefined {
|
|
27
|
+
if (!fs.existsSync(manifestPath)) return undefined;
|
|
28
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as ParallelHandoffManifest;
|
|
29
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.groups)) {
|
|
30
|
+
throw new Error(`Invalid parallel handoff manifest: ${manifestPath}`);
|
|
31
|
+
}
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function referenceFor(manifestPath: string, manifest: ParallelHandoffManifest): ParallelHandoffReference {
|
|
36
|
+
const children = manifest.groups.flatMap((group) => group.children);
|
|
37
|
+
return {
|
|
38
|
+
version: 1,
|
|
39
|
+
path: manifestPath,
|
|
40
|
+
groupCount: manifest.groups.length,
|
|
41
|
+
childCount: children.length,
|
|
42
|
+
changedPatches: children.filter((child) => child.patch.changed).length,
|
|
43
|
+
cleanupState: manifest.groups.every((group) => group.cleanup.state === "complete") ? "complete" : "partial",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function safeHandoffAgentName(agent: string): string {
|
|
48
|
+
return agent.replace(/[^\w.-]/g, "_");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function missingDiff(input: { manifestPath: string; stepIndex: number; taskIndex: number; agent: string; branch?: string }): WorktreeDiff {
|
|
52
|
+
const patchPath = path.join(path.dirname(input.manifestPath), `missing-diff-step-${input.stepIndex}-task-${input.taskIndex}-${safeHandoffAgentName(input.agent)}.patch`);
|
|
53
|
+
try {
|
|
54
|
+
fs.mkdirSync(path.dirname(patchPath), { recursive: true });
|
|
55
|
+
fs.writeFileSync(patchPath, "", "utf-8");
|
|
56
|
+
} catch {
|
|
57
|
+
// Handoff records the artifact failure below; patch creation remains best-effort.
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
index: input.taskIndex,
|
|
61
|
+
agent: input.agent,
|
|
62
|
+
branch: input.branch ?? "",
|
|
63
|
+
diffStat: "",
|
|
64
|
+
filesChanged: 0,
|
|
65
|
+
insertions: 0,
|
|
66
|
+
deletions: 0,
|
|
67
|
+
patchPath,
|
|
68
|
+
error: "diff artifact unavailable; no patch was captured",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function writeParallelHandoffGroup(input: {
|
|
73
|
+
manifestPath: string;
|
|
74
|
+
runId: string;
|
|
75
|
+
mode: "parallel" | "chain";
|
|
76
|
+
source: "foreground" | "async";
|
|
77
|
+
cwd: string;
|
|
78
|
+
stepIndex: number;
|
|
79
|
+
flatStartIndex: number;
|
|
80
|
+
setup: WorktreeSetup;
|
|
81
|
+
diffs: WorktreeDiff[];
|
|
82
|
+
cleanup: WorktreeCleanupReport;
|
|
83
|
+
results: ParallelHandoffResult[];
|
|
84
|
+
now?: number;
|
|
85
|
+
}): ParallelHandoffReference {
|
|
86
|
+
const now = input.now ?? Date.now();
|
|
87
|
+
const existing = readManifest(input.manifestPath);
|
|
88
|
+
if (existing && (existing.runId !== input.runId || existing.mode !== input.mode || existing.source !== input.source)) {
|
|
89
|
+
throw new Error(`Parallel handoff manifest belongs to a different run: ${input.manifestPath}`);
|
|
90
|
+
}
|
|
91
|
+
const group: ParallelHandoffGroup = {
|
|
92
|
+
stepIndex: input.stepIndex,
|
|
93
|
+
baseCommit: input.setup.baseCommit,
|
|
94
|
+
repoRoot: input.setup.cwd,
|
|
95
|
+
children: input.results.map((result, taskIndex) => {
|
|
96
|
+
const diff = input.diffs[taskIndex] ?? missingDiff({
|
|
97
|
+
manifestPath: input.manifestPath,
|
|
98
|
+
stepIndex: input.stepIndex,
|
|
99
|
+
taskIndex,
|
|
100
|
+
agent: result.agent,
|
|
101
|
+
branch: input.setup.worktrees[taskIndex]?.branch,
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
index: input.flatStartIndex + taskIndex,
|
|
105
|
+
taskIndex,
|
|
106
|
+
agent: result.agent,
|
|
107
|
+
status: result.status,
|
|
108
|
+
summary: result.summary,
|
|
109
|
+
...(result.outputPath ? { outputPath: result.outputPath } : {}),
|
|
110
|
+
...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
|
|
111
|
+
...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
|
|
112
|
+
...(result.sessionPath ? { sessionPath: result.sessionPath } : {}),
|
|
113
|
+
patch: {
|
|
114
|
+
path: diff.patchPath,
|
|
115
|
+
branch: diff.branch,
|
|
116
|
+
changed: diff.filesChanged > 0 || diff.insertions > 0 || diff.deletions > 0 || diff.diffStat.trim().length > 0,
|
|
117
|
+
diffStat: diff.diffStat,
|
|
118
|
+
filesChanged: diff.filesChanged,
|
|
119
|
+
insertions: diff.insertions,
|
|
120
|
+
deletions: diff.deletions,
|
|
121
|
+
...(diff.error ? { error: diff.error } : {}),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}),
|
|
125
|
+
cleanup: input.cleanup,
|
|
126
|
+
};
|
|
127
|
+
const groups = existing?.groups.filter((candidate) => candidate.stepIndex !== input.stepIndex) ?? [];
|
|
128
|
+
groups.push(group);
|
|
129
|
+
groups.sort((left, right) => left.stepIndex - right.stepIndex);
|
|
130
|
+
const manifest: ParallelHandoffManifest = {
|
|
131
|
+
version: 1,
|
|
132
|
+
runId: input.runId,
|
|
133
|
+
mode: input.mode,
|
|
134
|
+
source: input.source,
|
|
135
|
+
cwd: input.cwd,
|
|
136
|
+
createdAt: existing?.createdAt ?? now,
|
|
137
|
+
updatedAt: now,
|
|
138
|
+
groups,
|
|
139
|
+
};
|
|
140
|
+
writeAtomicJson(input.manifestPath, manifest);
|
|
141
|
+
return referenceFor(input.manifestPath, manifest);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function parallelHandoffPath(baseDir: string, runId?: string): string {
|
|
145
|
+
return runId ? path.join(baseDir, "handoffs", `${runId}.json`) : path.join(baseDir, "handoff.json");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function formatParallelHandoffReference(reference: ParallelHandoffReference): string {
|
|
149
|
+
return `Parallel handoff: ${reference.path} (${reference.childCount} children, ${reference.changedPatches} changed patches, cleanup ${reference.cleanupState})`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function formatParallelHandoffError(error: unknown): string {
|
|
153
|
+
return `Parallel handoff unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
154
|
+
}
|
|
@@ -3,6 +3,8 @@ export interface RunnerSubagentStep {
|
|
|
3
3
|
parentSessionId?: string;
|
|
4
4
|
agent: string;
|
|
5
5
|
task: string;
|
|
6
|
+
/** Resolved launch context for this child. */
|
|
7
|
+
context?: "fresh" | "fork";
|
|
6
8
|
importAsyncRoot?: {
|
|
7
9
|
runId: string;
|
|
8
10
|
asyncDir: string;
|
|
@@ -40,9 +42,11 @@ export interface RunnerSubagentStep {
|
|
|
40
42
|
outputPath: string;
|
|
41
43
|
};
|
|
42
44
|
structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject;
|
|
45
|
+
agentContract?: import("../../shared/types.ts").AgentContract;
|
|
43
46
|
effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
|
|
44
47
|
acceptanceInput?: import("../../shared/types.ts").AcceptanceInput;
|
|
45
48
|
acceptanceRole?: import("../../shared/types.ts").AcceptanceRole;
|
|
49
|
+
gateOn?: import("../../shared/types.ts").ChainGateLayer;
|
|
46
50
|
toolBudget?: import("../../shared/types.ts").ResolvedToolBudget;
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -66,6 +70,8 @@ export interface DynamicRunnerGroup {
|
|
|
66
70
|
effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
|
|
67
71
|
acceptanceInput?: import("../../shared/types.ts").AcceptanceInput;
|
|
68
72
|
acceptanceRole?: import("../../shared/types.ts").AcceptanceRole;
|
|
73
|
+
agentContract?: import("../../shared/types.ts").AgentContract;
|
|
74
|
+
gateOn?: import("../../shared/types.ts").ChainGateLayer;
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup;
|
|
@@ -4,13 +4,15 @@ import * as path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { encodeNestedPathEnv, parseNestedPathEnv, type NestedPathEntry } from "./nested-path.ts";
|
|
6
6
|
import { resolveMcpDirectToolNames } from "./mcp-direct-tool-allowlist.ts";
|
|
7
|
+
import { resolvePiPackageRoot } from "./pi-spawn.ts";
|
|
7
8
|
import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV } from "./structured-output.ts";
|
|
8
9
|
import { TEMP_ROOT_DIR, type JsonSchemaObject, type ResolvedToolBudget } from "../../shared/types.ts";
|
|
9
10
|
import { THINKING_LEVELS } from "../../shared/model-info.ts";
|
|
10
|
-
import { TOOL_BUDGET_ENV, encodeToolBudgetEnv } from "./tool-budget.ts";
|
|
11
|
+
import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV, encodeToolBudgetEnv } from "./tool-budget.ts";
|
|
11
12
|
import { CHILD_TOOL_DIAGNOSTIC_PATH_ENV, REQUIRED_CHILD_TOOLS_ENV } from "./tool-availability.ts";
|
|
12
13
|
import { CHILD_WATCHDOG_CONFIG_ENV, encodeChildWatchdogConfig, type ChildWatchdogConfig } from "../../watchdog/child-status.ts";
|
|
13
14
|
import { WAIT_TOOL_ENABLED_ENV } from "../background/wait-config.ts";
|
|
15
|
+
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
|
|
14
16
|
|
|
15
17
|
const TASK_ARG_LIMIT = 8000;
|
|
16
18
|
const PROMPT_RUNTIME_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-prompt-runtime.ts");
|
|
@@ -78,6 +80,7 @@ interface BuildPiArgsInput {
|
|
|
78
80
|
outputPath: string;
|
|
79
81
|
};
|
|
80
82
|
toolBudget?: ResolvedToolBudget;
|
|
83
|
+
allowZeroToolBudget?: boolean;
|
|
81
84
|
childWatchdog?: ChildWatchdogConfig;
|
|
82
85
|
waitToolEnabled?: boolean;
|
|
83
86
|
}
|
|
@@ -127,27 +130,30 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
127
130
|
args.push("--model", modelArg);
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
const explicitToolAllowlist = input.tools !== undefined || (input.mcpDirectTools?.length ?? 0) > 0;
|
|
130
134
|
const declaredBuiltinToolsBase = input.tools?.filter((tool) => !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) ?? [];
|
|
131
|
-
const declaredBuiltinTools = input.requireReadTool &&
|
|
135
|
+
const declaredBuiltinTools = input.requireReadTool && declaredBuiltinToolsBase.length > 0 && !declaredBuiltinToolsBase.includes("read")
|
|
132
136
|
? ["read", ...declaredBuiltinToolsBase]
|
|
133
137
|
: declaredBuiltinToolsBase;
|
|
134
138
|
const fanoutAuthorized = declaredBuiltinTools.includes("subagent");
|
|
135
139
|
const toolExtensionPaths: string[] = [];
|
|
136
140
|
let requiredChildTools: string[] = [];
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (!declaredBuiltinTools.includes(tool) && (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) {
|
|
141
|
-
toolExtensionPaths.push(tool);
|
|
142
|
-
}
|
|
141
|
+
for (const tool of input.tools ?? []) {
|
|
142
|
+
if (!declaredBuiltinTools.includes(tool) && (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) {
|
|
143
|
+
toolExtensionPaths.push(tool);
|
|
143
144
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
}
|
|
146
|
+
if (explicitToolAllowlist) {
|
|
147
|
+
const allowedTools = [
|
|
148
|
+
...declaredBuiltinTools,
|
|
149
|
+
...resolveMcpDirectToolNames(input.mcpDirectTools, input.cwd),
|
|
150
|
+
...(input.structuredOutput ? ["structured_output"] : []),
|
|
151
|
+
];
|
|
152
|
+
requiredChildTools = [...new Set(allowedTools)];
|
|
153
|
+
if (requiredChildTools.length > 0) {
|
|
150
154
|
args.push("--tools", requiredChildTools.join(","));
|
|
155
|
+
} else {
|
|
156
|
+
args.push("--no-tools");
|
|
151
157
|
}
|
|
152
158
|
}
|
|
153
159
|
|
|
@@ -190,6 +196,8 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
const env: Record<string, string | undefined> = {};
|
|
199
|
+
const piPackageRoot = process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] ?? resolvePiPackageRoot();
|
|
200
|
+
if (piPackageRoot) env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] = piPackageRoot;
|
|
193
201
|
let toolDiagnosticPath: string | undefined;
|
|
194
202
|
if (requiredChildTools.length > 0) {
|
|
195
203
|
if (!tempDir) tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
@@ -278,6 +286,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
278
286
|
if (input.steerAckDir) env[SUBAGENT_STEER_ACK_DIR_ENV] = input.steerAckDir;
|
|
279
287
|
const encodedToolBudget = encodeToolBudgetEnv(input.toolBudget);
|
|
280
288
|
if (encodedToolBudget) env[TOOL_BUDGET_ENV] = encodedToolBudget;
|
|
289
|
+
env[TOOL_BUDGET_ZERO_AUTH_ENV] = input.allowZeroToolBudget ? "1" : undefined;
|
|
281
290
|
const encodedChildWatchdog = encodeChildWatchdogConfig(input.childWatchdog);
|
|
282
291
|
if (encodedChildWatchdog) env[CHILD_WATCHDOG_CONFIG_ENV] = encodedChildWatchdog;
|
|
283
292
|
|