pi-subagents 0.61.0 → 0.63.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 +52 -1
- package/docs/agents.md +11 -6
- package/docs/configuration.md +31 -5
- package/docs/extension-api.md +2 -2
- package/docs/models.md +5 -5
- package/docs/observability.md +5 -2
- package/docs/tool-reference.md +3 -3
- package/install.mjs +0 -1
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +3 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
- package/src/agents/agent-management.ts +41 -4
- package/src/agents/agent-serializer.ts +3 -0
- package/src/agents/agents.ts +120 -124
- package/src/agents/runtime-agent-registry.ts +5 -1
- package/src/api/preflight.ts +4 -0
- package/src/api/shared-types.ts +3 -0
- package/src/extension/config.ts +20 -0
- package/src/extension/public-execution.ts +1 -0
- package/src/extension/schemas.ts +6 -2
- package/src/extension/tool-description.ts +1 -1
- package/src/inspectors/herdr/inspector-runner.ts +19 -13
- package/src/runs/background/active-async-capacity.ts +26 -8
- package/src/runs/background/async-execution.ts +100 -23
- package/src/runs/background/async-resume.ts +6 -2
- package/src/runs/background/async-status.ts +18 -2
- package/src/runs/background/notify.ts +13 -1
- package/src/runs/background/process-terminal.ts +16 -0
- package/src/runs/background/run-status.ts +22 -2
- package/src/runs/background/scheduled-runs.ts +63 -6
- package/src/runs/background/steering.ts +4 -1
- package/src/runs/background/subagent-runner.ts +44 -9
- package/src/runs/background/wait-completions.ts +13 -0
- package/src/runs/background/wait-tool.ts +1 -7
- package/src/runs/foreground/execution.ts +14 -7
- package/src/runs/foreground/subagent-executor.ts +38 -5
- package/src/runs/shared/acceptance.ts +85 -18
- package/src/runs/shared/capability-ceiling.ts +1 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/lane-metadata.ts +24 -3
- package/src/runs/shared/parallel-handoff.ts +4 -0
- package/src/runs/shared/parallel-utils.ts +2 -6
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/pi-args.ts +32 -14
- package/src/runs/shared/pi-spawn.ts +69 -35
- package/src/runs/shared/structured-output.ts +33 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
- package/src/runs/shared/task-intent.ts +21 -7
- package/src/runs/shared/tool-timeout.ts +1 -1
- package/src/runs/shared/worktree.ts +467 -63
- package/src/shared/atomic-json.ts +3 -1
- package/src/shared/fork-context.ts +0 -12
- package/src/shared/fork-session-cwd.ts +27 -0
- package/src/shared/launch-contract.ts +3 -0
- package/src/shared/types.ts +32 -1
- package/src/shared/utils.ts +18 -7
- package/src/slash/slash-commands.ts +1 -1
- package/src/slash/subagents-admin.ts +26 -12
- package/src/tui/fleet-status.ts +61 -2
- package/src/tui/fleet.ts +12 -7
- package/src/tui/render.ts +222 -14
- package/src/workflows/workflow-checklist.ts +441 -0
|
@@ -50,7 +50,7 @@ import { acquireActiveAsyncCapacity, ActiveAsyncCapacityError, getActiveAsyncCap
|
|
|
50
50
|
import { isScheduledRunAction } from "../background/scheduled-runs.ts";
|
|
51
51
|
import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
|
|
52
52
|
import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
|
|
53
|
-
import { normalizeGateAcceptance, validateExecutionAcceptance } from "../shared/acceptance.ts";
|
|
53
|
+
import { normalizeGateAcceptance, resolveAcceptanceReportMode, validateExecutionAcceptance } from "../shared/acceptance.ts";
|
|
54
54
|
import { canPreferFork, createForkContextResolver, forkedChildRequiresThinkingOff, resolveSubagentLaunchContext } from "../../shared/fork-context.ts";
|
|
55
55
|
import { createPrunedForkSessionWriter } from "../../shared/pruned-fork.ts";
|
|
56
56
|
import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
|
|
@@ -393,6 +393,7 @@ export interface SubagentParamsLike {
|
|
|
393
393
|
agentContract?: AgentContract;
|
|
394
394
|
at?: string;
|
|
395
395
|
every?: string;
|
|
396
|
+
sessionOnly?: boolean;
|
|
396
397
|
on?: string | number;
|
|
397
398
|
timezone?: string;
|
|
398
399
|
overlap?: "skip";
|
|
@@ -1313,6 +1314,9 @@ function appendStepToAsyncChain(input: {
|
|
|
1313
1314
|
waitToolEnabled: input.deps.waitToolEnabled,
|
|
1314
1315
|
waitToolDefaultTimeoutMs: input.deps.waitToolDefaultTimeoutMs,
|
|
1315
1316
|
contextForAgent: contextPolicy.contextForAgent,
|
|
1317
|
+
worktreeBaseDir: input.deps.config.worktreeBaseDir,
|
|
1318
|
+
worktreeProvider: input.deps.config.worktreeProvider,
|
|
1319
|
+
worktreeBranchPrefix: input.deps.config.worktreeBranchPrefix,
|
|
1316
1320
|
asyncDir: resolved.location.asyncDir,
|
|
1317
1321
|
validateOutputBindings: false,
|
|
1318
1322
|
capabilityCeiling: intersectSubagentCapabilityCeilings(status.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(asyncCtx.currentSessionId)),
|
|
@@ -1686,6 +1690,8 @@ async function resumeExternalJobFollowUp(input: {
|
|
|
1686
1690
|
worktreeSetupHook: input.deps.config.worktreeSetupHook,
|
|
1687
1691
|
worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
|
|
1688
1692
|
worktreeBaseDir: input.deps.config.worktreeBaseDir,
|
|
1693
|
+
worktreeProvider: input.deps.config.worktreeProvider,
|
|
1694
|
+
worktreeBranchPrefix: input.deps.config.worktreeBranchPrefix,
|
|
1689
1695
|
controlConfig: resolveControlConfig(input.deps.config.control, undefined),
|
|
1690
1696
|
controlIntercomTarget: input.intercomBridge.active ? input.intercomBridge.orchestratorTarget : undefined,
|
|
1691
1697
|
childIntercomTarget: input.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
|
|
@@ -1942,6 +1948,8 @@ async function resumeAsyncRun(input: {
|
|
|
1942
1948
|
worktreeSetupHook: input.deps.config.worktreeSetupHook,
|
|
1943
1949
|
worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
|
|
1944
1950
|
worktreeBaseDir: input.deps.config.worktreeBaseDir,
|
|
1951
|
+
worktreeProvider: input.deps.config.worktreeProvider,
|
|
1952
|
+
worktreeBranchPrefix: input.deps.config.worktreeBranchPrefix,
|
|
1945
1953
|
controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
|
|
1946
1954
|
controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
|
|
1947
1955
|
childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
|
|
@@ -2055,7 +2063,11 @@ async function resumeAsyncRun(input: {
|
|
|
2055
2063
|
worktreeSetupHook: input.deps.config.worktreeSetupHook,
|
|
2056
2064
|
worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
|
|
2057
2065
|
worktreeBaseDir: input.deps.config.worktreeBaseDir,
|
|
2058
|
-
|
|
2066
|
+
worktreeProvider: input.deps.config.worktreeProvider,
|
|
2067
|
+
worktreeBranchPrefix: input.deps.config.worktreeBranchPrefix,
|
|
2068
|
+
// A retained async child already owns the recorded worktree. Resume it in
|
|
2069
|
+
// place rather than allocating a second provider worktree around it.
|
|
2070
|
+
worktree: input.params.worktree === true && !("managedWorktree" in target && target.managedWorktree === true),
|
|
2059
2071
|
lane: input.params.lane ?? recoveryDescriptor?.lane,
|
|
2060
2072
|
controlConfig: recoveryDescriptor?.controlConfig ?? resolveControlConfig(input.deps.config.control, input.params.control),
|
|
2061
2073
|
intercomBridge: input.params.intercomBridge ?? recoveryDescriptor?.intercomBridge,
|
|
@@ -3254,6 +3266,8 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
3254
3266
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
3255
3267
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
3256
3268
|
worktreeBaseDir: deps.config.worktreeBaseDir,
|
|
3269
|
+
worktreeProvider: deps.config.worktreeProvider,
|
|
3270
|
+
worktreeBranchPrefix: deps.config.worktreeBranchPrefix,
|
|
3257
3271
|
controlConfig,
|
|
3258
3272
|
intercomBridge: params.intercomBridge,
|
|
3259
3273
|
controlIntercomTarget,
|
|
@@ -3290,6 +3304,10 @@ function createSingleWorktreeSetup(
|
|
|
3290
3304
|
setupHook: ExtensionConfig["worktreeSetupHook"],
|
|
3291
3305
|
setupHookTimeoutMs: ExtensionConfig["worktreeSetupHookTimeoutMs"],
|
|
3292
3306
|
baseDir: ExtensionConfig["worktreeBaseDir"],
|
|
3307
|
+
provider: ExtensionConfig["worktreeProvider"],
|
|
3308
|
+
branchPrefix: ExtensionConfig["worktreeBranchPrefix"],
|
|
3309
|
+
label?: string,
|
|
3310
|
+
task?: string,
|
|
3293
3311
|
beforeCreate?: (setup: WorktreeSetup) => void,
|
|
3294
3312
|
): { setup?: WorktreeSetup; errorResult?: AgentToolResult<Details> } {
|
|
3295
3313
|
if (!enabled) return {};
|
|
@@ -3301,6 +3319,10 @@ function createSingleWorktreeSetup(
|
|
|
3301
3319
|
? { hookPath: setupHook, ...(setupHookTimeoutMs === undefined ? {} : { timeoutMs: setupHookTimeoutMs }) }
|
|
3302
3320
|
: undefined,
|
|
3303
3321
|
baseDir,
|
|
3322
|
+
provider,
|
|
3323
|
+
branchPrefix,
|
|
3324
|
+
labels: [label],
|
|
3325
|
+
tasks: [task],
|
|
3304
3326
|
beforeCreate,
|
|
3305
3327
|
})),
|
|
3306
3328
|
};
|
|
@@ -3661,6 +3683,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3661
3683
|
deps.config.worktreeSetupHook,
|
|
3662
3684
|
deps.config.worktreeSetupHookTimeoutMs,
|
|
3663
3685
|
deps.config.worktreeBaseDir,
|
|
3686
|
+
deps.config.worktreeProvider,
|
|
3687
|
+
deps.config.worktreeBranchPrefix,
|
|
3688
|
+
params.lane?.key ?? params.workflowKey,
|
|
3689
|
+
task,
|
|
3664
3690
|
(plannedSetup) => {
|
|
3665
3691
|
pendingHandoff = writePendingParallelHandoff({
|
|
3666
3692
|
manifestPath: parallelHandoffPath(artifactsDir, runId),
|
|
@@ -3690,7 +3716,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3690
3716
|
return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
|
|
3691
3717
|
}
|
|
3692
3718
|
const structuredRuntime = params.outputSchema
|
|
3693
|
-
? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined, {
|
|
3719
|
+
? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined, { acceptanceReport: resolveAcceptanceReportMode(params.acceptance) })
|
|
3694
3720
|
: undefined;
|
|
3695
3721
|
// Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
|
|
3696
3722
|
// absolute paths pass through; relative paths resolve against the child cwd.
|
|
@@ -4163,6 +4189,10 @@ function workflowOutputPathMappingSummary(children: WorkflowScriptChildResult[])
|
|
|
4163
4189
|
return mappings.length > 0 ? ` Output path mappings: ${mappings.join("; ")}.` : "";
|
|
4164
4190
|
}
|
|
4165
4191
|
|
|
4192
|
+
function workflowDetailsResults(children: WorkflowScriptChildResult[]): SingleResult[] {
|
|
4193
|
+
return children.flatMap((child) => (child.results ?? []).map((result) => result.workflowKey ? result : { ...result, workflowKey: child.key }));
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4166
4196
|
function workflowSteerReceipt(key: string, result: AgentToolResult<Details>): WorkflowSteerResult {
|
|
4167
4197
|
const steering = result.details.steering;
|
|
4168
4198
|
const error = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n") || undefined;
|
|
@@ -5518,7 +5548,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5518
5548
|
const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
|
|
5519
5549
|
return attachWorkflowMission(withRunFanoutBudget({
|
|
5520
5550
|
content: [{ type: "text", text: displayText }],
|
|
5521
|
-
details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflow.children
|
|
5551
|
+
details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflowDetailsResults(workflow.children), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: finalPreflightTrace, emits: workflow.emits, console: workflow.console, ...(workflowResource ? { resource: workflowResource.provenance } : {}), ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
|
|
5522
5552
|
}, workflowFanoutBudget));
|
|
5523
5553
|
} catch (error) {
|
|
5524
5554
|
const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
|
|
@@ -5545,7 +5575,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5545
5575
|
return attachWorkflowMission(withRunFanoutBudget({
|
|
5546
5576
|
content: [{ type: "text", text: displayText }],
|
|
5547
5577
|
isError: true,
|
|
5548
|
-
details: compactOptional<Details>({ mode: "workflow", runId: _id, results: partial.children
|
|
5578
|
+
details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflowDetailsResults(partial.children), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: finalPreflightTrace, emits: partial.emits, console: partial.console, ...(workflowResource ? { resource: workflowResource.provenance } : {}), ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
|
|
5549
5579
|
}, workflowFanoutBudget));
|
|
5550
5580
|
}
|
|
5551
5581
|
}
|
|
@@ -6244,6 +6274,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
6244
6274
|
const canonicalParams = canonicalizeExecutionParams(effectiveParams, discoveredAgents, discovered.agentDiagnostics, unknownAgentDiagnosticContext);
|
|
6245
6275
|
if (canonicalParams.error) return buildRequestedModeError(effectiveParams, canonicalParams.error);
|
|
6246
6276
|
effectiveParams = canonicalParams.params!;
|
|
6277
|
+
if (effectiveParams.worktree === undefined && deps.config.worktree !== undefined) {
|
|
6278
|
+
effectiveParams = { ...effectiveParams, worktree: deps.config.worktree };
|
|
6279
|
+
}
|
|
6247
6280
|
const modelScope = discovered.modelScope;
|
|
6248
6281
|
effectiveParams = applySingleAgentLaunchDefaults(effectiveParams, discoveredAgents);
|
|
6249
6282
|
// An agent-level defaultContext is a preference, unlike an explicit request.
|
|
@@ -47,7 +47,7 @@ const VALID_EVIDENCE_KINDS: AcceptanceEvidenceKind[] = [
|
|
|
47
47
|
const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>(VALID_EVIDENCE_KINDS);
|
|
48
48
|
const ACCEPTANCE_EVIDENCE_HELP = `Supported evidence kinds: ${VALID_EVIDENCE_KINDS.join(", ")}. Example: { level: "checked", evidence: ["commands-run", "changed-files"] }.`;
|
|
49
49
|
const ACCEPTANCE_OBJECT_EXAMPLE = "Example: { level: \"checked\", evidence: [\"commands-run\", \"changed-files\"] }.";
|
|
50
|
-
const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
|
|
50
|
+
const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "report", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
|
|
51
51
|
const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
|
|
52
52
|
const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
|
|
53
53
|
const ACCEPTANCE_REVIEW_KEYS = new Set(["agent", "focus", "required"]);
|
|
@@ -100,12 +100,13 @@ function inferLevel(input: {
|
|
|
100
100
|
const writeTask = taskMayWrite
|
|
101
101
|
|| (input.acceptanceRole === "writer" && !readOnlyTask)
|
|
102
102
|
|| (input.acceptanceRole === undefined && /\bworker\b/.test(agent) && !readOnlyTask);
|
|
103
|
-
const inferredReadOnly = readOnlyTask || (input.acceptanceRole === "read-only" && !taskMayWrite);
|
|
103
|
+
const inferredReadOnly = readOnlyTask || ((readOnlyAgent || input.acceptanceRole === "read-only") && !taskMayWrite);
|
|
104
104
|
const roleResolvesReadOnly = input.acceptanceRole !== undefined && inferredReadOnly;
|
|
105
|
+
const dynamicResolvesReadOnly = inferredReadOnly && !writeTask;
|
|
105
106
|
const keywordRiskReadOnly = input.acceptanceRole === undefined ? intent.kind === "read-only" : inferredReadOnly;
|
|
106
107
|
const risky = Boolean(input.async && writeTask)
|
|
107
|
-
|| (Boolean(input.dynamic) && !roleResolvesReadOnly)
|
|
108
|
-
|| (Boolean(input.dynamicGroup) && !roleResolvesReadOnly)
|
|
108
|
+
|| (Boolean(input.dynamic) && !roleResolvesReadOnly && !dynamicResolvesReadOnly)
|
|
109
|
+
|| (Boolean(input.dynamicGroup) && !roleResolvesReadOnly && !dynamicResolvesReadOnly)
|
|
109
110
|
|| (!keywordRiskReadOnly && /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task));
|
|
110
111
|
|
|
111
112
|
if (risky) {
|
|
@@ -131,7 +132,7 @@ function inferLevel(input: {
|
|
|
131
132
|
if (readOnlyAgent || readOnlyTask) {
|
|
132
133
|
reasons.push(input.acceptanceRole === "read-only" && !readOnlyTask ? "declared read-only acceptance role" : readOnlyAgent ? "read-only/reviewer-style agent" : "read-only task wording");
|
|
133
134
|
return {
|
|
134
|
-
level: "
|
|
135
|
+
level: "none",
|
|
135
136
|
reasons,
|
|
136
137
|
criteria: ["Return concrete findings with file paths and severity when applicable"],
|
|
137
138
|
evidence: ["review-findings", "residual-risks"],
|
|
@@ -146,11 +147,46 @@ function inferLevel(input: {
|
|
|
146
147
|
};
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
|
|
150
|
+
type AcceptanceInputNormalizationResult = { value: unknown; error?: string };
|
|
151
|
+
|
|
152
|
+
function normalizeAcceptanceValue(input: unknown, pathLabel = "acceptance"): AcceptanceInputNormalizationResult {
|
|
153
|
+
if (typeof input !== "string") return { value: input };
|
|
154
|
+
const trimmed = input.trim();
|
|
155
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return { value: input };
|
|
156
|
+
let parsed: unknown;
|
|
157
|
+
try {
|
|
158
|
+
parsed = JSON.parse(trimmed);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return {
|
|
161
|
+
value: input,
|
|
162
|
+
error: `${pathLabel} JSON string must encode a valid acceptance object: ${error instanceof Error ? error.message : String(error)}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
166
|
+
const kind = parsed === null ? "null" : Array.isArray(parsed) ? "an array" : typeof parsed;
|
|
167
|
+
return { value: input, error: `${pathLabel} JSON string must encode an object; got ${kind}.` };
|
|
168
|
+
}
|
|
169
|
+
return { value: parsed };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function normalizeAcceptanceInput(input: unknown): AcceptanceConfig {
|
|
173
|
+
const normalized = normalizeAcceptanceValue(input);
|
|
174
|
+
if (normalized.error) throw new Error(normalized.error);
|
|
175
|
+
input = normalized.value;
|
|
150
176
|
if (input === undefined || input === "auto") return { level: "auto" };
|
|
151
177
|
if (input === false) return { level: "none", reason: "disabled by deprecated false shorthand" };
|
|
152
|
-
if (typeof input === "string") return { level: input };
|
|
153
|
-
return { ...input };
|
|
178
|
+
if (typeof input === "string") return { level: input as AcceptanceConfig["level"] };
|
|
179
|
+
return { ...(input as AcceptanceConfig) };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type ResolvedAcceptanceReportMode = "off" | "optional" | "required";
|
|
183
|
+
|
|
184
|
+
export function resolveAcceptanceReportMode(input: unknown): ResolvedAcceptanceReportMode {
|
|
185
|
+
const normalized = normalizeAcceptanceValue(input);
|
|
186
|
+
if (normalized.error) throw new Error(normalized.error);
|
|
187
|
+
const value = normalized.value;
|
|
188
|
+
const report = value && typeof value === "object" && !Array.isArray(value) ? (value as AcceptanceConfig).report : undefined;
|
|
189
|
+
return value === false || report === "off" ? "off" : report === "on" ? "required" : "optional";
|
|
154
190
|
}
|
|
155
191
|
|
|
156
192
|
type GateAcceptanceNormalizationResult =
|
|
@@ -158,7 +194,11 @@ type GateAcceptanceNormalizationResult =
|
|
|
158
194
|
| { ok: false; error: string };
|
|
159
195
|
|
|
160
196
|
export function normalizeGateAcceptance(gate: unknown, acceptance: AcceptanceInput | undefined): GateAcceptanceNormalizationResult {
|
|
161
|
-
if (gate === undefined)
|
|
197
|
+
if (gate === undefined) {
|
|
198
|
+
if (acceptance === undefined) return { ok: true };
|
|
199
|
+
const normalized = normalizeAcceptanceValue(acceptance);
|
|
200
|
+
return normalized.error ? { ok: false, error: normalized.error } : { ok: true, acceptance: normalized.value as AcceptanceInput };
|
|
201
|
+
}
|
|
162
202
|
if (typeof gate !== "string" || !gate.trim()) return { ok: false, error: "gate must be a non-empty command string." };
|
|
163
203
|
if (acceptance !== undefined) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." };
|
|
164
204
|
return { ok: true, acceptance: { level: "verified", verify: [{ id: "gate", command: gate.trim() }] } };
|
|
@@ -168,6 +208,10 @@ function explicitAcceptanceCanDisable(explicit: AcceptanceConfig): boolean {
|
|
|
168
208
|
return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
|
|
169
209
|
}
|
|
170
210
|
|
|
211
|
+
function explicitAcceptanceRequestsPolicy(explicit: AcceptanceConfig): boolean {
|
|
212
|
+
return (explicit.level !== undefined && explicit.level !== "auto") || Object.keys(explicit).some((key) => key !== "level");
|
|
213
|
+
}
|
|
214
|
+
|
|
171
215
|
function unsupportedEvidenceKindMessage(pathLabel: string, item: unknown): string {
|
|
172
216
|
const value = typeof item === "string" ? ` "${item}"` : "";
|
|
173
217
|
return `${pathLabel}${value} is not a supported evidence kind. ${ACCEPTANCE_EVIDENCE_HELP}`;
|
|
@@ -177,6 +221,9 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
177
221
|
const errors: string[] = [];
|
|
178
222
|
if (input === undefined) return errors;
|
|
179
223
|
if (input === false) return errors;
|
|
224
|
+
const normalized = normalizeAcceptanceValue(input, pathLabel);
|
|
225
|
+
if (normalized.error) return [normalized.error];
|
|
226
|
+
input = normalized.value;
|
|
180
227
|
if (typeof input === "string") {
|
|
181
228
|
if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
182
229
|
else if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
|
|
@@ -197,6 +244,9 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
197
244
|
} else if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level as AcceptanceLevel))) {
|
|
198
245
|
errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified.`);
|
|
199
246
|
}
|
|
247
|
+
if (value.report !== undefined && value.report !== "on" && value.report !== "off") {
|
|
248
|
+
errors.push(`${pathLabel}.report must be on or off.`);
|
|
249
|
+
}
|
|
200
250
|
if (value.level === "none" && (typeof value.reason !== "string" || !value.reason.trim())) {
|
|
201
251
|
errors.push(`${pathLabel}.reason is required when level is none.`);
|
|
202
252
|
}
|
|
@@ -304,29 +354,45 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
304
354
|
|
|
305
355
|
export function validateExecutionAcceptance(input: {
|
|
306
356
|
acceptance?: unknown;
|
|
307
|
-
|
|
357
|
+
outputSchema?: unknown;
|
|
358
|
+
tasks?: Array<{ acceptance?: unknown; outputSchema?: unknown }>;
|
|
308
359
|
chain?: Array<{
|
|
309
360
|
acceptance?: unknown;
|
|
310
|
-
|
|
361
|
+
outputSchema?: unknown;
|
|
362
|
+
parallel?: Array<{ acceptance?: unknown; outputSchema?: unknown }> | { acceptance?: unknown; outputSchema?: unknown };
|
|
311
363
|
}>;
|
|
312
364
|
}): string[] {
|
|
313
365
|
const errors = validateAcceptanceInput(input.acceptance, "acceptance");
|
|
366
|
+
errors.push(...validateAcceptanceReportMode(input.acceptance, input.outputSchema, "acceptance"));
|
|
314
367
|
for (const [index, task] of (input.tasks ?? []).entries()) {
|
|
315
368
|
errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
|
|
369
|
+
errors.push(...validateAcceptanceReportMode(task.acceptance, task.outputSchema, `tasks[${index}].acceptance`));
|
|
316
370
|
}
|
|
317
371
|
for (const [stepIndex, step] of (input.chain ?? []).entries()) {
|
|
318
372
|
errors.push(...validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`));
|
|
373
|
+
errors.push(...validateAcceptanceReportMode(step.acceptance, step.outputSchema, `chain[${stepIndex}].acceptance`));
|
|
319
374
|
if (Array.isArray(step.parallel)) {
|
|
320
375
|
for (const [taskIndex, task] of step.parallel.entries()) {
|
|
321
376
|
errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
|
|
377
|
+
errors.push(...validateAcceptanceReportMode(task.acceptance, task.outputSchema, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
|
|
322
378
|
}
|
|
323
379
|
} else if (step.parallel) {
|
|
324
380
|
errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
|
|
381
|
+
errors.push(...validateAcceptanceReportMode(step.parallel.acceptance, step.parallel.outputSchema, `chain[${stepIndex}].parallel.acceptance`));
|
|
325
382
|
}
|
|
326
383
|
}
|
|
327
384
|
return errors;
|
|
328
385
|
}
|
|
329
386
|
|
|
387
|
+
function validateAcceptanceReportMode(acceptance: unknown, outputSchema: unknown, pathLabel: string): string[] {
|
|
388
|
+
const normalized = normalizeAcceptanceValue(acceptance, pathLabel);
|
|
389
|
+
if (normalized.error) return [];
|
|
390
|
+
acceptance = normalized.value;
|
|
391
|
+
if (!acceptance || typeof acceptance !== "object" || Array.isArray(acceptance)) return [];
|
|
392
|
+
if (!("report" in acceptance)) return [];
|
|
393
|
+
return outputSchema === undefined ? [`${pathLabel}.report requires outputSchema.`] : [];
|
|
394
|
+
}
|
|
395
|
+
|
|
330
396
|
function normalizeCriteria(criteria: Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }> | undefined, evidence: AcceptanceEvidenceKind[]): ResolvedAcceptanceGate[] {
|
|
331
397
|
return (criteria ?? []).map((criterion, index): ResolvedAcceptanceGate => {
|
|
332
398
|
if (typeof criterion === "string") {
|
|
@@ -376,12 +442,13 @@ export function resolveEffectiveAcceptance(input: {
|
|
|
376
442
|
};
|
|
377
443
|
}
|
|
378
444
|
const inferred = inferLevel(input);
|
|
445
|
+
const inferredLevel = inferred.level === "none" && explicitAcceptanceRequestsPolicy(explicit) ? "attested" : inferred.level;
|
|
379
446
|
const level = explicitAcceptanceCanDisable(explicit)
|
|
380
447
|
? "none"
|
|
381
448
|
: explicitLevel === "auto"
|
|
382
|
-
?
|
|
383
|
-
: (LEVEL_RANK[explicitLevel] >= LEVEL_RANK[
|
|
384
|
-
const evidence = unique([...(level ===
|
|
449
|
+
? inferredLevel
|
|
450
|
+
: (LEVEL_RANK[explicitLevel] >= LEVEL_RANK[inferredLevel] ? explicitLevel : inferredLevel);
|
|
451
|
+
const evidence = unique([...(level === inferredLevel ? inferred.evidence : requiredEvidenceForLevel(level)), ...(explicit.evidence ?? [])]);
|
|
385
452
|
const criteria = normalizeCriteria(
|
|
386
453
|
(explicit.criteria?.length ? explicit.criteria : inferred.criteria) as Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }>,
|
|
387
454
|
evidence,
|
|
@@ -391,8 +458,8 @@ export function resolveEffectiveAcceptance(input: {
|
|
|
391
458
|
level,
|
|
392
459
|
explicit: input.explicit !== undefined,
|
|
393
460
|
inferredReason: inferred.reasons,
|
|
394
|
-
criteria,
|
|
395
|
-
evidence,
|
|
461
|
+
criteria: level === "none" ? [] : criteria,
|
|
462
|
+
evidence: level === "none" ? [] : evidence,
|
|
396
463
|
verify: explicit.verify ?? [],
|
|
397
464
|
review,
|
|
398
465
|
stopRules: explicit.stopRules ?? [],
|
|
@@ -851,7 +918,7 @@ function validateStringArrayField(errors: string[], value: unknown, pathLabel: s
|
|
|
851
918
|
}
|
|
852
919
|
}
|
|
853
920
|
|
|
854
|
-
function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: AcceptanceReport; errors: string[] } {
|
|
921
|
+
export function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: AcceptanceReport; errors: string[] } {
|
|
855
922
|
const normalized = normalizeAcceptanceReportValue(value, pathLabel);
|
|
856
923
|
value = normalized.value;
|
|
857
924
|
pathLabel = normalized.pathLabel;
|
|
@@ -1310,7 +1377,7 @@ export async function evaluateAcceptance(input: {
|
|
|
1310
1377
|
|
|
1311
1378
|
const parsed: AcceptanceReportParseResult = input.reportError
|
|
1312
1379
|
? { error: input.reportError }
|
|
1313
|
-
: input.report
|
|
1380
|
+
: input.report !== undefined
|
|
1314
1381
|
? (() => {
|
|
1315
1382
|
const validation = validateAcceptanceReport(input.report);
|
|
1316
1383
|
return validation.report
|
|
@@ -50,7 +50,7 @@ const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outpu
|
|
|
50
50
|
const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
51
51
|
...DYNAMIC_PARALLEL_KEYS,
|
|
52
52
|
"outputName", "structured", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills", "outputPath", "namespaceOutputPath", "maxSubagentDepth", "timeoutMs", "waitToolEnabled", "waitToolDefaultTimeoutMs",
|
|
53
|
-
"structuredOutput", "structuredOutputSchema", "tools", "allowNestedSubagents", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "capabilityCeiling", "completionGuard", "systemPrompt",
|
|
53
|
+
"structuredOutput", "structuredOutputSchema", "tools", "excludeTools", "allowNestedSubagents", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "capabilityCeiling", "completionGuard", "systemPrompt",
|
|
54
54
|
"systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance", "acceptanceInput", "acceptanceRole", "parentSessionId", "launchResolvedExtensions", "requestedCwd",
|
|
55
55
|
]);
|
|
56
56
|
const DYNAMIC_COLLECT_KEYS = new Set(["as", "outputSchema"]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AsyncStatus, WorkflowLaneMetadata, WorkflowLaneMode } from "../../shared/types.ts";
|
|
1
|
+
import type { AsyncStatus, ManagedWorktreeProvider, WorktreeNaming, WorkflowLaneMetadata, WorkflowLaneMode } from "../../shared/types.ts";
|
|
2
2
|
|
|
3
3
|
export const WORKFLOW_LANE_KEY_MAX_BYTES = 128;
|
|
4
4
|
export const WORKFLOW_LANE_SOURCE_REF_MAX_BYTES = 128;
|
|
@@ -8,6 +8,7 @@ export const WORKFLOW_LANE_OUTPUT_PATH_MAX_BYTES = 256;
|
|
|
8
8
|
export const WORKFLOW_LANE_OUTPUT_PATHS_MAX = 10;
|
|
9
9
|
export const WORKTREE_STATUS_PATH_MAX_BYTES = 4096;
|
|
10
10
|
export const WORKTREE_STATUS_BRANCH_MAX_BYTES = 256;
|
|
11
|
+
export const WORKTREE_STATUS_NAMING_LABEL_MAX_BYTES = 256;
|
|
11
12
|
|
|
12
13
|
const WORKFLOW_LANE_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
13
14
|
const WORKFLOW_LANE_MODES = new Set<WorkflowLaneMode>(["mutation", "review", "scout", "gate"]);
|
|
@@ -75,16 +76,36 @@ export function assertWorkflowLaneKey(lane: WorkflowLaneMetadata | undefined, wo
|
|
|
75
76
|
export interface WorktreeStatusReference {
|
|
76
77
|
worktreePath: string;
|
|
77
78
|
branch: string;
|
|
79
|
+
provider?: ManagedWorktreeProvider;
|
|
80
|
+
naming?: WorktreeNaming;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeWorktreeNaming(value: unknown, label: string): WorktreeNaming {
|
|
84
|
+
assertPlainObject(value, label);
|
|
85
|
+
assertKnownFields(value, ["requestedBranch", "branchPrefix", "label", "sanitizedPathComponent", "collision", "collisionSuffix"], label);
|
|
86
|
+
const collision = value.collision;
|
|
87
|
+
if (collision !== undefined && collision !== "branch" && collision !== "path" && collision !== "both") throw new Error(`${label}.collision is invalid.`);
|
|
88
|
+
return {
|
|
89
|
+
requestedBranch: boundedNonEmptyString(value.requestedBranch, `${label}.requestedBranch`, WORKTREE_STATUS_BRANCH_MAX_BYTES),
|
|
90
|
+
branchPrefix: boundedNonEmptyString(value.branchPrefix, `${label}.branchPrefix`, WORKTREE_STATUS_BRANCH_MAX_BYTES),
|
|
91
|
+
label: boundedNonEmptyString(value.label, `${label}.label`, WORKTREE_STATUS_NAMING_LABEL_MAX_BYTES),
|
|
92
|
+
sanitizedPathComponent: boundedNonEmptyString(value.sanitizedPathComponent, `${label}.sanitizedPathComponent`, WORKTREE_STATUS_NAMING_LABEL_MAX_BYTES),
|
|
93
|
+
...(collision !== undefined ? { collision } : {}),
|
|
94
|
+
...(value.collisionSuffix !== undefined ? { collisionSuffix: boundedNonEmptyString(value.collisionSuffix, `${label}.collisionSuffix`, WORKTREE_STATUS_NAMING_LABEL_MAX_BYTES) } : {}),
|
|
95
|
+
};
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
/** Validate the display-only worktree fields copied into status.json. */
|
|
81
99
|
export function normalizeWorktreeStatusReference(value: unknown, label = "worktree status reference"): WorktreeStatusReference | undefined {
|
|
82
100
|
if (value === undefined) return undefined;
|
|
83
101
|
assertPlainObject(value, label);
|
|
84
|
-
assertKnownFields(value, ["worktreePath", "branch"], label);
|
|
102
|
+
assertKnownFields(value, ["worktreePath", "branch", "provider", "naming"], label);
|
|
103
|
+
if (value.provider !== undefined && value.provider !== "native" && value.provider !== "worktrunk") throw new Error(`${label}.provider is invalid.`);
|
|
85
104
|
return {
|
|
86
105
|
worktreePath: boundedNonEmptyString(value.worktreePath, `${label}.worktreePath`, WORKTREE_STATUS_PATH_MAX_BYTES),
|
|
87
106
|
branch: boundedNonEmptyString(value.branch, `${label}.branch`, WORKTREE_STATUS_BRANCH_MAX_BYTES),
|
|
107
|
+
...(value.provider !== undefined ? { provider: value.provider as ManagedWorktreeProvider } : {}),
|
|
108
|
+
...(value.naming !== undefined ? { naming: normalizeWorktreeNaming(value.naming, `${label}.naming`) } : {}),
|
|
88
109
|
};
|
|
89
110
|
}
|
|
90
111
|
|
|
@@ -100,6 +121,6 @@ export function validateAsyncStatusLaneMetadata(status: Pick<AsyncStatus, "runId
|
|
|
100
121
|
const hasPath = step.worktreePath !== undefined;
|
|
101
122
|
const hasBranch = step.branch !== undefined;
|
|
102
123
|
if (hasPath !== hasBranch) throw new Error(`${label}.steps[${index}] must include both worktreePath and branch.`);
|
|
103
|
-
if (hasPath) normalizeWorktreeStatusReference({ worktreePath: step.worktreePath, branch: step.branch }, `${label}.steps[${index}]`);
|
|
124
|
+
if (hasPath) normalizeWorktreeStatusReference({ worktreePath: step.worktreePath, branch: step.branch, ...(step.provider ? { provider: step.provider } : {}), ...(step.naming ? { naming: step.naming } : {}) }, `${label}.steps[${index}]`);
|
|
104
125
|
}
|
|
105
126
|
}
|
|
@@ -574,6 +574,8 @@ export function writeParallelHandoffGroup(input: {
|
|
|
574
574
|
index: worktree.index,
|
|
575
575
|
path: worktree.path,
|
|
576
576
|
branch: worktree.branch,
|
|
577
|
+
...(worktree.provider ? { provider: worktree.provider } : {}),
|
|
578
|
+
...(worktree.naming ? { naming: worktree.naming } : {}),
|
|
577
579
|
worktreeRemoved: false,
|
|
578
580
|
branchRemoved: false,
|
|
579
581
|
preserved: true,
|
|
@@ -653,6 +655,8 @@ export function discardPreservedWorktrees(
|
|
|
653
655
|
path: task.path,
|
|
654
656
|
agentCwd: task.path,
|
|
655
657
|
branch: task.branch,
|
|
658
|
+
...(task.provider ? { provider: task.provider } : {}),
|
|
659
|
+
...(task.naming ? { naming: task.naming } : {}),
|
|
656
660
|
index: task.index,
|
|
657
661
|
nodeModulesLinked: false,
|
|
658
662
|
syntheticPaths: [],
|
|
@@ -42,6 +42,7 @@ export interface RunnerSubagentStep {
|
|
|
42
42
|
skipPrimaryModelVerification?: boolean;
|
|
43
43
|
modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
|
|
44
44
|
tools?: string[];
|
|
45
|
+
excludeTools?: string[];
|
|
45
46
|
allowNestedSubagents?: boolean;
|
|
46
47
|
extensions?: string[];
|
|
47
48
|
subagentOnlyExtensions?: string[];
|
|
@@ -68,12 +69,7 @@ export interface RunnerSubagentStep {
|
|
|
68
69
|
toolTimeoutMs?: number;
|
|
69
70
|
waitToolEnabled?: boolean;
|
|
70
71
|
waitToolDefaultTimeoutMs?: number;
|
|
71
|
-
structuredOutput?:
|
|
72
|
-
schema: import("../../shared/types.ts").JsonSchemaObject;
|
|
73
|
-
schemaPath: string;
|
|
74
|
-
outputPath: string;
|
|
75
|
-
acceptanceReportPath?: string;
|
|
76
|
-
};
|
|
72
|
+
structuredOutput?: import("./structured-output.ts").StructuredOutputRuntime;
|
|
77
73
|
structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject;
|
|
78
74
|
agentContract?: import("../../shared/types.ts").AgentContract;
|
|
79
75
|
definitionDigest?: string;
|
|
@@ -7,7 +7,7 @@ export interface PermissionConfig { rules?: PermissionRules }
|
|
|
7
7
|
|
|
8
8
|
export const PERMISSION_POLICY_ENV = "PI_SUBAGENT_PERMISSION_POLICY";
|
|
9
9
|
export const PERMISSION_AUDIT_PATH_ENV = "PI_SUBAGENT_PERMISSION_AUDIT_PATH";
|
|
10
|
-
const INTERNAL_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait", "
|
|
10
|
+
const INTERNAL_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait", "structured_output"]);
|
|
11
11
|
const DECISIONS = new Set<PermissionDecision>(["allow", "ask", "deny"]);
|
|
12
12
|
const MAX_POLICY_BYTES = 16 * 1024;
|
|
13
13
|
const MAX_PREVIEW_BYTES = 2048;
|