pi-subagents 0.34.0 → 0.35.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 +78 -9
- package/README.md +213 -32
- package/index.ts +1 -0
- package/install.mjs +1 -1
- package/package.json +23 -8
- package/prompts/review-loop.md +3 -1
- package/skills/pi-subagents/SKILL.md +87 -25
- package/src/agents/agent-management.ts +82 -15
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +91 -49
- package/src/agents/frontmatter.ts +67 -13
- package/src/agents/skills.ts +25 -12
- package/src/api/background-work.ts +197 -0
- package/src/api/delegation.ts +158 -0
- package/src/extension/chain-validation.ts +165 -0
- package/src/extension/doctor.ts +15 -0
- package/src/extension/fanout-child.ts +3 -1
- package/src/extension/index.ts +65 -124
- package/src/extension/rpc.ts +10 -2
- package/src/extension/schemas.ts +18 -14
- package/src/extension/steering-notices.ts +35 -0
- package/src/extension/tool-description.ts +20 -9
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/native-supervisor-channel.ts +9 -1
- package/src/intercom/result-intercom.ts +4 -0
- package/src/runs/background/async-execution.ts +293 -44
- package/src/runs/background/async-job-tracker.ts +56 -9
- package/src/runs/background/async-resume.ts +159 -52
- package/src/runs/background/async-status.ts +25 -18
- package/src/runs/background/auto-drain.ts +67 -0
- package/src/runs/background/chain-root-attachment.ts +16 -8
- package/src/runs/background/control-channel.ts +260 -13
- package/src/runs/background/fleet-view.ts +23 -2
- package/src/runs/background/notify.ts +79 -10
- package/src/runs/background/result-watcher.ts +12 -9
- package/src/runs/background/run-id-resolver.ts +14 -2
- package/src/runs/background/run-status.ts +23 -15
- package/src/runs/background/scheduled-runs.ts +3 -0
- package/src/runs/background/stale-run-reconciler.ts +32 -10
- package/src/runs/background/steering.ts +237 -0
- package/src/runs/background/subagent-runner.ts +898 -236
- package/src/runs/background/subagent-wait.ts +484 -0
- package/src/runs/background/top-level-async.ts +2 -1
- package/src/runs/background/wait-config.ts +36 -0
- package/src/runs/background/wait-tool.ts +26 -0
- package/src/runs/foreground/async-steering-action.ts +230 -0
- package/src/runs/foreground/chain-clarify.ts +22 -6
- package/src/runs/foreground/chain-execution.ts +50 -32
- package/src/runs/foreground/execution.ts +308 -94
- package/src/runs/foreground/subagent-executor.ts +592 -268
- package/src/runs/shared/acceptance.ts +355 -97
- package/src/runs/shared/child-protocol.ts +121 -0
- package/src/runs/shared/completion-guard.ts +8 -127
- package/src/runs/shared/dynamic-fanout.ts +6 -4
- package/src/runs/shared/model-fallback.ts +36 -0
- package/src/runs/shared/nested-events.ts +9 -4
- package/src/runs/shared/nested-render.ts +4 -1
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +34 -7
- package/src/runs/shared/pi-spawn.ts +18 -12
- package/src/runs/shared/session-lease.ts +279 -0
- package/src/runs/shared/single-output.ts +61 -6
- package/src/runs/shared/spawn-budget.ts +128 -0
- package/src/runs/shared/subagent-control.ts +10 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +127 -26
- package/src/runs/shared/task-intent.ts +176 -0
- package/src/runs/shared/tool-availability.ts +65 -0
- package/src/runs/shared/turn-budget.ts +49 -4
- package/src/shared/atomic-json.ts +4 -1
- package/src/shared/fork-context.ts +28 -3
- package/src/shared/model-info.ts +7 -4
- package/src/shared/status-format.ts +7 -1
- package/src/shared/types.ts +203 -25
- package/src/shared/utils.ts +35 -7
- package/src/slash/delegation-adapters.ts +457 -0
- package/src/slash/delegation-request.ts +103 -0
- package/src/slash/prompt-template-bridge.ts +167 -344
- package/src/slash/slash-commands.ts +239 -6
- package/src/slash/subagents-admin.ts +428 -0
- package/src/slash/subagents-editor.ts +86 -0
- package/src/tui/fleet.ts +405 -0
- package/src/tui/render.ts +90 -16
- package/src/watchdog/change-signature.ts +127 -0
- package/src/watchdog/child-status.ts +205 -0
- package/src/watchdog/emission-guard.ts +123 -0
- package/src/watchdog/lsp-diagnostics.ts +532 -0
- package/src/watchdog/model-selection.ts +167 -0
- package/src/watchdog/register-child.ts +117 -0
- package/src/watchdog/register-main.ts +433 -0
- package/src/watchdog/render.ts +54 -0
- package/src/watchdog/review.ts +293 -0
- package/src/watchdog/runtime.ts +712 -0
- package/src/watchdog/settings.ts +528 -0
- package/src/watchdog/tool-actions.ts +155 -0
- package/src/watchdog/turn-delta.ts +161 -0
- package/src/watchdog/types.ts +188 -0
- package/src/watchdog/warning-format.ts +73 -0
- package/src/runs/background/wait.ts +0 -394
|
@@ -9,7 +9,9 @@ import type {
|
|
|
9
9
|
AcceptanceLedger,
|
|
10
10
|
AcceptanceLevel,
|
|
11
11
|
AcceptanceReport,
|
|
12
|
+
AcceptanceRole,
|
|
12
13
|
AcceptanceRuntimeCheck,
|
|
14
|
+
AcceptanceRuntimeCheckStatus,
|
|
13
15
|
AcceptanceReviewResult,
|
|
14
16
|
AcceptanceVerifyCommand,
|
|
15
17
|
AcceptanceVerifyResult,
|
|
@@ -18,6 +20,7 @@ import type {
|
|
|
18
20
|
SingleResult,
|
|
19
21
|
SubagentRunMode,
|
|
20
22
|
} from "../../shared/types.ts";
|
|
23
|
+
import { classifyTaskMutationIntent, taskMayMutate } from "./task-intent.ts";
|
|
21
24
|
|
|
22
25
|
const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
|
|
23
26
|
none: 0,
|
|
@@ -43,6 +46,7 @@ const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify
|
|
|
43
46
|
const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
|
|
44
47
|
const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
|
|
45
48
|
const ACCEPTANCE_REVIEW_KEYS = new Set(["agent", "focus", "required"]);
|
|
49
|
+
const EXPLICIT_REVIEWED_UNAVAILABLE = "cannot be requested explicitly because this run cannot supply an independent reviewer result; use checked/verified and orchestrate the reviewer separately, or omit acceptance for read-only review tasks.";
|
|
46
50
|
|
|
47
51
|
function normalizeLevel(level: AcceptanceLevel | undefined): Exclude<AcceptanceLevel, "auto"> | "auto" {
|
|
48
52
|
return level ?? "auto";
|
|
@@ -68,6 +72,7 @@ function requiredEvidenceForLevel(level: Exclude<AcceptanceLevel, "auto">): Acce
|
|
|
68
72
|
|
|
69
73
|
function inferLevel(input: {
|
|
70
74
|
agentName: string;
|
|
75
|
+
acceptanceRole?: AcceptanceRole;
|
|
71
76
|
task?: string;
|
|
72
77
|
mode?: SubagentRunMode;
|
|
73
78
|
async?: boolean;
|
|
@@ -77,14 +82,27 @@ function inferLevel(input: {
|
|
|
77
82
|
const agent = input.agentName.toLowerCase();
|
|
78
83
|
const task = input.task?.toLowerCase() ?? "";
|
|
79
84
|
const reasons: string[] = [];
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|| /\
|
|
85
|
+
// Declared roles replace name heuristics, so use the full writer grammar to detect explicit mutation independently of the actual agent name.
|
|
86
|
+
const intent = classifyTaskMutationIntent(input.acceptanceRole ? "worker" : input.agentName, input.task ?? "");
|
|
87
|
+
const readOnlyTask = intent.kind === "read-only"
|
|
88
|
+
|| (intent.kind === "unknown" && /\b(?:read[- ]only|review[- ]only|no edits|without edits|inspect|summari[sz]e)\b/.test(task));
|
|
89
|
+
const rolePatchTask = input.acceptanceRole !== undefined
|
|
90
|
+
&& intent.kind !== "read-only"
|
|
91
|
+
&& !/\b(?:do not|don't|must not)\s+patch\b/.test(task)
|
|
92
|
+
&& /\bpatch\s+(?:(?:\.{0,2}[\\/])?(?:[\w.-]+[\\/])+[\w.-]+|[\w.-]+\.[a-z0-9]+\b|(?:the\s+)?parser\b)/.test(task);
|
|
93
|
+
const taskMayWrite = taskMayMutate(input.task ?? "") || intent.kind === "implementation" || rolePatchTask;
|
|
94
|
+
const readOnlyAgent = input.acceptanceRole === "read-only"
|
|
95
|
+
|| (input.acceptanceRole === undefined && /\b(?:reviewer|scout|context-builder|researcher|analyst)\b/.test(agent));
|
|
96
|
+
const writeTask = taskMayWrite
|
|
97
|
+
|| (input.acceptanceRole === "writer" && !readOnlyTask)
|
|
98
|
+
|| (input.acceptanceRole === undefined && /\bworker\b/.test(agent) && !readOnlyTask);
|
|
99
|
+
const inferredReadOnly = readOnlyTask || (input.acceptanceRole === "read-only" && !taskMayWrite);
|
|
100
|
+
const roleResolvesReadOnly = input.acceptanceRole !== undefined && inferredReadOnly;
|
|
101
|
+
const keywordRiskReadOnly = input.acceptanceRole === undefined ? intent.kind === "read-only" : inferredReadOnly;
|
|
84
102
|
const risky = Boolean(input.async && writeTask)
|
|
85
|
-
|| Boolean(input.dynamic)
|
|
86
|
-
|| Boolean(input.dynamicGroup)
|
|
87
|
-
|| /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task);
|
|
103
|
+
|| (Boolean(input.dynamic) && !roleResolvesReadOnly)
|
|
104
|
+
|| (Boolean(input.dynamicGroup) && !roleResolvesReadOnly)
|
|
105
|
+
|| (!keywordRiskReadOnly && /\b(?:release|migration|migrate|security|data[- ]loss|destructive|post-review|fix pass)\b/.test(task));
|
|
88
106
|
|
|
89
107
|
if (risky) {
|
|
90
108
|
reasons.push(input.async ? "async write-capable or risky run" : "risky write-capable run");
|
|
@@ -98,7 +116,7 @@ function inferLevel(input: {
|
|
|
98
116
|
};
|
|
99
117
|
}
|
|
100
118
|
if (writeTask && !readOnlyTask) {
|
|
101
|
-
reasons.push("write-capable worker/task");
|
|
119
|
+
reasons.push(input.acceptanceRole === "writer" && !taskMayWrite ? "declared writer acceptance role" : "write-capable worker/task");
|
|
102
120
|
return {
|
|
103
121
|
level: "checked",
|
|
104
122
|
reasons,
|
|
@@ -107,7 +125,7 @@ function inferLevel(input: {
|
|
|
107
125
|
};
|
|
108
126
|
}
|
|
109
127
|
if (readOnlyAgent || readOnlyTask) {
|
|
110
|
-
reasons.push(readOnlyAgent ? "read-only/reviewer-style agent" : "read-only task wording");
|
|
128
|
+
reasons.push(input.acceptanceRole === "read-only" && !readOnlyTask ? "declared read-only acceptance role" : readOnlyAgent ? "read-only/reviewer-style agent" : "read-only task wording");
|
|
111
129
|
return {
|
|
112
130
|
level: "attested",
|
|
113
131
|
reasons,
|
|
@@ -141,6 +159,8 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
141
159
|
if (input === false) return errors;
|
|
142
160
|
if (typeof input === "string") {
|
|
143
161
|
if (!VALID_LEVELS.has(input as AcceptanceLevel)) errors.push(`${pathLabel} has invalid level '${input}'.`);
|
|
162
|
+
else if (input === "none") errors.push(`${pathLabel} level "none" requires a reason; use { level: "none", reason: "..." }.`);
|
|
163
|
+
else if (input === "reviewed") errors.push(`${pathLabel} ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
144
164
|
return errors;
|
|
145
165
|
}
|
|
146
166
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
@@ -157,9 +177,11 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
157
177
|
if (value.level === "none" && (typeof value.reason !== "string" || !value.reason.trim())) {
|
|
158
178
|
errors.push(`${pathLabel}.reason is required when level is none.`);
|
|
159
179
|
}
|
|
180
|
+
if (value.level === "reviewed") errors.push(`${pathLabel}.level ${EXPLICIT_REVIEWED_UNAVAILABLE}`);
|
|
160
181
|
if (value.reason !== undefined && typeof value.reason !== "string") errors.push(`${pathLabel}.reason must be a string.`);
|
|
161
182
|
if (value.criteria !== undefined && !Array.isArray(value.criteria)) errors.push(`${pathLabel}.criteria must be an array.`);
|
|
162
183
|
if (Array.isArray(value.criteria)) {
|
|
184
|
+
const criterionIds = new Set<string>();
|
|
163
185
|
for (const [index, criterion] of value.criteria.entries()) {
|
|
164
186
|
if (typeof criterion === "string") continue;
|
|
165
187
|
const criterionPath = `${pathLabel}.criteria[${index}]`;
|
|
@@ -171,7 +193,13 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
171
193
|
for (const key of Object.keys(gate)) {
|
|
172
194
|
if (!ACCEPTANCE_GATE_KEYS.has(key)) errors.push(`${criterionPath}.${key} is not supported.`);
|
|
173
195
|
}
|
|
174
|
-
if (typeof gate.id !== "string" || !gate.id.trim())
|
|
196
|
+
if (typeof gate.id !== "string" || !gate.id.trim()) {
|
|
197
|
+
errors.push(`${criterionPath}.id is required.`);
|
|
198
|
+
} else {
|
|
199
|
+
const normalizedId = normalizedToken(gate.id);
|
|
200
|
+
if (criterionIds.has(normalizedId)) errors.push(`${criterionPath}.id duplicates normalized criterion id '${normalizedId}'.`);
|
|
201
|
+
criterionIds.add(normalizedId);
|
|
202
|
+
}
|
|
175
203
|
if (typeof gate.must !== "string" || !gate.must.trim()) errors.push(`${criterionPath}.must is required.`);
|
|
176
204
|
if (gate.evidence !== undefined && !Array.isArray(gate.evidence)) errors.push(`${criterionPath}.evidence must be an array.`);
|
|
177
205
|
if (Array.isArray(gate.evidence)) {
|
|
@@ -248,6 +276,31 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
248
276
|
return errors;
|
|
249
277
|
}
|
|
250
278
|
|
|
279
|
+
export function validateExecutionAcceptance(input: {
|
|
280
|
+
acceptance?: unknown;
|
|
281
|
+
tasks?: Array<{ acceptance?: unknown }>;
|
|
282
|
+
chain?: Array<{
|
|
283
|
+
acceptance?: unknown;
|
|
284
|
+
parallel?: Array<{ acceptance?: unknown }> | { acceptance?: unknown };
|
|
285
|
+
}>;
|
|
286
|
+
}): string[] {
|
|
287
|
+
const errors = validateAcceptanceInput(input.acceptance, "acceptance");
|
|
288
|
+
for (const [index, task] of (input.tasks ?? []).entries()) {
|
|
289
|
+
errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
|
|
290
|
+
}
|
|
291
|
+
for (const [stepIndex, step] of (input.chain ?? []).entries()) {
|
|
292
|
+
errors.push(...validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`));
|
|
293
|
+
if (Array.isArray(step.parallel)) {
|
|
294
|
+
for (const [taskIndex, task] of step.parallel.entries()) {
|
|
295
|
+
errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
|
|
296
|
+
}
|
|
297
|
+
} else if (step.parallel) {
|
|
298
|
+
errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return errors;
|
|
302
|
+
}
|
|
303
|
+
|
|
251
304
|
function normalizeCriteria(criteria: Array<string | { id?: string; must?: string; evidence?: AcceptanceEvidenceKind[]; severity?: "required" | "recommended" }> | undefined, evidence: AcceptanceEvidenceKind[]): ResolvedAcceptanceGate[] {
|
|
252
305
|
return (criteria ?? []).map((criterion, index) => {
|
|
253
306
|
if (typeof criterion === "string") {
|
|
@@ -265,6 +318,7 @@ function normalizeCriteria(criteria: Array<string | { id?: string; must?: string
|
|
|
265
318
|
export function resolveEffectiveAcceptance(input: {
|
|
266
319
|
explicit?: AcceptanceInput;
|
|
267
320
|
agentName: string;
|
|
321
|
+
acceptanceRole?: AcceptanceRole;
|
|
268
322
|
task?: string;
|
|
269
323
|
mode?: SubagentRunMode;
|
|
270
324
|
async?: boolean;
|
|
@@ -285,7 +339,7 @@ export function resolveEffectiveAcceptance(input: {
|
|
|
285
339
|
evidence,
|
|
286
340
|
);
|
|
287
341
|
let review = explicit.review !== undefined ? explicit.review : inferred.review;
|
|
288
|
-
if (level === "reviewed" &&
|
|
342
|
+
if (level === "reviewed" && input.explicit !== undefined && explicitLevel !== "reviewed" && explicit.review === undefined && review && review !== false) {
|
|
289
343
|
review = { ...review, required: false };
|
|
290
344
|
}
|
|
291
345
|
return {
|
|
@@ -329,9 +383,14 @@ export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig): st
|
|
|
329
383
|
"",
|
|
330
384
|
"Finish with a fenced JSON block tagged `acceptance-report` in this shape:",
|
|
331
385
|
"Use empty arrays when no items apply; array fields contain strings unless object entries are shown.",
|
|
386
|
+
"`criteriaSatisfied[].status` must be exactly one of: satisfied, not-satisfied, not-applicable.",
|
|
387
|
+
"`commandsRun[].result` must be exactly one of: passed, failed, not-run.",
|
|
388
|
+
"`manualNotes` and `notes` are optional strings; an empty string means no note and does not satisfy `manual-notes` evidence.",
|
|
332
389
|
"```acceptance-report",
|
|
333
390
|
JSON.stringify({
|
|
334
|
-
criteriaSatisfied:
|
|
391
|
+
criteriaSatisfied: acceptance.criteria
|
|
392
|
+
.filter((criterion) => criterion.severity !== "recommended")
|
|
393
|
+
.map((criterion) => ({ id: criterion.id, status: "satisfied", evidence: "specific proof" })),
|
|
335
394
|
changedFiles: ["src/file.ts"],
|
|
336
395
|
testsAddedOrUpdated: ["test/file.test.ts"],
|
|
337
396
|
commandsRun: [{ command: "command", result: "passed", summary: "short result" }],
|
|
@@ -372,38 +431,166 @@ function extractBalancedJson(text: string, start: number): string | undefined {
|
|
|
372
431
|
return undefined;
|
|
373
432
|
}
|
|
374
433
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
434
|
+
const ACCEPTANCE_REPORT_WRAPPERS = new Set(["acceptance", "acceptance-report", "acceptance_report", "acceptanceReport"]);
|
|
435
|
+
|
|
436
|
+
const ACCEPTANCE_REPORT_FIELDS: Record<string, keyof AcceptanceReport> = {
|
|
437
|
+
criteriaSatisfied: "criteriaSatisfied",
|
|
438
|
+
criteria_satisfied: "criteriaSatisfied",
|
|
439
|
+
changedFiles: "changedFiles",
|
|
440
|
+
changed_files: "changedFiles",
|
|
441
|
+
testsAddedOrUpdated: "testsAddedOrUpdated",
|
|
442
|
+
tests_added_or_updated: "testsAddedOrUpdated",
|
|
443
|
+
commandsRun: "commandsRun",
|
|
444
|
+
commands_run: "commandsRun",
|
|
445
|
+
validationOutput: "validationOutput",
|
|
446
|
+
validation_output: "validationOutput",
|
|
447
|
+
residualRisks: "residualRisks",
|
|
448
|
+
residual_risks: "residualRisks",
|
|
449
|
+
noStagedFiles: "noStagedFiles",
|
|
450
|
+
no_staged_files: "noStagedFiles",
|
|
451
|
+
diffSummary: "diffSummary",
|
|
452
|
+
diff_summary: "diffSummary",
|
|
453
|
+
reviewFindings: "reviewFindings",
|
|
454
|
+
review_findings: "reviewFindings",
|
|
455
|
+
manualNotes: "manualNotes",
|
|
456
|
+
manual_notes: "manualNotes",
|
|
457
|
+
notes: "notes",
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const CRITERION_REPORT_FIELDS = new Set(["id", "status", "evidence"]);
|
|
461
|
+
const COMMAND_REPORT_FIELDS = new Set(["command", "result", "summary"]);
|
|
462
|
+
|
|
463
|
+
function normalizedToken(value: string): string {
|
|
464
|
+
return value.trim().toLowerCase().replace(/[\s_]+/g, "-").replace(/-+/g, "-");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function normalizeCriterionStatus(value: unknown): unknown {
|
|
468
|
+
if (typeof value !== "string") return value;
|
|
469
|
+
const token = normalizedToken(value);
|
|
470
|
+
if (["satisfied", "met", "complete", "completed", "done", "pass", "passed", "success", "succeeded"].includes(token)) return "satisfied";
|
|
471
|
+
if (["not-satisfied", "not-met", "unmet", "incomplete", "fail", "failed"].includes(token)) return "not-satisfied";
|
|
472
|
+
if (["not-applicable", "n-a", "na", "skip", "skipped"].includes(token)) return "not-applicable";
|
|
380
473
|
return value;
|
|
381
474
|
}
|
|
382
475
|
|
|
383
|
-
function
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
476
|
+
function normalizeCommandResult(value: unknown): unknown {
|
|
477
|
+
if (typeof value !== "string") return value;
|
|
478
|
+
const token = normalizedToken(value);
|
|
479
|
+
if (["passed", "pass", "success", "successful", "succeeded", "ok"].includes(token)) return "passed";
|
|
480
|
+
if (["failed", "fail", "failure", "error"].includes(token)) return "failed";
|
|
481
|
+
if (["not-run", "not-executed", "skip", "skipped"].includes(token)) return "not-run";
|
|
482
|
+
return value;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function normalizeCriterionReport(value: unknown, pathLabel: string, errors: string[]): unknown {
|
|
486
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
487
|
+
const normalized: Record<string, unknown> = {};
|
|
488
|
+
for (const [key, fieldValue] of Object.entries(value as Record<string, unknown>)) {
|
|
489
|
+
if (!CRITERION_REPORT_FIELDS.has(key)) {
|
|
490
|
+
errors.push(`${pathLabel}.${key}: unsupported acceptance criterion field`);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
normalized[key] = key === "id" && typeof fieldValue === "string"
|
|
494
|
+
? normalizedToken(fieldValue)
|
|
495
|
+
: key === "status"
|
|
496
|
+
? normalizeCriterionStatus(fieldValue)
|
|
497
|
+
: fieldValue;
|
|
498
|
+
}
|
|
499
|
+
return normalized;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function normalizeCommandReport(value: unknown, pathLabel: string, errors: string[]): unknown {
|
|
503
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
504
|
+
const normalized: Record<string, unknown> = {};
|
|
505
|
+
for (const [key, fieldValue] of Object.entries(value as Record<string, unknown>)) {
|
|
506
|
+
if (!COMMAND_REPORT_FIELDS.has(key)) {
|
|
507
|
+
errors.push(`${pathLabel}.${key}: unsupported acceptance command field`);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
normalized[key] = key === "result" ? normalizeCommandResult(fieldValue) : fieldValue;
|
|
511
|
+
}
|
|
512
|
+
return normalized;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function normalizeAcceptanceReportValue(value: unknown, pathLabel = ""): { value: unknown; pathLabel: string; errors: string[] } {
|
|
516
|
+
const errors: string[] = [];
|
|
517
|
+
let reportValue = value;
|
|
518
|
+
let reportPath = pathLabel;
|
|
519
|
+
if (reportValue && typeof reportValue === "object" && !Array.isArray(reportValue)) {
|
|
520
|
+
const record = reportValue as Record<string, unknown>;
|
|
521
|
+
const wrapperKeys = Object.keys(record).filter((key) => ACCEPTANCE_REPORT_WRAPPERS.has(key));
|
|
522
|
+
if (wrapperKeys.length > 0) {
|
|
523
|
+
const wrapperKey = wrapperKeys[0]!;
|
|
524
|
+
if (wrapperKeys.length > 1) errors.push(`${pathLabel || "acceptance-report"}: multiple acceptance report wrappers are ambiguous`);
|
|
525
|
+
for (const key of Object.keys(record)) {
|
|
526
|
+
if (key !== wrapperKey) errors.push(`${pathFor(pathLabel, key)}: unsupported alongside acceptance report wrapper '${wrapperKey}'`);
|
|
527
|
+
}
|
|
528
|
+
reportValue = record[wrapperKey];
|
|
529
|
+
reportPath = pathFor(pathLabel, wrapperKey);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (!reportValue || typeof reportValue !== "object" || Array.isArray(reportValue)) return { value: reportValue, pathLabel: reportPath, errors };
|
|
533
|
+
|
|
534
|
+
const normalized: Record<string, unknown> = {};
|
|
535
|
+
for (const [key, fieldValue] of Object.entries(reportValue as Record<string, unknown>)) {
|
|
536
|
+
const canonical = ACCEPTANCE_REPORT_FIELDS[key];
|
|
537
|
+
if (!canonical) {
|
|
538
|
+
errors.push(`${pathFor(reportPath, key)}: unsupported acceptance report field`);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (Object.hasOwn(normalized, canonical)) {
|
|
542
|
+
errors.push(`${pathFor(reportPath, key)}: duplicates normalized field '${canonical}'`);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const fieldPath = pathFor(reportPath, canonical);
|
|
546
|
+
switch (canonical) {
|
|
547
|
+
case "criteriaSatisfied": {
|
|
548
|
+
const items = Array.isArray(fieldValue) ? fieldValue : fieldValue && typeof fieldValue === "object" ? [fieldValue] : fieldValue;
|
|
549
|
+
normalized[canonical] = Array.isArray(items)
|
|
550
|
+
? items.map((item, index) => normalizeCriterionReport(item, `${fieldPath}[${index}]`, errors))
|
|
551
|
+
: items;
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case "commandsRun": {
|
|
555
|
+
const items = Array.isArray(fieldValue) ? fieldValue : fieldValue && typeof fieldValue === "object" ? [fieldValue] : fieldValue;
|
|
556
|
+
normalized[canonical] = Array.isArray(items)
|
|
557
|
+
? items.map((item, index) => normalizeCommandReport(item, `${fieldPath}[${index}]`, errors))
|
|
558
|
+
: items;
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case "changedFiles":
|
|
562
|
+
case "testsAddedOrUpdated":
|
|
563
|
+
case "validationOutput":
|
|
564
|
+
case "residualRisks":
|
|
565
|
+
case "reviewFindings":
|
|
566
|
+
normalized[canonical] = typeof fieldValue === "string" ? [fieldValue] : fieldValue;
|
|
567
|
+
break;
|
|
568
|
+
case "noStagedFiles": {
|
|
569
|
+
const token = typeof fieldValue === "string" ? fieldValue.trim().toLowerCase() : undefined;
|
|
570
|
+
normalized[canonical] = token === "true" ? true : token === "false" ? false : fieldValue;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
default:
|
|
574
|
+
normalized[canonical] = fieldValue;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return { value: normalized, pathLabel: reportPath, errors };
|
|
391
578
|
}
|
|
392
579
|
|
|
393
580
|
function hasGenericAcceptanceReportSignal(value: unknown): boolean {
|
|
394
581
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
395
582
|
const record = value as Record<string, unknown>;
|
|
396
|
-
return "criteriaSatisfied" in record &&
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
);
|
|
583
|
+
return "criteriaSatisfied" in record && [
|
|
584
|
+
"changedFiles",
|
|
585
|
+
"testsAddedOrUpdated",
|
|
586
|
+
"commandsRun",
|
|
587
|
+
"validationOutput",
|
|
588
|
+
"residualRisks",
|
|
589
|
+
"noStagedFiles",
|
|
590
|
+
"diffSummary",
|
|
591
|
+
"reviewFindings",
|
|
592
|
+
"manualNotes",
|
|
593
|
+
].some((key) => key in record);
|
|
407
594
|
}
|
|
408
595
|
|
|
409
596
|
function parseReportJson(body: string): unknown {
|
|
@@ -426,30 +613,29 @@ function fencedBlocks(output: string, tag: string): string[] {
|
|
|
426
613
|
.filter((value): value is string => Boolean(value));
|
|
427
614
|
}
|
|
428
615
|
|
|
429
|
-
function validationPathLabelForWrapper(value: unknown): string {
|
|
430
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
|
|
431
|
-
const record = value as Record<string, unknown>;
|
|
432
|
-
if ("acceptance" in record) return "acceptance";
|
|
433
|
-
if ("acceptance-report" in record) return "acceptance-report";
|
|
434
|
-
return "";
|
|
435
|
-
}
|
|
436
|
-
|
|
437
616
|
function parseAcceptanceReportBody(body: string): { report?: AcceptanceReport; errors: string[] } {
|
|
438
|
-
|
|
439
|
-
const report = unwrapAcceptanceReport(parsed);
|
|
440
|
-
return validateAcceptanceReport(report, validationPathLabelForWrapper(parsed));
|
|
617
|
+
return validateAcceptanceReport(parseReportJson(body));
|
|
441
618
|
}
|
|
442
619
|
|
|
443
|
-
function parseGenericJsonAcceptanceReportBody(body: string): AcceptanceReport
|
|
620
|
+
function parseGenericJsonAcceptanceReportBody(body: string): { report?: AcceptanceReport; error?: string } {
|
|
444
621
|
const parsed = parseReportJson(body);
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
622
|
+
const normalized = normalizeAcceptanceReportValue(parsed);
|
|
623
|
+
const hasCriteriaMarker = normalized.value !== null
|
|
624
|
+
&& typeof normalized.value === "object"
|
|
625
|
+
&& !Array.isArray(normalized.value)
|
|
626
|
+
&& "criteriaSatisfied" in normalized.value;
|
|
627
|
+
if (!hasGenericAcceptanceReportSignal(normalized.value) && !(hasCriteriaMarker && normalized.errors.length > 0)) return {};
|
|
628
|
+
const validation = validateAcceptanceReport(parsed);
|
|
629
|
+
return validation.report
|
|
630
|
+
? { report: validation.report }
|
|
631
|
+
: { error: `Invalid acceptance-report: ${validation.errors.join("; ")}` };
|
|
449
632
|
}
|
|
450
633
|
|
|
634
|
+
export const ACCEPTANCE_REPORT_NOT_FOUND = "Structured acceptance report not found.";
|
|
635
|
+
|
|
451
636
|
export function parseAcceptanceReport(output: string): { report?: AcceptanceReport; error?: string } {
|
|
452
|
-
const
|
|
637
|
+
const explicitFencePresent = /```acceptance[-_]report\b/i.test(output);
|
|
638
|
+
const fenced = fencedBlocks(output, "acceptance[-_]report");
|
|
453
639
|
const parseErrors: string[] = [];
|
|
454
640
|
for (const body of fenced) {
|
|
455
641
|
try {
|
|
@@ -461,38 +647,65 @@ export function parseAcceptanceReport(output: string): { report?: AcceptanceRepo
|
|
|
461
647
|
}
|
|
462
648
|
}
|
|
463
649
|
if (parseErrors.length > 0) return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}` };
|
|
650
|
+
if (explicitFencePresent) {
|
|
651
|
+
return { error: "Failed to parse acceptance-report: Empty or unterminated acceptance-report fence." };
|
|
652
|
+
}
|
|
464
653
|
for (const body of fencedBlocks(output, "(?:json|jsonc|json5)")) {
|
|
465
654
|
try {
|
|
466
|
-
const
|
|
467
|
-
if (report) return { report };
|
|
655
|
+
const parsed = parseGenericJsonAcceptanceReportBody(body);
|
|
656
|
+
if (parsed.report) return { report: parsed.report };
|
|
657
|
+
if (parsed.error) return { error: `Failed to parse acceptance-report: ${parsed.error}` };
|
|
468
658
|
} catch {
|
|
469
|
-
// Ignore unrelated
|
|
470
|
-
//
|
|
659
|
+
// Ignore unrelated malformed generic JSON. A recognizable report shape
|
|
660
|
+
// returns exact validation errors above instead of being mistaken for prose.
|
|
471
661
|
}
|
|
472
662
|
}
|
|
473
663
|
const markerIndex = output.search(/ACCEPTANCE_REPORT\s*:/i);
|
|
474
664
|
if (markerIndex !== -1) {
|
|
475
665
|
const jsonStart = output.indexOf("{", markerIndex);
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (json) {
|
|
479
|
-
try {
|
|
480
|
-
const parsed = JSON.parse(json) as unknown;
|
|
481
|
-
const report = unwrapAcceptanceReport(parsed);
|
|
482
|
-
const validation = validateAcceptanceReport(report, validationPathLabelForWrapper(parsed));
|
|
483
|
-
if (validation.report) return { report: validation.report };
|
|
484
|
-
return { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}` };
|
|
485
|
-
} catch (error) {
|
|
486
|
-
return { error: error instanceof Error ? error.message : String(error) };
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
666
|
+
if (jsonStart === -1) {
|
|
667
|
+
return { error: "Failed to parse acceptance-report: Expected a JSON object after ACCEPTANCE_REPORT:." };
|
|
490
668
|
}
|
|
491
|
-
|
|
669
|
+
const json = extractBalancedJson(output, jsonStart);
|
|
670
|
+
if (!json) {
|
|
671
|
+
return { error: "Failed to parse acceptance-report: Unterminated JSON object after ACCEPTANCE_REPORT:." };
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
const parsed = JSON.parse(json) as unknown;
|
|
675
|
+
const validation = validateAcceptanceReport(parsed);
|
|
676
|
+
if (validation.report) return { report: validation.report };
|
|
677
|
+
return { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}` };
|
|
678
|
+
} catch (error) {
|
|
679
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
680
|
+
return { error: `Failed to parse acceptance-report: ${message}` };
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return { error: ACCEPTANCE_REPORT_NOT_FOUND };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function parseAcceptanceReportSources(
|
|
687
|
+
output: string,
|
|
688
|
+
fileOutput: { content: string; path: string; authoritative?: boolean } | undefined,
|
|
689
|
+
): { report?: AcceptanceReport; error?: string } {
|
|
690
|
+
const fromText = () => parseAcceptanceReport(output);
|
|
691
|
+
const fromFile = () => {
|
|
692
|
+
if (!fileOutput) return { error: ACCEPTANCE_REPORT_NOT_FOUND };
|
|
693
|
+
const parsed = parseAcceptanceReport(fileOutput.content);
|
|
694
|
+
return parsed.report || parsed.error === ACCEPTANCE_REPORT_NOT_FOUND
|
|
695
|
+
? parsed
|
|
696
|
+
: { error: `${parsed.error} (in configured output ${fileOutput.path})` };
|
|
697
|
+
};
|
|
698
|
+
const [primary, secondary] = fileOutput?.authoritative ? [fromFile, fromText] : [fromText, fromFile];
|
|
699
|
+
const first = primary();
|
|
700
|
+
// A malformed report in the primary source is a defect to surface, not a
|
|
701
|
+
// miss to paper over with the secondary source; only a genuinely absent
|
|
702
|
+
// report falls through.
|
|
703
|
+
if (first.report || first.error !== ACCEPTANCE_REPORT_NOT_FOUND) return first;
|
|
704
|
+
return secondary();
|
|
492
705
|
}
|
|
493
706
|
|
|
494
707
|
export function stripAcceptanceReport(output: string): string {
|
|
495
|
-
const trailingFencePattern = /\n?```(acceptance-report|json|jsonc|json5)\s*\n([\s\S]*?)```\s*/gi;
|
|
708
|
+
const trailingFencePattern = /\n?```(acceptance[-_]report|json|jsonc|json5)\s*\n([\s\S]*?)```\s*/gi;
|
|
496
709
|
let trailingFence: { index: number; tag: string; body: string } | undefined;
|
|
497
710
|
for (const match of output.matchAll(trailingFencePattern)) {
|
|
498
711
|
const end = (match.index ?? 0) + match[0].length;
|
|
@@ -501,15 +714,15 @@ export function stripAcceptanceReport(output: string): string {
|
|
|
501
714
|
}
|
|
502
715
|
}
|
|
503
716
|
if (trailingFence) {
|
|
504
|
-
if (trailingFence.tag === "acceptance-report") return output.slice(0, trailingFence.index).trimEnd();
|
|
717
|
+
if (trailingFence.tag === "acceptance-report" || trailingFence.tag === "acceptance_report") return output.slice(0, trailingFence.index).trimEnd();
|
|
505
718
|
try {
|
|
506
|
-
if (parseGenericJsonAcceptanceReportBody(trailingFence.body)) return output.slice(0, trailingFence.index).trimEnd();
|
|
719
|
+
if (parseGenericJsonAcceptanceReportBody(trailingFence.body).report) return output.slice(0, trailingFence.index).trimEnd();
|
|
507
720
|
} catch {
|
|
508
721
|
// Leave unrelated or malformed generic JSON fences visible.
|
|
509
722
|
}
|
|
510
723
|
}
|
|
511
724
|
return output
|
|
512
|
-
.replace(/\n?```acceptance-report\s*\n[\s\S]*?```\s*$/i, "")
|
|
725
|
+
.replace(/\n?```acceptance[-_]report\s*\n[\s\S]*?```\s*$/i, "")
|
|
513
726
|
.replace(/\n?ACCEPTANCE_REPORT\s*:\s*\{[\s\S]*\}\s*$/i, "")
|
|
514
727
|
.trimEnd();
|
|
515
728
|
}
|
|
@@ -544,12 +757,15 @@ function validateStringArrayField(errors: string[], value: unknown, pathLabel: s
|
|
|
544
757
|
return;
|
|
545
758
|
}
|
|
546
759
|
for (const [index, item] of value.entries()) {
|
|
547
|
-
if (typeof item !== "string") pushTypeError(errors, `${pathLabel}[${index}]`, "string", item);
|
|
760
|
+
if (typeof item !== "string" || !item.trim()) pushTypeError(errors, `${pathLabel}[${index}]`, "non-empty string", item);
|
|
548
761
|
}
|
|
549
762
|
}
|
|
550
763
|
|
|
551
764
|
function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: AcceptanceReport; errors: string[] } {
|
|
552
|
-
const
|
|
765
|
+
const normalized = normalizeAcceptanceReportValue(value, pathLabel);
|
|
766
|
+
value = normalized.value;
|
|
767
|
+
pathLabel = normalized.pathLabel;
|
|
768
|
+
const errors = normalized.errors;
|
|
553
769
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
554
770
|
pushTypeError(errors, pathLabel || "acceptance-report", "object", value);
|
|
555
771
|
return { errors };
|
|
@@ -559,6 +775,7 @@ function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: Ac
|
|
|
559
775
|
if (!Array.isArray(report.criteriaSatisfied)) {
|
|
560
776
|
pushTypeError(errors, pathFor(pathLabel, "criteriaSatisfied"), "array", report.criteriaSatisfied);
|
|
561
777
|
} else {
|
|
778
|
+
const criterionIds = new Set<string>();
|
|
562
779
|
for (const [index, item] of report.criteriaSatisfied.entries()) {
|
|
563
780
|
const itemPath = `${pathFor(pathLabel, "criteriaSatisfied")}[${index}]`;
|
|
564
781
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
@@ -566,7 +783,12 @@ function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: Ac
|
|
|
566
783
|
continue;
|
|
567
784
|
}
|
|
568
785
|
const criterion = item as { id?: unknown; status?: unknown; evidence?: unknown };
|
|
569
|
-
if (criterion.id !== undefined && typeof criterion.id !== "string")
|
|
786
|
+
if (criterion.id !== undefined && typeof criterion.id !== "string") {
|
|
787
|
+
pushTypeError(errors, `${itemPath}.id`, "string", criterion.id);
|
|
788
|
+
} else if (typeof criterion.id === "string" && criterion.id) {
|
|
789
|
+
if (criterionIds.has(criterion.id)) errors.push(`${itemPath}.id: duplicate normalized criterion id '${criterion.id}'`);
|
|
790
|
+
criterionIds.add(criterion.id);
|
|
791
|
+
}
|
|
570
792
|
if (criterion.status !== "satisfied" && criterion.status !== "not-satisfied" && criterion.status !== "not-applicable") {
|
|
571
793
|
pushTypeError(errors, `${itemPath}.status`, "one of \"satisfied\", \"not-satisfied\", \"not-applicable\"", criterion.status);
|
|
572
794
|
}
|
|
@@ -591,14 +813,14 @@ function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: Ac
|
|
|
591
813
|
if (command.result !== "passed" && command.result !== "failed" && command.result !== "not-run") {
|
|
592
814
|
pushTypeError(errors, `${itemPath}.result`, "one of \"passed\", \"failed\", \"not-run\"", command.result);
|
|
593
815
|
}
|
|
594
|
-
if (typeof command.summary !== "string") pushTypeError(errors, `${itemPath}.summary`, "string", command.summary);
|
|
816
|
+
if (typeof command.summary !== "string" || !command.summary.trim()) pushTypeError(errors, `${itemPath}.summary`, "non-empty string", command.summary);
|
|
595
817
|
}
|
|
596
818
|
}
|
|
597
819
|
}
|
|
598
820
|
if (report.validationOutput !== undefined) validateStringArrayField(errors, report.validationOutput, pathFor(pathLabel, "validationOutput"));
|
|
599
821
|
if (report.residualRisks !== undefined) validateStringArrayField(errors, report.residualRisks, pathFor(pathLabel, "residualRisks"));
|
|
600
822
|
if (report.noStagedFiles !== undefined && typeof report.noStagedFiles !== "boolean") pushTypeError(errors, pathFor(pathLabel, "noStagedFiles"), "boolean", report.noStagedFiles);
|
|
601
|
-
if (report.diffSummary !== undefined && typeof report.diffSummary !== "string") pushTypeError(errors, pathFor(pathLabel, "diffSummary"), "string", report.diffSummary);
|
|
823
|
+
if (report.diffSummary !== undefined && (typeof report.diffSummary !== "string" || !report.diffSummary.trim())) pushTypeError(errors, pathFor(pathLabel, "diffSummary"), "non-empty string", report.diffSummary);
|
|
602
824
|
if (report.reviewFindings !== undefined) validateStringArrayField(errors, report.reviewFindings, pathFor(pathLabel, "reviewFindings"));
|
|
603
825
|
if (report.manualNotes !== undefined && typeof report.manualNotes !== "string") pushTypeError(errors, pathFor(pathLabel, "manualNotes"), "string", report.manualNotes);
|
|
604
826
|
if (report.notes !== undefined && typeof report.notes !== "string") pushTypeError(errors, pathFor(pathLabel, "notes"), "string", report.notes);
|
|
@@ -620,26 +842,30 @@ function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: Ac
|
|
|
620
842
|
}
|
|
621
843
|
|
|
622
844
|
function checkCriteriaSatisfied(criteria: ResolvedAcceptanceGate[], report: AcceptanceReport): AcceptanceRuntimeCheck[] {
|
|
623
|
-
const reports = new Map((report.criteriaSatisfied ?? []).filter((item) => item.id).map((item) => [item.id
|
|
845
|
+
const reports = new Map((report.criteriaSatisfied ?? []).filter((item) => item.id).map((item) => [normalizedToken(item.id!), item]));
|
|
624
846
|
return criteria.filter((criterion) => criterion.severity !== "recommended").map((criterion) => {
|
|
625
|
-
const item = reports.get(criterion.id);
|
|
847
|
+
const item = reports.get(normalizedToken(criterion.id));
|
|
626
848
|
if (!item) return { id: `criterion:${criterion.id}`, status: "failed", message: `Required criterion '${criterion.id}' was not reported.` };
|
|
627
849
|
if (item.status !== "satisfied") return { id: `criterion:${criterion.id}`, status: "failed", message: `Required criterion '${criterion.id}' was reported as ${item.status}.` };
|
|
628
850
|
return { id: `criterion:${criterion.id}`, status: "passed", message: `Required criterion '${criterion.id}' satisfied.` };
|
|
629
851
|
});
|
|
630
852
|
}
|
|
631
853
|
|
|
632
|
-
function
|
|
854
|
+
function reportEvidenceStatus(report: AcceptanceReport, kind: AcceptanceEvidenceKind): AcceptanceRuntimeCheckStatus {
|
|
633
855
|
switch (kind) {
|
|
634
|
-
case "changed-files":
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
case "
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
case "
|
|
641
|
-
case "
|
|
642
|
-
case "
|
|
856
|
+
case "changed-files":
|
|
857
|
+
if (!isStringArray(report.changedFiles)) return "failed";
|
|
858
|
+
return report.changedFiles.length === 0 ? "not-applicable" : "passed";
|
|
859
|
+
case "tests-added":
|
|
860
|
+
if (!isStringArray(report.testsAddedOrUpdated)) return "failed";
|
|
861
|
+
return report.testsAddedOrUpdated.length === 0 ? "not-applicable" : "passed";
|
|
862
|
+
case "commands-run": return Array.isArray(report.commandsRun) && report.commandsRun.length > 0 ? "passed" : "failed";
|
|
863
|
+
case "validation-output": return isStringArray(report.validationOutput) && report.validationOutput.length > 0 ? "passed" : "failed";
|
|
864
|
+
case "residual-risks": return isStringArray(report.residualRisks) ? "passed" : "failed";
|
|
865
|
+
case "no-staged-files": return report.noStagedFiles === true ? "passed" : "failed";
|
|
866
|
+
case "diff-summary": return typeof report.diffSummary === "string" && report.diffSummary.trim().length > 0 ? "passed" : "failed";
|
|
867
|
+
case "review-findings": return isStringArray(report.reviewFindings) ? "passed" : "failed";
|
|
868
|
+
case "manual-notes": return Boolean((report.manualNotes ?? report.notes)?.trim()) ? "passed" : "failed";
|
|
643
869
|
}
|
|
644
870
|
}
|
|
645
871
|
|
|
@@ -657,11 +883,15 @@ function checkNoStagedFiles(cwd: string): AcceptanceRuntimeCheck {
|
|
|
657
883
|
function runStructuralChecks(acceptance: ResolvedAcceptanceConfig, report: AcceptanceReport, cwd: string): AcceptanceRuntimeCheck[] {
|
|
658
884
|
const checks: AcceptanceRuntimeCheck[] = [];
|
|
659
885
|
for (const kind of acceptance.evidence) {
|
|
660
|
-
const
|
|
886
|
+
const status = reportEvidenceStatus(report, kind);
|
|
661
887
|
checks.push({
|
|
662
888
|
id: `evidence:${kind}`,
|
|
663
|
-
status
|
|
664
|
-
message:
|
|
889
|
+
status,
|
|
890
|
+
message: status === "passed"
|
|
891
|
+
? `${kind} evidence present.`
|
|
892
|
+
: status === "not-applicable"
|
|
893
|
+
? `${kind} evidence explicitly reported as not applicable.`
|
|
894
|
+
: `${kind} evidence missing from child report.`,
|
|
665
895
|
});
|
|
666
896
|
}
|
|
667
897
|
if (acceptance.evidence.includes("no-staged-files")) checks.push(checkNoStagedFiles(cwd));
|
|
@@ -788,6 +1018,13 @@ export async function evaluateAcceptance(input: {
|
|
|
788
1018
|
acceptance: ResolvedAcceptanceConfig;
|
|
789
1019
|
output: string;
|
|
790
1020
|
cwd: string;
|
|
1021
|
+
/**
|
|
1022
|
+
* Content the child sent to its configured output file (from its own write
|
|
1023
|
+
* tool calls, not from disk, so a concurrent writer to the same path cannot
|
|
1024
|
+
* be misattributed). Searched for the acceptance report; searched before
|
|
1025
|
+
* the assistant output when `authoritative` (outputMode "file-only").
|
|
1026
|
+
*/
|
|
1027
|
+
fileOutput?: { content: string; path: string; authoritative?: boolean };
|
|
791
1028
|
report?: AcceptanceReport;
|
|
792
1029
|
reviewResult?: AcceptanceReviewResult;
|
|
793
1030
|
signal?: AbortSignal;
|
|
@@ -805,7 +1042,14 @@ export async function evaluateAcceptance(input: {
|
|
|
805
1042
|
};
|
|
806
1043
|
if (acceptance.level === "none") return ledger;
|
|
807
1044
|
|
|
808
|
-
const parsed = input.report
|
|
1045
|
+
const parsed = input.report
|
|
1046
|
+
? (() => {
|
|
1047
|
+
const validation = validateAcceptanceReport(input.report);
|
|
1048
|
+
return validation.report
|
|
1049
|
+
? { report: validation.report }
|
|
1050
|
+
: { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}` };
|
|
1051
|
+
})()
|
|
1052
|
+
: parseAcceptanceReportSources(input.output, input.fileOutput);
|
|
809
1053
|
if (parsed.report) {
|
|
810
1054
|
ledger.childReport = parsed.report;
|
|
811
1055
|
ledger.status = "attested";
|
|
@@ -867,6 +1111,20 @@ export async function evaluateAcceptance(input: {
|
|
|
867
1111
|
return ledger;
|
|
868
1112
|
}
|
|
869
1113
|
|
|
1114
|
+
export function buildSkippedAcceptanceLedger(acceptance: ResolvedAcceptanceConfig, input: { id: string; message: string }): AcceptanceLedger {
|
|
1115
|
+
return {
|
|
1116
|
+
status: acceptance.level === "none" ? "not-required" : "rejected",
|
|
1117
|
+
explicit: acceptance.explicit,
|
|
1118
|
+
effectiveAcceptance: acceptance,
|
|
1119
|
+
inferredReason: acceptance.inferredReason,
|
|
1120
|
+
criteria: acceptance.criteria,
|
|
1121
|
+
runtimeChecks: acceptance.level === "none"
|
|
1122
|
+
? []
|
|
1123
|
+
: [{ id: input.id, status: "failed", message: input.message }],
|
|
1124
|
+
verifyRuns: [],
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
870
1128
|
export function acceptanceFailureMessage(ledger: AcceptanceLedger): string | undefined {
|
|
871
1129
|
if (ledger.status !== "rejected") return undefined;
|
|
872
1130
|
const failedCheck = ledger.runtimeChecks.find((check) => check.status === "failed");
|