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
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import type {
|
|
4
|
-
AcceptanceEvidenceKind,
|
|
5
|
-
AcceptanceLedger,
|
|
6
|
-
AcceptanceProvenanceLevel,
|
|
7
|
-
AcceptanceReport,
|
|
8
|
-
AcceptanceRuntimeCheck,
|
|
9
|
-
AcceptanceReviewResult,
|
|
10
|
-
AcceptanceVerifyCommand,
|
|
11
|
-
AcceptanceVerifyResult,
|
|
12
|
-
ResolvedAcceptanceConfig,
|
|
13
|
-
ResolvedAcceptanceGate,
|
|
14
|
-
} from "../../shared/types.ts";
|
|
15
|
-
import { parseAcceptanceReport } from "./acceptance-reports.ts";
|
|
16
|
-
|
|
17
|
-
const LEVEL_RANK: Record<AcceptanceProvenanceLevel, number> = {
|
|
18
|
-
none: 0,
|
|
19
|
-
attested: 1,
|
|
20
|
-
checked: 2,
|
|
21
|
-
verified: 3,
|
|
22
|
-
reviewed: 4,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function isStringArray(value: unknown): value is string[] {
|
|
26
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function checkCriteriaSatisfied(criteria: ResolvedAcceptanceGate[], report: AcceptanceReport): AcceptanceRuntimeCheck[] {
|
|
30
|
-
const reports = new Map((report.criteriaSatisfied ?? []).filter((item) => item.id).map((item) => [item.id!, item]));
|
|
31
|
-
return criteria.filter((criterion) => criterion.severity !== "recommended").map((criterion) => {
|
|
32
|
-
const item = reports.get(criterion.id);
|
|
33
|
-
if (!item) return { id: `criterion:${criterion.id}`, status: "failed", message: `Required criterion '${criterion.id}' was not reported.` };
|
|
34
|
-
if (item.status !== "satisfied") return { id: `criterion:${criterion.id}`, status: "failed", message: `Required criterion '${criterion.id}' was reported as ${item.status}.` };
|
|
35
|
-
return { id: `criterion:${criterion.id}`, status: "passed", message: `Required criterion '${criterion.id}' satisfied.` };
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function reportEvidencePresent(report: AcceptanceReport, kind: AcceptanceEvidenceKind): boolean {
|
|
40
|
-
switch (kind) {
|
|
41
|
-
case "changed-files": return isStringArray(report.changedFiles) && report.changedFiles.length > 0;
|
|
42
|
-
case "tests-added": return isStringArray(report.testsAddedOrUpdated) && report.testsAddedOrUpdated.length > 0;
|
|
43
|
-
case "commands-run": return Array.isArray(report.commandsRun) && report.commandsRun.length > 0;
|
|
44
|
-
case "validation-output": return isStringArray(report.validationOutput) && report.validationOutput.length > 0;
|
|
45
|
-
case "residual-risks": return isStringArray(report.residualRisks);
|
|
46
|
-
case "no-staged-files": return report.noStagedFiles === true;
|
|
47
|
-
case "diff-summary": return typeof report.diffSummary === "string" && report.diffSummary.trim().length > 0;
|
|
48
|
-
case "review-findings": return isStringArray(report.reviewFindings);
|
|
49
|
-
case "manual-notes": return Boolean((report.manualNotes ?? report.notes)?.trim());
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function checkNoStagedFiles(cwd: string): AcceptanceRuntimeCheck {
|
|
54
|
-
const result = spawnSync("git", ["status", "--short"], { cwd, encoding: "utf-8" });
|
|
55
|
-
if (result.status !== 0) {
|
|
56
|
-
return { id: "no-staged-files", status: "not-applicable", message: "git status unavailable; no staged-files check skipped" };
|
|
57
|
-
}
|
|
58
|
-
const staged = result.stdout.split(/\r?\n/).filter((line) => line.length >= 2 && line[0] !== " " && line[0] !== "?");
|
|
59
|
-
return staged.length === 0
|
|
60
|
-
? { id: "no-staged-files", status: "passed", message: "No staged files detected." }
|
|
61
|
-
: { id: "no-staged-files", status: "failed", message: `Staged files present: ${staged.join(", ")}` };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function runStructuralChecks(acceptance: ResolvedAcceptanceConfig, report: AcceptanceReport, cwd: string): AcceptanceRuntimeCheck[] {
|
|
65
|
-
const checks: AcceptanceRuntimeCheck[] = [];
|
|
66
|
-
checks.push(...checkCriteriaSatisfied(acceptance.criteria, report));
|
|
67
|
-
for (const kind of acceptance.evidence) {
|
|
68
|
-
const present = reportEvidencePresent(report, kind);
|
|
69
|
-
checks.push({
|
|
70
|
-
id: `evidence:${kind}`,
|
|
71
|
-
status: present ? "passed" : "failed",
|
|
72
|
-
message: present ? `${kind} evidence present.` : `${kind} evidence missing from child report.`,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
if (acceptance.evidence.includes("no-staged-files")) checks.push(checkNoStagedFiles(cwd));
|
|
76
|
-
return checks;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function trimOutput(value: string): string | undefined {
|
|
80
|
-
const trimmed = value.trim();
|
|
81
|
-
if (!trimmed) return undefined;
|
|
82
|
-
return trimmed.length > 12_000 ? `${trimmed.slice(0, 12_000)}\n...[truncated]` : trimmed;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function runVerifyCommand(command: AcceptanceVerifyCommand, defaultCwd: string): Promise<AcceptanceVerifyResult> {
|
|
86
|
-
return new Promise((resolve) => {
|
|
87
|
-
const startedAt = Date.now();
|
|
88
|
-
const cwd = command.cwd ? path.resolve(defaultCwd, command.cwd) : defaultCwd;
|
|
89
|
-
let stdout = "";
|
|
90
|
-
let stderr = "";
|
|
91
|
-
let timedOut = false;
|
|
92
|
-
const child = spawn(command.command, {
|
|
93
|
-
cwd,
|
|
94
|
-
env: { ...process.env, ...(command.env ?? {}) },
|
|
95
|
-
shell: true,
|
|
96
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
97
|
-
windowsHide: true,
|
|
98
|
-
});
|
|
99
|
-
const timeout = setTimeout(() => {
|
|
100
|
-
timedOut = true;
|
|
101
|
-
child.kill("SIGTERM");
|
|
102
|
-
setTimeout(() => child.kill("SIGKILL"), 1000).unref?.();
|
|
103
|
-
}, command.timeoutMs ?? 120_000);
|
|
104
|
-
timeout.unref?.();
|
|
105
|
-
child.stdout.on("data", (chunk: Buffer) => {
|
|
106
|
-
stdout += chunk.toString();
|
|
107
|
-
});
|
|
108
|
-
child.stderr.on("data", (chunk: Buffer) => {
|
|
109
|
-
stderr += chunk.toString();
|
|
110
|
-
});
|
|
111
|
-
child.on("close", (exitCode) => {
|
|
112
|
-
clearTimeout(timeout);
|
|
113
|
-
const durationMs = Date.now() - startedAt;
|
|
114
|
-
const passed = exitCode === 0 && !timedOut;
|
|
115
|
-
resolve({
|
|
116
|
-
id: command.id,
|
|
117
|
-
command: command.command,
|
|
118
|
-
cwd,
|
|
119
|
-
exitCode,
|
|
120
|
-
status: timedOut ? "timed-out" : passed ? "passed" : command.allowFailure ? "allowed-failure" : "failed",
|
|
121
|
-
stdout: trimOutput(stdout),
|
|
122
|
-
stderr: trimOutput(stderr),
|
|
123
|
-
durationMs,
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
child.on("error", (error) => {
|
|
127
|
-
clearTimeout(timeout);
|
|
128
|
-
resolve({
|
|
129
|
-
id: command.id,
|
|
130
|
-
command: command.command,
|
|
131
|
-
cwd,
|
|
132
|
-
exitCode: 1,
|
|
133
|
-
status: command.allowFailure ? "allowed-failure" : "failed",
|
|
134
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
135
|
-
durationMs: Date.now() - startedAt,
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export async function evaluateAcceptance(input: {
|
|
142
|
-
acceptance: ResolvedAcceptanceConfig;
|
|
143
|
-
output: string;
|
|
144
|
-
cwd: string;
|
|
145
|
-
report?: AcceptanceReport;
|
|
146
|
-
reviewResult?: AcceptanceReviewResult;
|
|
147
|
-
}): Promise<AcceptanceLedger> {
|
|
148
|
-
const acceptance = input.acceptance;
|
|
149
|
-
const ledger: AcceptanceLedger = {
|
|
150
|
-
status: acceptance.level === "none" ? "not-required" : "claimed",
|
|
151
|
-
explicit: acceptance.explicit,
|
|
152
|
-
effectiveAcceptance: acceptance,
|
|
153
|
-
inferredReason: acceptance.inferredReason,
|
|
154
|
-
criteria: acceptance.criteria,
|
|
155
|
-
runtimeChecks: [],
|
|
156
|
-
verifyRuns: [],
|
|
157
|
-
};
|
|
158
|
-
if (acceptance.level === "none") return ledger;
|
|
159
|
-
|
|
160
|
-
const parsed = input.report ? { report: input.report } : parseAcceptanceReport(input.output);
|
|
161
|
-
if (parsed.report) {
|
|
162
|
-
ledger.childReport = parsed.report;
|
|
163
|
-
ledger.status = "attested";
|
|
164
|
-
} else {
|
|
165
|
-
ledger.childReportParseError = parsed.error;
|
|
166
|
-
ledger.runtimeChecks.push({ id: "attestation", status: "failed", message: parsed.error ?? "Structured acceptance report missing." });
|
|
167
|
-
ledger.status = "rejected";
|
|
168
|
-
return ledger;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (LEVEL_RANK[acceptance.level] >= LEVEL_RANK.checked) {
|
|
172
|
-
ledger.runtimeChecks = runStructuralChecks(acceptance, parsed.report, input.cwd);
|
|
173
|
-
if (ledger.runtimeChecks.some((check) => check.status === "failed")) {
|
|
174
|
-
ledger.status = "rejected";
|
|
175
|
-
return ledger;
|
|
176
|
-
}
|
|
177
|
-
ledger.status = "checked";
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (acceptance.verify.length > 0) {
|
|
181
|
-
ledger.verifyRuns = [];
|
|
182
|
-
for (const command of acceptance.verify) ledger.verifyRuns.push(await runVerifyCommand(command, input.cwd));
|
|
183
|
-
if (ledger.verifyRuns.some((run) => run.status === "failed" || run.status === "timed-out")) {
|
|
184
|
-
ledger.status = "rejected";
|
|
185
|
-
return ledger;
|
|
186
|
-
}
|
|
187
|
-
ledger.status = "verified";
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (acceptance.review) {
|
|
191
|
-
if (input.reviewResult) {
|
|
192
|
-
ledger.reviewResult = input.reviewResult;
|
|
193
|
-
ledger.status = input.reviewResult.status === "no-blockers" ? "reviewed" : "rejected";
|
|
194
|
-
} else {
|
|
195
|
-
const optionalReview = acceptance.review.required === false;
|
|
196
|
-
ledger.reviewResult = {
|
|
197
|
-
status: "needs-parent-decision",
|
|
198
|
-
findings: [{
|
|
199
|
-
severity: optionalReview ? "non-blocking" : "blocker",
|
|
200
|
-
issue: "Reviewed acceptance requires an independent reviewer result.",
|
|
201
|
-
rationale: "The run cannot be marked reviewed from child self-review or evidence alone.",
|
|
202
|
-
}],
|
|
203
|
-
};
|
|
204
|
-
if (!optionalReview) ledger.status = "rejected";
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return ledger;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
export function acceptanceFailureMessage(ledger: AcceptanceLedger): string | undefined {
|
|
213
|
-
if (ledger.status !== "rejected") return undefined;
|
|
214
|
-
const failedCheck = ledger.runtimeChecks.find((check) => check.status === "failed");
|
|
215
|
-
if (failedCheck) return `Acceptance rejected: ${failedCheck.message}`;
|
|
216
|
-
const failedVerify = ledger.verifyRuns.find((run) => run.status === "failed" || run.status === "timed-out");
|
|
217
|
-
if (failedVerify) return `Acceptance verification '${failedVerify.id}' ${failedVerify.status}.`;
|
|
218
|
-
if (ledger.reviewResult?.status === "needs-parent-decision") return "Acceptance review required but no automatic reviewer result is available.";
|
|
219
|
-
if (ledger.reviewResult?.status === "blockers") return "Acceptance review found blockers.";
|
|
220
|
-
return "Acceptance rejected.";
|
|
221
|
-
}
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AcceptanceFinalizationTurn,
|
|
3
|
-
AcceptanceLedger,
|
|
4
|
-
ResolvedAcceptanceConfig,
|
|
5
|
-
} from "../../shared/types.ts";
|
|
6
|
-
import { acceptanceFailureMessage } from "./acceptance-evaluation.ts";
|
|
7
|
-
import { formatEvidenceReportFieldMapping } from "./acceptance-contract.ts";
|
|
8
|
-
import { stripAcceptanceReport } from "./acceptance-reports.ts";
|
|
9
|
-
|
|
10
|
-
const INITIAL_OUTPUT_LIMIT = 8_000;
|
|
11
|
-
|
|
12
|
-
function truncateForPrompt(value: string): string {
|
|
13
|
-
const trimmed = stripAcceptanceReport(value).trim();
|
|
14
|
-
if (trimmed.length <= INITIAL_OUTPUT_LIMIT) return trimmed || "(initial output was empty after removing acceptance-report)";
|
|
15
|
-
return `${trimmed.slice(0, INITIAL_OUTPUT_LIMIT)}\n...[truncated]`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function formatReportForPrompt(ledger: AcceptanceLedger): string {
|
|
19
|
-
if (ledger.childReport) return JSON.stringify(ledger.childReport, null, 2);
|
|
20
|
-
return `Missing or malformed acceptance report: ${ledger.childReportParseError ?? "no parse detail"}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function formatAcceptanceFinalizationPrompt(input: {
|
|
24
|
-
acceptance: ResolvedAcceptanceConfig;
|
|
25
|
-
initialOutput: string;
|
|
26
|
-
initialLedger: AcceptanceLedger;
|
|
27
|
-
turn: number;
|
|
28
|
-
maxTurns: number;
|
|
29
|
-
previousFailure?: string;
|
|
30
|
-
}): string {
|
|
31
|
-
const lines = [
|
|
32
|
-
"## Acceptance Finalization",
|
|
33
|
-
"You are continuing the same subagent session. Before this run can be accepted, compare the current work to the acceptance contract and the evidence below.",
|
|
34
|
-
`This is finalization turn ${input.turn} of ${input.maxTurns}. The run will be rejected if the contract is still not satisfied after turn ${input.maxTurns}.`,
|
|
35
|
-
"",
|
|
36
|
-
"If a criterion is incomplete and fixable in this session, keep working now before returning the final report.",
|
|
37
|
-
"If a criterion cannot be satisfied in this session, report it as not-satisfied, explain the blocker in residualRisks, and say what input would unblock progress.",
|
|
38
|
-
"Do not claim a criterion is satisfied unless the current work has concrete evidence from files, commands, validation output, or other inspectable artifacts.",
|
|
39
|
-
"",
|
|
40
|
-
"## Acceptance Contract",
|
|
41
|
-
"Criteria:",
|
|
42
|
-
...(input.acceptance.criteria.length ? input.acceptance.criteria.map((criterion) => `- ${criterion.id}: ${criterion.must}`) : ["- No explicit criteria were configured; satisfy the requested task and required evidence/checks."]),
|
|
43
|
-
"",
|
|
44
|
-
`Required evidence: ${input.acceptance.evidence.join(", ") || "none explicitly requested"}`,
|
|
45
|
-
];
|
|
46
|
-
if (input.acceptance.evidence.length > 0) {
|
|
47
|
-
lines.push(
|
|
48
|
-
"",
|
|
49
|
-
"Structured evidence must be present in the final `acceptance-report` JSON fields. Markdown sections in the visible answer do not satisfy required evidence by themselves. If the previous visible output already included the evidence, copy or summarize it into the matching JSON field.",
|
|
50
|
-
"Evidence field mapping:",
|
|
51
|
-
...formatEvidenceReportFieldMapping(input.acceptance.evidence),
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
if (input.acceptance.verify.length > 0) {
|
|
55
|
-
lines.push("", "Runtime verification commands that must pass:", ...input.acceptance.verify.map((command) => `- ${command.id}: ${command.command}`));
|
|
56
|
-
}
|
|
57
|
-
if (input.acceptance.review) {
|
|
58
|
-
lines.push("", `Independent review gate after self-review: ${input.acceptance.review.required === false ? "optional" : "required"}${input.acceptance.review.agent ? ` by ${input.acceptance.review.agent}` : ""}.`);
|
|
59
|
-
}
|
|
60
|
-
if (input.acceptance.stopRules.length > 0) {
|
|
61
|
-
lines.push("", "Stop rules are hard constraints while deciding whether to continue, stop as blocked, or report success:", ...input.acceptance.stopRules.map((rule) => `- ${rule}`));
|
|
62
|
-
}
|
|
63
|
-
lines.push(
|
|
64
|
-
"",
|
|
65
|
-
"Initial visible output:",
|
|
66
|
-
truncateForPrompt(input.initialOutput),
|
|
67
|
-
"",
|
|
68
|
-
"Initial acceptance report:",
|
|
69
|
-
formatReportForPrompt(input.initialLedger),
|
|
70
|
-
);
|
|
71
|
-
if (input.previousFailure) {
|
|
72
|
-
lines.push("", "Previous finalization failure to address:", input.previousFailure);
|
|
73
|
-
}
|
|
74
|
-
lines.push(
|
|
75
|
-
"",
|
|
76
|
-
"Now do the self-check. If work was missing and you repaired it, report the repaired final state. Finish with exactly one fenced JSON block tagged `acceptance-report`.",
|
|
77
|
-
"```acceptance-report",
|
|
78
|
-
JSON.stringify({
|
|
79
|
-
criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "specific proof from the final state" }],
|
|
80
|
-
changedFiles: [],
|
|
81
|
-
testsAddedOrUpdated: [],
|
|
82
|
-
commandsRun: [{ command: "command", result: "passed", summary: "short result" }],
|
|
83
|
-
validationOutput: [],
|
|
84
|
-
residualRisks: [],
|
|
85
|
-
noStagedFiles: true,
|
|
86
|
-
diffSummary: "concise summary of changed behavior and important files",
|
|
87
|
-
reviewFindings: [],
|
|
88
|
-
manualNotes: "manual notes or external evidence, if any",
|
|
89
|
-
notes: "final self-review summary",
|
|
90
|
-
}, null, 2),
|
|
91
|
-
"```",
|
|
92
|
-
);
|
|
93
|
-
return lines.join("\n");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function createFinalizationTurn(input: {
|
|
97
|
-
turn: number;
|
|
98
|
-
prompt: string;
|
|
99
|
-
rawOutput: string;
|
|
100
|
-
ledger: AcceptanceLedger;
|
|
101
|
-
}): AcceptanceFinalizationTurn {
|
|
102
|
-
const failureMessage = acceptanceFailureMessage(input.ledger);
|
|
103
|
-
return {
|
|
104
|
-
turn: input.turn,
|
|
105
|
-
prompt: input.prompt,
|
|
106
|
-
status: input.ledger.status,
|
|
107
|
-
rawOutput: input.rawOutput,
|
|
108
|
-
...(input.ledger.childReport ? { report: input.ledger.childReport } : {}),
|
|
109
|
-
...(input.ledger.childReportParseError ? { parseError: input.ledger.childReportParseError } : {}),
|
|
110
|
-
runtimeChecks: input.ledger.runtimeChecks,
|
|
111
|
-
verifyRuns: input.ledger.verifyRuns,
|
|
112
|
-
...(failureMessage ? { failureMessage } : {}),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function createFinalizationProcessFailureTurn(input: {
|
|
117
|
-
turn: number;
|
|
118
|
-
prompt: string;
|
|
119
|
-
rawOutput?: string;
|
|
120
|
-
message: string;
|
|
121
|
-
}): AcceptanceFinalizationTurn {
|
|
122
|
-
return {
|
|
123
|
-
turn: input.turn,
|
|
124
|
-
prompt: input.prompt,
|
|
125
|
-
status: "rejected",
|
|
126
|
-
...(input.rawOutput ? { rawOutput: input.rawOutput } : {}),
|
|
127
|
-
runtimeChecks: [{ id: "finalization-process", status: "failed", message: input.message }],
|
|
128
|
-
verifyRuns: [],
|
|
129
|
-
failureMessage: `Acceptance rejected: ${input.message}`,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function attachFinalizationToLedger(input: {
|
|
134
|
-
initialLedger: AcceptanceLedger;
|
|
135
|
-
authoritativeLedger: AcceptanceLedger;
|
|
136
|
-
turns: AcceptanceFinalizationTurn[];
|
|
137
|
-
status: "completed" | "failed";
|
|
138
|
-
maxTurns: number;
|
|
139
|
-
}): AcceptanceLedger {
|
|
140
|
-
return {
|
|
141
|
-
...input.authoritativeLedger,
|
|
142
|
-
...(input.initialLedger.childReport ? { initialChildReport: input.initialLedger.childReport } : {}),
|
|
143
|
-
...(input.initialLedger.childReportParseError ? { initialChildReportParseError: input.initialLedger.childReportParseError } : {}),
|
|
144
|
-
finalization: {
|
|
145
|
-
mode: "self-review-loop",
|
|
146
|
-
status: input.status,
|
|
147
|
-
maxTurns: input.maxTurns,
|
|
148
|
-
turns: input.turns,
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function buildFinalizationProcessFailureLedger(input: {
|
|
154
|
-
initialLedger: AcceptanceLedger;
|
|
155
|
-
turns: AcceptanceFinalizationTurn[];
|
|
156
|
-
maxTurns: number;
|
|
157
|
-
message: string;
|
|
158
|
-
}): AcceptanceLedger {
|
|
159
|
-
return attachFinalizationToLedger({
|
|
160
|
-
initialLedger: input.initialLedger,
|
|
161
|
-
authoritativeLedger: {
|
|
162
|
-
...input.initialLedger,
|
|
163
|
-
status: "rejected",
|
|
164
|
-
runtimeChecks: [
|
|
165
|
-
...input.initialLedger.runtimeChecks,
|
|
166
|
-
{ id: "finalization-process", status: "failed", message: input.message },
|
|
167
|
-
],
|
|
168
|
-
},
|
|
169
|
-
turns: input.turns,
|
|
170
|
-
status: "failed",
|
|
171
|
-
maxTurns: input.maxTurns,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AcceptanceReport,
|
|
3
|
-
} from "../../shared/types.ts";
|
|
4
|
-
|
|
5
|
-
function extractBalancedJson(text: string, start: number): string | undefined {
|
|
6
|
-
let depth = 0;
|
|
7
|
-
let inString = false;
|
|
8
|
-
let escaped = false;
|
|
9
|
-
for (let i = start; i < text.length; i++) {
|
|
10
|
-
const char = text[i]!;
|
|
11
|
-
if (inString) {
|
|
12
|
-
if (escaped) escaped = false;
|
|
13
|
-
else if (char === "\\") escaped = true;
|
|
14
|
-
else if (char === "\"") inString = false;
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
if (char === "\"") {
|
|
18
|
-
inString = true;
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
if (char === "{") depth++;
|
|
22
|
-
if (char === "}") {
|
|
23
|
-
depth--;
|
|
24
|
-
if (depth === 0) return text.slice(start, i + 1);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function parseAcceptanceReport(output: string): { report?: AcceptanceReport; error?: string } {
|
|
31
|
-
const fenced = [...output.matchAll(/```acceptance-report\s*\n([\s\S]*?)```/gi)]
|
|
32
|
-
.map((match) => match[1]?.trim())
|
|
33
|
-
.filter((value): value is string => Boolean(value));
|
|
34
|
-
const parseErrors: string[] = [];
|
|
35
|
-
for (const body of fenced) {
|
|
36
|
-
try {
|
|
37
|
-
const parsed = JSON.parse(body) as unknown;
|
|
38
|
-
const report = (parsed && typeof parsed === "object" && "acceptance" in parsed)
|
|
39
|
-
? (parsed as { acceptance?: unknown }).acceptance
|
|
40
|
-
: parsed;
|
|
41
|
-
if (isAcceptanceReport(report)) return { report };
|
|
42
|
-
parseErrors.push("acceptance-report block does not contain a valid acceptance report");
|
|
43
|
-
} catch (error) {
|
|
44
|
-
parseErrors.push(error instanceof Error ? error.message : String(error));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
if (parseErrors.length > 0) return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}` };
|
|
48
|
-
const markerIndex = output.search(/ACCEPTANCE_REPORT\s*:/i);
|
|
49
|
-
if (markerIndex !== -1) {
|
|
50
|
-
const jsonStart = output.indexOf("{", markerIndex);
|
|
51
|
-
if (jsonStart !== -1) {
|
|
52
|
-
const json = extractBalancedJson(output, jsonStart);
|
|
53
|
-
if (json) {
|
|
54
|
-
try {
|
|
55
|
-
const parsed = JSON.parse(json) as unknown;
|
|
56
|
-
if (isAcceptanceReport(parsed)) return { report: parsed };
|
|
57
|
-
} catch (error) {
|
|
58
|
-
return { error: error instanceof Error ? error.message : String(error) };
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return { error: "Structured acceptance report not found." };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function stripAcceptanceReport(output: string): string {
|
|
67
|
-
return output
|
|
68
|
-
.replace(/\n?```acceptance-report\s*\n[\s\S]*?```\s*$/i, "")
|
|
69
|
-
.replace(/\n?ACCEPTANCE_REPORT\s*:\s*\{[\s\S]*\}\s*$/i, "")
|
|
70
|
-
.trimEnd();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function isStringArray(value: unknown): value is string[] {
|
|
74
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function isCriterionReport(value: unknown): value is NonNullable<AcceptanceReport["criteriaSatisfied"]>[number] {
|
|
78
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
79
|
-
const criterion = value as { id?: unknown; status?: unknown; evidence?: unknown };
|
|
80
|
-
if (criterion.id !== undefined && typeof criterion.id !== "string") return false;
|
|
81
|
-
if (criterion.status !== "satisfied" && criterion.status !== "not-satisfied" && criterion.status !== "not-applicable") return false;
|
|
82
|
-
return typeof criterion.evidence === "string" && criterion.evidence.trim().length > 0;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function isCommandReport(value: unknown): value is NonNullable<AcceptanceReport["commandsRun"]>[number] {
|
|
86
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
87
|
-
const command = value as { command?: unknown; result?: unknown; summary?: unknown };
|
|
88
|
-
return typeof command.command === "string"
|
|
89
|
-
&& (command.result === "passed" || command.result === "failed" || command.result === "not-run")
|
|
90
|
-
&& typeof command.summary === "string";
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function isAcceptanceReport(value: unknown): value is AcceptanceReport {
|
|
94
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
95
|
-
const report = value as {
|
|
96
|
-
criteriaSatisfied?: unknown;
|
|
97
|
-
changedFiles?: unknown;
|
|
98
|
-
testsAddedOrUpdated?: unknown;
|
|
99
|
-
commandsRun?: unknown;
|
|
100
|
-
validationOutput?: unknown;
|
|
101
|
-
residualRisks?: unknown;
|
|
102
|
-
noStagedFiles?: unknown;
|
|
103
|
-
diffSummary?: unknown;
|
|
104
|
-
reviewFindings?: unknown;
|
|
105
|
-
manualNotes?: unknown;
|
|
106
|
-
notes?: unknown;
|
|
107
|
-
};
|
|
108
|
-
if (report.criteriaSatisfied !== undefined && (!Array.isArray(report.criteriaSatisfied) || !report.criteriaSatisfied.every(isCriterionReport))) return false;
|
|
109
|
-
if (report.changedFiles !== undefined && !isStringArray(report.changedFiles)) return false;
|
|
110
|
-
if (report.testsAddedOrUpdated !== undefined && !isStringArray(report.testsAddedOrUpdated)) return false;
|
|
111
|
-
if (report.commandsRun !== undefined && (!Array.isArray(report.commandsRun) || !report.commandsRun.every(isCommandReport))) return false;
|
|
112
|
-
if (report.validationOutput !== undefined && !isStringArray(report.validationOutput)) return false;
|
|
113
|
-
if (report.residualRisks !== undefined && !isStringArray(report.residualRisks)) return false;
|
|
114
|
-
if (report.noStagedFiles !== undefined && typeof report.noStagedFiles !== "boolean") return false;
|
|
115
|
-
if (report.diffSummary !== undefined && typeof report.diffSummary !== "string") return false;
|
|
116
|
-
if (report.reviewFindings !== undefined && !isStringArray(report.reviewFindings)) return false;
|
|
117
|
-
if (report.manualNotes !== undefined && typeof report.manualNotes !== "string") return false;
|
|
118
|
-
if (report.notes !== undefined && typeof report.notes !== "string") return false;
|
|
119
|
-
return report.criteriaSatisfied !== undefined
|
|
120
|
-
|| report.changedFiles !== undefined
|
|
121
|
-
|| report.testsAddedOrUpdated !== undefined
|
|
122
|
-
|| report.commandsRun !== undefined
|
|
123
|
-
|| report.residualRisks !== undefined
|
|
124
|
-
|| report.manualNotes !== undefined
|
|
125
|
-
|| report.reviewFindings !== undefined;
|
|
126
|
-
}
|
|
127
|
-
|