pi-subagents 0.28.0 → 0.30.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 +31 -0
- package/README.md +26 -62
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +29 -35
- package/src/agents/agent-management.ts +29 -22
- package/src/agents/agent-selection.ts +2 -0
- package/src/agents/agent-serializer.ts +5 -10
- package/src/agents/agents.ts +339 -47
- package/src/agents/chain-serializer.ts +4 -9
- package/src/agents/proactive-skills.ts +191 -0
- package/src/extension/doctor.ts +4 -3
- package/src/extension/fanout-child.ts +1 -3
- package/src/extension/index.ts +6 -9
- package/src/extension/schemas.ts +63 -26
- package/src/intercom/intercom-bridge.ts +11 -1
- package/src/intercom/result-intercom.ts +0 -5
- package/src/runs/background/async-execution.ts +186 -74
- package/src/runs/background/async-resume.ts +53 -5
- package/src/runs/background/async-status.ts +4 -1
- package/src/runs/background/chain-append.ts +282 -0
- package/src/runs/background/chain-root-attachment.ts +161 -0
- package/src/runs/background/run-status.ts +2 -7
- package/src/runs/background/subagent-runner.ts +160 -219
- package/src/runs/foreground/chain-execution.ts +62 -58
- package/src/runs/foreground/execution.ts +39 -343
- package/src/runs/foreground/subagent-executor.ts +316 -111
- package/src/runs/shared/acceptance.ts +605 -22
- package/src/runs/shared/chain-outputs.ts +23 -8
- package/src/runs/shared/completion-guard.ts +3 -26
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +38 -0
- package/src/runs/shared/parallel-utils.ts +13 -10
- package/src/runs/shared/pi-args.ts +3 -2
- package/src/runs/shared/subagent-control.ts +8 -11
- package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
- package/src/runs/shared/workflow-graph.ts +2 -6
- package/src/shared/atomic-json.ts +68 -11
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +20 -49
- package/src/shared/utils.ts +2 -8
- package/src/slash/slash-bridge.ts +3 -1
- package/src/slash/slash-commands.ts +1 -1
- package/src/tui/render.ts +14 -29
- package/src/runs/shared/acceptance-contract.ts +0 -318
- package/src/runs/shared/acceptance-evaluation.ts +0 -221
- package/src/runs/shared/acceptance-finalization.ts +0 -173
- package/src/runs/shared/acceptance-reports.ts +0 -127
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
interface SlashSubagentRequest {
|
|
14
14
|
requestId: string;
|
|
15
15
|
params: SubagentParamsLike;
|
|
16
|
+
/** Optional requester context for in-process extension bridge calls. */
|
|
17
|
+
ctx?: ExtensionContext;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface SlashSubagentResponse {
|
|
@@ -77,7 +79,7 @@ export function registerSlashSubagentBridge(options: SlashBridgeOptions): {
|
|
|
77
79
|
if (typeof request.requestId !== "string" || !request.params) return;
|
|
78
80
|
const { requestId, params } = request as SlashSubagentRequest;
|
|
79
81
|
|
|
80
|
-
const ctx = options.getContext();
|
|
82
|
+
const ctx = request.ctx ?? options.getContext();
|
|
81
83
|
if (!ctx) {
|
|
82
84
|
const response: SlashSubagentResponse = {
|
|
83
85
|
requestId,
|
|
@@ -245,7 +245,7 @@ async function requestSlashRun(
|
|
|
245
245
|
next();
|
|
246
246
|
};
|
|
247
247
|
|
|
248
|
-
pi.events.emit(SLASH_SUBAGENT_REQUEST_EVENT, { requestId, params });
|
|
248
|
+
pi.events.emit(SLASH_SUBAGENT_REQUEST_EVENT, { requestId, params, ctx });
|
|
249
249
|
|
|
250
250
|
// Bridge emits STARTED synchronously during REQUEST emit.
|
|
251
251
|
// If not started, no bridge received the request.
|
package/src/tui/render.ts
CHANGED
|
@@ -222,22 +222,11 @@ function firstOutputLine(text: string): string {
|
|
|
222
222
|
return text.split("\n").find((line) => line.trim())?.trim() ?? "";
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
function formatAcceptanceStatus(result: Details["results"][number]): string | undefined {
|
|
226
|
-
const acceptance = result.acceptance;
|
|
227
|
-
if (!acceptance?.status || acceptance.status === "not-required") return undefined;
|
|
228
|
-
const finalization = acceptance.finalization
|
|
229
|
-
? ` · finalization: ${acceptance.finalization.status} after ${acceptance.finalization.turns.length}/${acceptance.finalization.maxTurns} turns`
|
|
230
|
-
: "";
|
|
231
|
-
return `acceptance: ${acceptance.status}${finalization}`;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
225
|
function resultStatusLine(result: Details["results"][number], output: string): string {
|
|
235
226
|
if (result.detached) return result.detachedReason ? `Detached: ${result.detachedReason}` : "Detached";
|
|
236
|
-
if (result.timedOut) return `Timed out${result.error ? `: ${result.error}` : ""}`;
|
|
237
227
|
if (result.interrupted) return "Paused";
|
|
238
228
|
if (result.exitCode !== 0) return `Error: ${result.error ?? (firstOutputLine(output) || `exit ${result.exitCode}`)}`;
|
|
239
|
-
|
|
240
|
-
if (acceptance) return `Done · ${acceptance}`;
|
|
229
|
+
if (result.acceptance?.status && result.acceptance.status !== "not-required") return `Done · acceptance: ${result.acceptance.status}`;
|
|
241
230
|
if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return "Done (no text output)";
|
|
242
231
|
return "Done";
|
|
243
232
|
}
|
|
@@ -245,7 +234,6 @@ function resultStatusLine(result: Details["results"][number], output: string): s
|
|
|
245
234
|
function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", seed = progressRunningSeed(result.progress ?? result.progressSummary)): string {
|
|
246
235
|
if (running) return theme.fg("accent", runningGlyph(seed));
|
|
247
236
|
if (result.detached) return theme.fg("warning", "■");
|
|
248
|
-
if (result.timedOut) return theme.fg("error", "✗");
|
|
249
237
|
if (result.interrupted) return theme.fg("warning", "■");
|
|
250
238
|
if (result.exitCode !== 0) return theme.fg("error", "✗");
|
|
251
239
|
if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return theme.fg("warning", "✓");
|
|
@@ -365,19 +353,18 @@ function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
|
365
353
|
return theme.fg("error", "✗");
|
|
366
354
|
}
|
|
367
355
|
|
|
368
|
-
function widgetStepGlyph(status: AsyncJobStep["status"]
|
|
356
|
+
function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number): string {
|
|
369
357
|
if (status === "running") return theme.fg("accent", runningGlyph(seed));
|
|
370
358
|
if (status === "complete" || status === "completed") return theme.fg("success", "✓");
|
|
371
|
-
if (status === "failed"
|
|
359
|
+
if (status === "failed") return theme.fg("error", "✗");
|
|
372
360
|
if (status === "paused") return theme.fg("warning", "■");
|
|
373
361
|
return theme.fg("muted", "◦");
|
|
374
362
|
}
|
|
375
363
|
|
|
376
|
-
function widgetStepStatus(status: AsyncJobStep["status"]
|
|
364
|
+
function widgetStepStatus(status: AsyncJobStep["status"], theme: Theme): string {
|
|
377
365
|
if (status === "running") return theme.fg("accent", "running");
|
|
378
366
|
if (status === "complete" || status === "completed") return theme.fg("success", "complete");
|
|
379
367
|
if (status === "failed") return theme.fg("error", "failed");
|
|
380
|
-
if (status === "timed-out") return theme.fg("error", "timed out");
|
|
381
368
|
if (status === "paused") return theme.fg("warning", "paused");
|
|
382
369
|
return theme.fg("dim", status);
|
|
383
370
|
}
|
|
@@ -514,7 +501,7 @@ function isDoneResult(result: Details["results"][number]): boolean {
|
|
|
514
501
|
const status = result.progress?.status;
|
|
515
502
|
if (status === "completed") return true;
|
|
516
503
|
if (status === "running" || status === "pending") return false;
|
|
517
|
-
if (result.interrupted || result.detached
|
|
504
|
+
if (result.interrupted || result.detached) return false;
|
|
518
505
|
return result.exitCode === 0;
|
|
519
506
|
}
|
|
520
507
|
|
|
@@ -586,7 +573,7 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
586
573
|
|
|
587
574
|
if (details.mode === "parallel") {
|
|
588
575
|
const totalCount = details.totalSteps ?? details.results.length;
|
|
589
|
-
const statuses = new Array(totalCount).fill("pending") as
|
|
576
|
+
const statuses = new Array(totalCount).fill("pending") as Array<"pending" | "running" | "completed" | "failed" | "detached">;
|
|
590
577
|
for (const progress of details.progress ?? []) {
|
|
591
578
|
if (progress.index >= 0 && progress.index < totalCount) statuses[progress.index] = progress.status;
|
|
592
579
|
}
|
|
@@ -597,13 +584,11 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
597
584
|
const index = result.progress?.index ?? progressFromArray?.index ?? i;
|
|
598
585
|
if (index < 0 || index >= totalCount) continue;
|
|
599
586
|
const status = result.progress?.status
|
|
600
|
-
?? (result.
|
|
601
|
-
? "
|
|
602
|
-
: result.
|
|
603
|
-
? "
|
|
604
|
-
:
|
|
605
|
-
? "completed"
|
|
606
|
-
: "failed");
|
|
587
|
+
?? (result.interrupted || result.detached
|
|
588
|
+
? "detached"
|
|
589
|
+
: result.exitCode === 0
|
|
590
|
+
? "completed"
|
|
591
|
+
: "failed");
|
|
607
592
|
statuses[index] = status;
|
|
608
593
|
}
|
|
609
594
|
const running = statuses.filter((status) => status === "running").length;
|
|
@@ -1065,7 +1050,7 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
1065
1050
|
|| d.results.some((r) => r.progress?.status === "running")
|
|
1066
1051
|
|| workflowGraphHasStatus(d, ["running"]);
|
|
1067
1052
|
const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running")
|
|
1068
|
-
|| workflowGraphHasStatus(d, ["failed"
|
|
1053
|
+
|| workflowGraphHasStatus(d, ["failed"]);
|
|
1069
1054
|
const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running")
|
|
1070
1055
|
|| workflowGraphHasStatus(d, ["paused", "detached"]);
|
|
1071
1056
|
let totalSummary = d.progressSummary;
|
|
@@ -1141,7 +1126,7 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
1141
1126
|
const activity = compactCurrentActivity(rProg);
|
|
1142
1127
|
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
1143
1128
|
c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
|
|
1144
|
-
} else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached ||
|
|
1129
|
+
} else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
|
|
1145
1130
|
c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
|
|
1146
1131
|
}
|
|
1147
1132
|
const outputTarget = extractOutputTarget(r.task);
|
|
@@ -1278,7 +1263,7 @@ export function renderSubagentResult(
|
|
|
1278
1263
|
&& r.progress?.status !== "running"
|
|
1279
1264
|
&& hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
|
|
1280
1265
|
);
|
|
1281
|
-
const hasWorkflowFailure = workflowGraphHasStatus(d, ["failed"
|
|
1266
|
+
const hasWorkflowFailure = workflowGraphHasStatus(d, ["failed"]);
|
|
1282
1267
|
const hasWorkflowPause = workflowGraphHasStatus(d, ["paused", "detached"]);
|
|
1283
1268
|
const icon = hasRunning
|
|
1284
1269
|
? theme.fg("warning", "running")
|
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AcceptanceConfig,
|
|
3
|
-
AcceptanceEvidenceKind,
|
|
4
|
-
AcceptanceInput,
|
|
5
|
-
AcceptanceProvenanceLevel,
|
|
6
|
-
ResolvedAcceptanceConfig,
|
|
7
|
-
ResolvedAcceptanceGate,
|
|
8
|
-
SubagentRunMode,
|
|
9
|
-
} from "../../shared/types.ts";
|
|
10
|
-
|
|
11
|
-
const DEFAULT_FINALIZATION_MAX_TURNS = 3;
|
|
12
|
-
const MAX_FINALIZATION_TURNS = 10;
|
|
13
|
-
|
|
14
|
-
const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>([
|
|
15
|
-
"changed-files",
|
|
16
|
-
"tests-added",
|
|
17
|
-
"commands-run",
|
|
18
|
-
"validation-output",
|
|
19
|
-
"residual-risks",
|
|
20
|
-
"no-staged-files",
|
|
21
|
-
"diff-summary",
|
|
22
|
-
"review-findings",
|
|
23
|
-
"manual-notes",
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
const ACCEPTANCE_KEYS = new Set([
|
|
27
|
-
"criteria",
|
|
28
|
-
"evidence",
|
|
29
|
-
"verify",
|
|
30
|
-
"review",
|
|
31
|
-
"stopRules",
|
|
32
|
-
"maxFinalizationTurns",
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
|
-
const REMOVED_ACCEPTANCE_KEYS = new Set(["level", "finalization", "reason"]);
|
|
36
|
-
|
|
37
|
-
const EVIDENCE_REPORT_FIELDS: Record<AcceptanceEvidenceKind, string> = {
|
|
38
|
-
"changed-files": "changedFiles: array of changed file paths",
|
|
39
|
-
"tests-added": "testsAddedOrUpdated: array of test files, suites, or cases added/updated",
|
|
40
|
-
"commands-run": "commandsRun: array of commands with result passed/failed/not-run and a short summary",
|
|
41
|
-
"validation-output": "validationOutput: array of relevant validation output summaries",
|
|
42
|
-
"residual-risks": "residualRisks: array of remaining risks or blockers; use [] when none remain",
|
|
43
|
-
"no-staged-files": "noStagedFiles: boolean",
|
|
44
|
-
"diff-summary": "diffSummary: non-empty string summarizing changed behavior and important files",
|
|
45
|
-
"review-findings": "reviewFindings: array of reviewer findings; use [] when no findings remain",
|
|
46
|
-
"manual-notes": "manualNotes: string for manual notes or external evidence",
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export function formatEvidenceReportFieldMapping(evidence: AcceptanceEvidenceKind[]): string[] {
|
|
50
|
-
return evidence.map((kind) => `- ${kind} -> ${EVIDENCE_REPORT_FIELDS[kind]}`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function hasArrayItems(value: unknown): boolean {
|
|
54
|
-
return Array.isArray(value) && value.length > 0;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"): string[] {
|
|
58
|
-
const errors: string[] = [];
|
|
59
|
-
if (input === undefined) return errors;
|
|
60
|
-
if (input === false || typeof input === "string") {
|
|
61
|
-
errors.push(`${pathLabel} must be an object. Public acceptance levels and false disables are no longer supported.`);
|
|
62
|
-
return errors;
|
|
63
|
-
}
|
|
64
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
65
|
-
errors.push(`${pathLabel} must be an object.`);
|
|
66
|
-
return errors;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const value = input as Record<string, unknown>;
|
|
70
|
-
if (Object.hasOwn(value, "level")) {
|
|
71
|
-
errors.push(`${pathLabel}.level is no longer supported; configure criteria, evidence, verify, and review directly.`);
|
|
72
|
-
}
|
|
73
|
-
if (Object.hasOwn(value, "finalization")) {
|
|
74
|
-
errors.push(`${pathLabel}.finalization is not supported; acceptance contracts always run the self-review loop.`);
|
|
75
|
-
}
|
|
76
|
-
if (Object.hasOwn(value, "reason")) {
|
|
77
|
-
errors.push(`${pathLabel}.reason is not supported because acceptance is disabled by omitting the field.`);
|
|
78
|
-
}
|
|
79
|
-
for (const key of Object.keys(value)) {
|
|
80
|
-
if (!ACCEPTANCE_KEYS.has(key) && !REMOVED_ACCEPTANCE_KEYS.has(key)) errors.push(`${pathLabel}.${key} is not supported.`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (value.criteria !== undefined) {
|
|
84
|
-
if (!Array.isArray(value.criteria)) {
|
|
85
|
-
errors.push(`${pathLabel}.criteria must be an array.`);
|
|
86
|
-
} else {
|
|
87
|
-
for (const [index, criterion] of value.criteria.entries()) {
|
|
88
|
-
if (typeof criterion === "string") {
|
|
89
|
-
if (!criterion.trim()) errors.push(`${pathLabel}.criteria[${index}] must not be empty.`);
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (!criterion || typeof criterion !== "object" || Array.isArray(criterion)) {
|
|
93
|
-
errors.push(`${pathLabel}.criteria[${index}] must be a string or object.`);
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
const item = criterion as Record<string, unknown>;
|
|
97
|
-
if (typeof item.id !== "string" || !item.id.trim()) errors.push(`${pathLabel}.criteria[${index}].id is required.`);
|
|
98
|
-
if (typeof item.must !== "string" || !item.must.trim()) errors.push(`${pathLabel}.criteria[${index}].must is required.`);
|
|
99
|
-
if (item.evidence !== undefined && !Array.isArray(item.evidence)) errors.push(`${pathLabel}.criteria[${index}].evidence must be an array.`);
|
|
100
|
-
if (Array.isArray(item.evidence)) {
|
|
101
|
-
for (const [evidenceIndex, evidence] of item.evidence.entries()) {
|
|
102
|
-
if (typeof evidence !== "string" || !VALID_EVIDENCE.has(evidence as AcceptanceEvidenceKind)) {
|
|
103
|
-
errors.push(`${pathLabel}.criteria[${index}].evidence[${evidenceIndex}] is not a supported evidence kind.`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (item.severity !== undefined && item.severity !== "required" && item.severity !== "recommended") {
|
|
108
|
-
errors.push(`${pathLabel}.criteria[${index}].severity must be required or recommended.`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (Array.isArray(value.evidence)) {
|
|
115
|
-
for (const [index, item] of value.evidence.entries()) {
|
|
116
|
-
if (typeof item !== "string" || !VALID_EVIDENCE.has(item as AcceptanceEvidenceKind)) {
|
|
117
|
-
errors.push(`${pathLabel}.evidence[${index}] is not a supported evidence kind.`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
} else if (value.evidence !== undefined) {
|
|
121
|
-
errors.push(`${pathLabel}.evidence must be an array.`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (value.verify !== undefined && !Array.isArray(value.verify)) errors.push(`${pathLabel}.verify must be an array.`);
|
|
125
|
-
if (Array.isArray(value.verify)) {
|
|
126
|
-
for (const [index, command] of value.verify.entries()) {
|
|
127
|
-
if (!command || typeof command !== "object" || Array.isArray(command)) {
|
|
128
|
-
errors.push(`${pathLabel}.verify[${index}] must be an object.`);
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const cmd = command as Record<string, unknown>;
|
|
132
|
-
if (typeof cmd.id !== "string" || !cmd.id.trim()) errors.push(`${pathLabel}.verify[${index}].id is required.`);
|
|
133
|
-
if (typeof cmd.command !== "string" || !cmd.command.trim()) errors.push(`${pathLabel}.verify[${index}].command is required.`);
|
|
134
|
-
if (cmd.timeoutMs !== undefined && (!Number.isInteger(cmd.timeoutMs) || Number(cmd.timeoutMs) <= 0)) {
|
|
135
|
-
errors.push(`${pathLabel}.verify[${index}].timeoutMs must be a positive integer.`);
|
|
136
|
-
}
|
|
137
|
-
if (cmd.cwd !== undefined && typeof cmd.cwd !== "string") errors.push(`${pathLabel}.verify[${index}].cwd must be a string.`);
|
|
138
|
-
if (cmd.env !== undefined) {
|
|
139
|
-
if (!cmd.env || typeof cmd.env !== "object" || Array.isArray(cmd.env)) {
|
|
140
|
-
errors.push(`${pathLabel}.verify[${index}].env must be an object with string values.`);
|
|
141
|
-
} else {
|
|
142
|
-
for (const [key, envValue] of Object.entries(cmd.env as Record<string, unknown>)) {
|
|
143
|
-
if (typeof envValue !== "string") errors.push(`${pathLabel}.verify[${index}].env.${key} must be a string.`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
if (cmd.allowFailure !== undefined && typeof cmd.allowFailure !== "boolean") errors.push(`${pathLabel}.verify[${index}].allowFailure must be a boolean.`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (value.review !== undefined) {
|
|
152
|
-
if (!value.review || typeof value.review !== "object" || Array.isArray(value.review)) {
|
|
153
|
-
errors.push(`${pathLabel}.review must be an object.`);
|
|
154
|
-
} else {
|
|
155
|
-
const review = value.review as Record<string, unknown>;
|
|
156
|
-
if (review.agent !== undefined && typeof review.agent !== "string") errors.push(`${pathLabel}.review.agent must be a string.`);
|
|
157
|
-
if (review.focus !== undefined && typeof review.focus !== "string") errors.push(`${pathLabel}.review.focus must be a string.`);
|
|
158
|
-
if (review.required !== undefined && typeof review.required !== "boolean") errors.push(`${pathLabel}.review.required must be a boolean.`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (value.stopRules !== undefined) {
|
|
163
|
-
if (!Array.isArray(value.stopRules)) {
|
|
164
|
-
errors.push(`${pathLabel}.stopRules must be an array.`);
|
|
165
|
-
} else {
|
|
166
|
-
for (const [index, rule] of value.stopRules.entries()) {
|
|
167
|
-
if (typeof rule !== "string" || !rule.trim()) errors.push(`${pathLabel}.stopRules[${index}] must be a non-empty string.`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (value.maxFinalizationTurns !== undefined) {
|
|
173
|
-
if (!Number.isInteger(value.maxFinalizationTurns) || Number(value.maxFinalizationTurns) < 1 || Number(value.maxFinalizationTurns) > MAX_FINALIZATION_TURNS) {
|
|
174
|
-
errors.push(`${pathLabel}.maxFinalizationTurns must be an integer from 1 to ${MAX_FINALIZATION_TURNS}.`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const hasContract = hasArrayItems(value.criteria)
|
|
179
|
-
|| hasArrayItems(value.evidence)
|
|
180
|
-
|| hasArrayItems(value.verify)
|
|
181
|
-
|| value.review !== undefined
|
|
182
|
-
|| hasArrayItems(value.stopRules);
|
|
183
|
-
if (!hasContract) {
|
|
184
|
-
errors.push(`${pathLabel} must include at least one of criteria, evidence, verify, review, or stopRules.`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return errors;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function normalizeCriteria(criteria: AcceptanceConfig["criteria"], evidence: AcceptanceEvidenceKind[]): ResolvedAcceptanceGate[] {
|
|
191
|
-
return (criteria ?? []).map((criterion, index) => {
|
|
192
|
-
if (typeof criterion === "string") {
|
|
193
|
-
return { id: `criterion-${index + 1}`, must: criterion, evidence, severity: "required" as const };
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
id: criterion.id.trim(),
|
|
197
|
-
must: criterion.must,
|
|
198
|
-
evidence: criterion.evidence?.filter((item) => VALID_EVIDENCE.has(item)) ?? evidence,
|
|
199
|
-
severity: criterion.severity ?? "required",
|
|
200
|
-
};
|
|
201
|
-
}).filter((criterion) => criterion.must.trim());
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function deriveAcceptanceLevel(config: AcceptanceConfig): AcceptanceProvenanceLevel {
|
|
205
|
-
if (config.review) return "reviewed";
|
|
206
|
-
if ((config.verify?.length ?? 0) > 0) return "verified";
|
|
207
|
-
return "checked";
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function resolveEffectiveAcceptance(input: {
|
|
211
|
-
explicit?: AcceptanceInput;
|
|
212
|
-
agentName: string;
|
|
213
|
-
task?: string;
|
|
214
|
-
mode?: SubagentRunMode;
|
|
215
|
-
async?: boolean;
|
|
216
|
-
dynamic?: boolean;
|
|
217
|
-
dynamicGroup?: boolean;
|
|
218
|
-
}): ResolvedAcceptanceConfig {
|
|
219
|
-
if (input.explicit === undefined) {
|
|
220
|
-
return {
|
|
221
|
-
level: "none",
|
|
222
|
-
explicit: false,
|
|
223
|
-
inferredReason: ["acceptance not configured"],
|
|
224
|
-
criteria: [],
|
|
225
|
-
evidence: [],
|
|
226
|
-
verify: [],
|
|
227
|
-
stopRules: [],
|
|
228
|
-
finalization: { mode: "none", maxTurns: 0 },
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const validationErrors = validateAcceptanceInput(input.explicit);
|
|
233
|
-
if (validationErrors.length > 0) throw new Error(validationErrors.join(" "));
|
|
234
|
-
const explicit = input.explicit;
|
|
235
|
-
const evidence = [...new Set(explicit.evidence ?? [])];
|
|
236
|
-
const criteria = normalizeCriteria(explicit.criteria, evidence);
|
|
237
|
-
const verify = explicit.verify ?? [];
|
|
238
|
-
const stopRules = explicit.stopRules ?? [];
|
|
239
|
-
return {
|
|
240
|
-
level: deriveAcceptanceLevel(explicit),
|
|
241
|
-
explicit: true,
|
|
242
|
-
inferredReason: ["explicit acceptance contract"],
|
|
243
|
-
criteria,
|
|
244
|
-
evidence,
|
|
245
|
-
verify,
|
|
246
|
-
...(explicit.review ? { review: explicit.review } : {}),
|
|
247
|
-
stopRules,
|
|
248
|
-
finalization: { mode: "self-review-loop", maxTurns: explicit.maxFinalizationTurns ?? DEFAULT_FINALIZATION_MAX_TURNS },
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export function shouldRunAcceptanceFinalization(acceptance: ResolvedAcceptanceConfig): boolean {
|
|
253
|
-
return acceptance.explicit && acceptance.finalization.mode === "self-review-loop" && acceptance.finalization.maxTurns > 0;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export function acceptanceSelfReviewConfig(acceptance: ResolvedAcceptanceConfig): ResolvedAcceptanceConfig {
|
|
257
|
-
if (!acceptance.review && acceptance.verify.length === 0) return acceptance;
|
|
258
|
-
const { review: _review, verify: _verify, ...selfReview } = acceptance;
|
|
259
|
-
return {
|
|
260
|
-
...selfReview,
|
|
261
|
-
level: "checked",
|
|
262
|
-
verify: [],
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig): string {
|
|
267
|
-
if (acceptance.level === "none") return "";
|
|
268
|
-
const lines = [
|
|
269
|
-
"",
|
|
270
|
-
"## Acceptance Contract",
|
|
271
|
-
"Completion is not accepted from prose alone. End the initial response with a structured acceptance report.",
|
|
272
|
-
"After the initial response, the runtime will continue this same session for a bounded self-review/repair loop before accepting the run.",
|
|
273
|
-
"",
|
|
274
|
-
"Criteria:",
|
|
275
|
-
...(acceptance.criteria.length ? acceptance.criteria.map((criterion) => `- ${criterion.id}: ${criterion.must}`) : ["- No explicit criteria were configured; satisfy the requested task and the required evidence/checks below."]),
|
|
276
|
-
"",
|
|
277
|
-
`Required evidence: ${acceptance.evidence.join(", ") || "none explicitly requested"}`,
|
|
278
|
-
];
|
|
279
|
-
if (acceptance.evidence.length > 0) {
|
|
280
|
-
lines.push(
|
|
281
|
-
"",
|
|
282
|
-
"Structured evidence must be present in the `acceptance-report` JSON fields. Markdown sections in your visible answer do not satisfy required evidence by themselves. If you already described evidence in prose, copy or summarize it into the matching JSON field.",
|
|
283
|
-
"Evidence field mapping:",
|
|
284
|
-
...formatEvidenceReportFieldMapping(acceptance.evidence),
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
if (acceptance.verify.length > 0) {
|
|
288
|
-
lines.push("", "Runtime verification commands configured by parent:");
|
|
289
|
-
for (const command of acceptance.verify) lines.push(`- ${command.id}: ${command.command}`);
|
|
290
|
-
}
|
|
291
|
-
if (acceptance.review) {
|
|
292
|
-
lines.push("", `Independent review gate: ${acceptance.review.required === false ? "optional" : "required"}${acceptance.review.agent ? ` by ${acceptance.review.agent}` : ""}.`);
|
|
293
|
-
if (acceptance.review.focus) lines.push(`Review focus: ${acceptance.review.focus}`);
|
|
294
|
-
}
|
|
295
|
-
if (acceptance.stopRules.length > 0) {
|
|
296
|
-
lines.push("", "Stop rules:", ...acceptance.stopRules.map((rule) => `- ${rule}`));
|
|
297
|
-
}
|
|
298
|
-
lines.push(
|
|
299
|
-
"",
|
|
300
|
-
"Finish with a fenced JSON block tagged `acceptance-report` in this shape:",
|
|
301
|
-
"```acceptance-report",
|
|
302
|
-
JSON.stringify({
|
|
303
|
-
criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "specific proof" }],
|
|
304
|
-
changedFiles: [],
|
|
305
|
-
testsAddedOrUpdated: [],
|
|
306
|
-
commandsRun: [{ command: "command", result: "passed", summary: "short result" }],
|
|
307
|
-
validationOutput: [],
|
|
308
|
-
residualRisks: [],
|
|
309
|
-
noStagedFiles: true,
|
|
310
|
-
diffSummary: "concise summary of changed behavior and important files",
|
|
311
|
-
reviewFindings: [],
|
|
312
|
-
manualNotes: "manual notes or external evidence, if any",
|
|
313
|
-
notes: "anything else the parent should know",
|
|
314
|
-
}, null, 2),
|
|
315
|
-
"```",
|
|
316
|
-
);
|
|
317
|
-
return lines.join("\n");
|
|
318
|
-
}
|