@kylecheng3146/agent-ops 0.5.2 → 0.6.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/README.md +64 -0
- package/dist/packages/cli/src/args.js +214 -10
- package/dist/packages/cli/src/bin.js +77 -17
- package/dist/packages/cli/src/cli.js +42 -10
- package/dist/packages/cli/src/commands/advance.js +173 -0
- package/dist/packages/cli/src/commands/review-show.js +189 -0
- package/dist/packages/cli/src/commands/review.js +201 -34
- package/dist/packages/cli/src/commands/run.js +111 -0
- package/dist/packages/cli/src/commands/task.js +62 -17
- package/dist/packages/cli/src/commands/verify.js +1 -0
- package/dist/packages/cli/src/context.js +63 -1
- package/dist/packages/cli/src/hook-process.js +31 -1
- package/dist/packages/cli/src/owned-run-step.js +65 -0
- package/dist/packages/cli/src/parallel-deps.js +18 -5
- package/dist/packages/cli/src/run-deps.js +314 -0
- package/dist/packages/cli/src/run-entry.js +943 -0
- package/dist/packages/cli/src/run-setup-entry.js +32 -0
- package/dist/packages/cli/src/run-step-entry.js +58 -0
- package/dist/runtime/src/config/explain.js +6 -0
- package/dist/runtime/src/config/hash.js +1 -1
- package/dist/runtime/src/config/merge.js +9 -1
- package/dist/runtime/src/contracts.js +2 -2
- package/dist/runtime/src/hooks/completion-gate.js +8 -2
- package/dist/runtime/src/install/doctor.js +2 -0
- package/dist/runtime/src/install/harness.js +1 -1
- package/dist/runtime/src/install/preauth.js +10 -1
- package/dist/runtime/src/parallel/finish.js +217 -39
- package/dist/runtime/src/parallel/guard.js +5 -1
- package/dist/runtime/src/parallel/integrate.js +416 -0
- package/dist/runtime/src/parallel/manage.js +4 -1
- package/dist/runtime/src/parallel/receipt.js +153 -0
- package/dist/runtime/src/parallel/service.js +45 -3
- package/dist/runtime/src/review/attestation.js +60 -11
- package/dist/runtime/src/review/execute.js +24 -1
- package/dist/runtime/src/review/packet.js +35 -1
- package/dist/runtime/src/review/render.js +71 -0
- package/dist/runtime/src/review/runner.js +12 -1
- package/dist/runtime/src/review/scope.js +18 -1
- package/dist/runtime/src/review/slots.js +35 -10
- package/dist/runtime/src/run/authorization.js +60 -0
- package/dist/runtime/src/run/budget.js +30 -0
- package/dist/runtime/src/run/controls.js +139 -0
- package/dist/runtime/src/run/convergence.js +177 -0
- package/dist/runtime/src/run/hosts/claude.js +180 -0
- package/dist/runtime/src/run/hosts/codex.js +279 -0
- package/dist/runtime/src/run/hosts/index.js +4 -0
- package/dist/runtime/src/run/hosts/protocol.js +254 -0
- package/dist/runtime/src/run/hosts/types.js +1 -0
- package/dist/runtime/src/run/hosts/util.js +136 -0
- package/dist/runtime/src/run/integration.js +204 -0
- package/dist/runtime/src/run/macOS.js +189 -0
- package/dist/runtime/src/run/ownership.js +22 -0
- package/dist/runtime/src/run/planning.js +25 -0
- package/dist/runtime/src/run/policy-transition.js +1076 -0
- package/dist/runtime/src/run/policy.js +176 -0
- package/dist/runtime/src/run/ratchet.js +71 -0
- package/dist/runtime/src/run/scheduler.js +260 -0
- package/dist/runtime/src/run/service.js +425 -0
- package/dist/runtime/src/run/supervisor.js +635 -0
- package/dist/runtime/src/run/transport.js +159 -0
- package/dist/runtime/src/run/types.js +1 -0
- package/dist/runtime/src/run/usage.js +81 -0
- package/dist/runtime/src/run/verification.js +64 -0
- package/dist/runtime/src/run/worker-gate.js +48 -0
- package/dist/runtime/src/schema/acceptance.js +91 -0
- package/dist/runtime/src/schema/validate.js +72 -7
- package/dist/runtime/src/task/completion.js +26 -6
- package/dist/runtime/src/task/contract.js +17 -0
- package/dist/runtime/src/task/pin-finding.js +81 -0
- package/dist/runtime/src/task/render.js +3 -0
- package/dist/runtime/src/task/service.js +361 -10
- package/dist/runtime/src/task/store.js +57 -7
- package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
- package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
- package/dist/runtime/src/verify/acceptance-replay.js +239 -0
- package/dist/runtime/src/verify/adapters/index.js +5 -0
- package/dist/runtime/src/verify/adapters/jest.js +142 -0
- package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
- package/dist/runtime/src/verify/adapters/node.js +254 -0
- package/dist/runtime/src/verify/adapters/pytest.js +188 -0
- package/dist/runtime/src/verify/adapters/rust.js +84 -0
- package/dist/runtime/src/verify/adapters/shared.js +82 -0
- package/dist/runtime/src/verify/adapters/vitest.js +218 -0
- package/dist/runtime/src/verify/evidence.js +5 -2
- package/dist/runtime/src/verify/service.js +24 -8
- package/dist/runtime/src/verify/source-fingerprint.js +1 -1
- package/dist/runtime/src/verify/spawn.js +13 -2
- package/docs/en/guides/configuration.md +10 -0
- package/docs/en/spec/acceptance-and-evidence.md +58 -0
- package/docs/en/spec/delegation.md +4 -4
- package/docs/en/spec/loop-engineering.md +49 -0
- package/docs/en/spec/review.md +24 -2
- package/docs/zh-TW/guides/configuration.md +9 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +31 -1
- package/docs/zh-TW/spec/delegation.md +4 -4
- package/docs/zh-TW/spec/loop-engineering.md +22 -0
- package/docs/zh-TW/spec/review.md +24 -2
- package/package.json +1 -1
- package/schemas/config.schema.json +221 -19
- package/schemas/evidence.schema.json +129 -6
- package/schemas/task.schema.json +268 -6
- package/templates/acceptance/agent_ops_acceptance.rs +36 -0
- package/templates/acceptance/agent_ops_pytest.py +46 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { parseJsonDocument, parseJsonLines, stableCheckId } from "../acceptance-protocol.js";
|
|
2
|
+
import { attempts, lower, object, result, run, string, terminal, terminalEvidence } from "./shared.js";
|
|
3
|
+
function inputEvents(input) {
|
|
4
|
+
if (Array.isArray(input))
|
|
5
|
+
return { events: input, diagnostics: [] };
|
|
6
|
+
if (typeof input !== "string") {
|
|
7
|
+
const value = object(input);
|
|
8
|
+
return value === undefined
|
|
9
|
+
? { events: [], diagnostics: ["malformed-output"] }
|
|
10
|
+
: { events: [value], diagnostics: [] };
|
|
11
|
+
}
|
|
12
|
+
const document = parseJsonDocument(input);
|
|
13
|
+
if (Array.isArray(document))
|
|
14
|
+
return { events: document, diagnostics: [] };
|
|
15
|
+
if (object(document) !== undefined)
|
|
16
|
+
return { events: [document], diagnostics: [] };
|
|
17
|
+
const lines = parseJsonLines(input);
|
|
18
|
+
return {
|
|
19
|
+
events: lines.values,
|
|
20
|
+
diagnostics: lines.invalidLines.map((line) => `invalid-json-line:${line}`)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function status(value) {
|
|
24
|
+
const normalized = lower(value);
|
|
25
|
+
if (normalized === "pass" || normalized === "passed" || normalized === "ok")
|
|
26
|
+
return "PASS";
|
|
27
|
+
if (normalized === "fail" || normalized === "failed")
|
|
28
|
+
return "FAIL";
|
|
29
|
+
return "UNKNOWN";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Collect the stdlib harness' JSON-lines output. Native libtest text without
|
|
33
|
+
* this explicit protocol is intentionally unsupported instead of guessed.
|
|
34
|
+
*/
|
|
35
|
+
export function collectRustAcceptance(input, options) {
|
|
36
|
+
const parsed = inputEvents(input);
|
|
37
|
+
const results = [];
|
|
38
|
+
const diagnostics = [...parsed.diagnostics];
|
|
39
|
+
let completed = false;
|
|
40
|
+
let completionEvidence = [];
|
|
41
|
+
for (const raw of parsed.events) {
|
|
42
|
+
const value = object(raw);
|
|
43
|
+
if (value === undefined) {
|
|
44
|
+
diagnostics.push("malformed-event");
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const type = lower(value.type ?? value.event ?? value.kind);
|
|
48
|
+
if (terminal(value) || type === "completed" || type === "complete") {
|
|
49
|
+
completed = true;
|
|
50
|
+
completionEvidence = [terminalEvidence(value, options.executionId)];
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (type !== "check" && type !== "assertion" && type !== "result") {
|
|
54
|
+
diagnostics.push(`unsupported-event:${type ?? "unknown"}`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const rawId = string(value.checkId ?? value.testId ?? value.name);
|
|
58
|
+
if (rawId === undefined) {
|
|
59
|
+
diagnostics.push("missing-check-id");
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
let checkId;
|
|
63
|
+
try {
|
|
64
|
+
checkId = stableCheckId([rawId]);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
diagnostics.push("missing-check-id");
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const checkStatus = status(value.status ?? value.outcome ?? value.state);
|
|
71
|
+
const failureClass = lower(value.failureClass ?? value.failureKind) ??
|
|
72
|
+
(checkStatus === "PASS" ? "none" : checkStatus === "FAIL" ? "infrastructure-error" : "unsupported-output");
|
|
73
|
+
const evidence = Array.isArray(value.evidence)
|
|
74
|
+
? value.evidence.filter((item) => typeof item === "string")
|
|
75
|
+
: [`rust:${checkId}`];
|
|
76
|
+
try {
|
|
77
|
+
results.push(result(options, "rust-harness", checkId, checkStatus, evidence, failureClass, attempts(value.attempts, null)));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
diagnostics.push(`malformed-result:${checkId}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return run(options, "rust-harness", results, completed, completionEvidence, diagnostics);
|
|
84
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ACCEPTANCE_FAILURE, makeAcceptanceCheck, makeAcceptanceRun } from "../acceptance-protocol.js";
|
|
2
|
+
export function object(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
4
|
+
? value
|
|
5
|
+
: undefined;
|
|
6
|
+
}
|
|
7
|
+
export function string(value) {
|
|
8
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9
|
+
}
|
|
10
|
+
export function number(value) {
|
|
11
|
+
return typeof value === "number" && Number.isSafeInteger(value) ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
export function array(value) {
|
|
14
|
+
return Array.isArray(value) ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
export function attempts(value, fallback = 1) {
|
|
17
|
+
const parsed = number(value);
|
|
18
|
+
return parsed === undefined ? fallback : parsed;
|
|
19
|
+
}
|
|
20
|
+
export function result(options, framework, checkId, status, evidence, failureClass = status === "PASS" ? "none" : ACCEPTANCE_FAILURE.infrastructure, attemptCount = 1) {
|
|
21
|
+
return makeAcceptanceCheck({
|
|
22
|
+
executionId: options.executionId,
|
|
23
|
+
framework,
|
|
24
|
+
frameworkVersion: options.frameworkVersion,
|
|
25
|
+
checkId,
|
|
26
|
+
phase: options.phase,
|
|
27
|
+
status,
|
|
28
|
+
failureClass,
|
|
29
|
+
attempts: attemptCount,
|
|
30
|
+
evidence
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function unknownResult(options, framework, checkId, failureClass, evidence = ["adapter:unknown"], attemptCount = 1) {
|
|
34
|
+
return result(options, framework, checkId, "UNKNOWN", evidence, failureClass, attemptCount);
|
|
35
|
+
}
|
|
36
|
+
export function run(options, framework, results, completed, completionEvidence, diagnostics = []) {
|
|
37
|
+
return makeAcceptanceRun({
|
|
38
|
+
executionId: options.executionId,
|
|
39
|
+
framework,
|
|
40
|
+
frameworkVersion: options.frameworkVersion,
|
|
41
|
+
phase: options.phase,
|
|
42
|
+
results,
|
|
43
|
+
completed,
|
|
44
|
+
completionEvidence,
|
|
45
|
+
diagnostics
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function lower(value) {
|
|
49
|
+
const text = string(value);
|
|
50
|
+
return text?.toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
export function hasRetry(value) {
|
|
53
|
+
if (value.retry === true || value.retried === true)
|
|
54
|
+
return true;
|
|
55
|
+
const retryCount = number(value.retryCount);
|
|
56
|
+
if (retryCount !== undefined && retryCount > 0)
|
|
57
|
+
return true;
|
|
58
|
+
const repeatCount = number(value.repeatCount);
|
|
59
|
+
if (repeatCount !== undefined && repeatCount > 0)
|
|
60
|
+
return true;
|
|
61
|
+
const rerun = number(value.rerun);
|
|
62
|
+
if (rerun !== undefined && rerun > 0)
|
|
63
|
+
return true;
|
|
64
|
+
const invocations = number(value.invocations);
|
|
65
|
+
if (invocations !== undefined && invocations > 1)
|
|
66
|
+
return true;
|
|
67
|
+
if (Array.isArray(value.retryReasons) && value.retryReasons.length > 0)
|
|
68
|
+
return true;
|
|
69
|
+
const attemptsValue = number(value.attempts);
|
|
70
|
+
return attemptsValue !== undefined && attemptsValue > 1;
|
|
71
|
+
}
|
|
72
|
+
export function terminal(value) {
|
|
73
|
+
if (value.completed === true || value.complete === true || value.terminal === true)
|
|
74
|
+
return true;
|
|
75
|
+
const type = lower(value.type ?? value.event);
|
|
76
|
+
return type === "complete" || type === "completed" || type === "finished" ||
|
|
77
|
+
type === "session-finished" || type === "run-complete";
|
|
78
|
+
}
|
|
79
|
+
export function terminalEvidence(value, fallback) {
|
|
80
|
+
const id = string(value.executionId) ?? string(value.id) ?? fallback;
|
|
81
|
+
return `terminal:${id}`;
|
|
82
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { makeAcceptanceCheck, makeAcceptanceRun, parseJsonDocument, parseJsonLines, stableCheckId } from "../acceptance-protocol.js";
|
|
2
|
+
import { collectJestAcceptance } from "./jest.js";
|
|
3
|
+
import { array, hasRetry, lower, number, object, result, run, string, terminalEvidence } from "./shared.js";
|
|
4
|
+
function mapFramework(runValue, options) {
|
|
5
|
+
const results = runValue.results.map((item) => makeAcceptanceCheck({
|
|
6
|
+
executionId: options.executionId,
|
|
7
|
+
framework: "vitest",
|
|
8
|
+
frameworkVersion: options.frameworkVersion,
|
|
9
|
+
checkId: item.checkId,
|
|
10
|
+
phase: options.phase,
|
|
11
|
+
status: item.status,
|
|
12
|
+
failureClass: item.failureClass,
|
|
13
|
+
attempts: item.attempts,
|
|
14
|
+
evidence: item.evidence.map((reference) => reference.replace(/^jest:/u, "vitest:"))
|
|
15
|
+
}));
|
|
16
|
+
return makeAcceptanceRun({
|
|
17
|
+
executionId: options.executionId,
|
|
18
|
+
framework: "vitest",
|
|
19
|
+
frameworkVersion: options.frameworkVersion,
|
|
20
|
+
phase: options.phase,
|
|
21
|
+
results,
|
|
22
|
+
completed: runValue.completed,
|
|
23
|
+
completionEvidence: runValue.completionEvidence.map((reference) => reference.replace(/^jest:/u, "vitest:")),
|
|
24
|
+
diagnostics: runValue.diagnostics
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function eventInput(input) {
|
|
28
|
+
if (Array.isArray(input))
|
|
29
|
+
return { events: input, diagnostics: [] };
|
|
30
|
+
if (typeof input !== "string")
|
|
31
|
+
return undefined;
|
|
32
|
+
const document = parseJsonDocument(input);
|
|
33
|
+
if (Array.isArray(document))
|
|
34
|
+
return { events: document, diagnostics: [] };
|
|
35
|
+
const lines = parseJsonLines(input);
|
|
36
|
+
const events = lines.values.length === 1 && Array.isArray(lines.values[0]) ? lines.values[0] : lines.values;
|
|
37
|
+
return events.length > 0 ? { events, diagnostics: lines.invalidLines.map(line => `invalid-json-line:${line}`) } : undefined;
|
|
38
|
+
}
|
|
39
|
+
function flattenTasks(tasks, file, ancestors, assertions, lifecycleErrors) {
|
|
40
|
+
for (const raw of tasks) {
|
|
41
|
+
const task = object(raw);
|
|
42
|
+
if (task === undefined) {
|
|
43
|
+
assertions.push(null);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const name = string(task.name ?? task.title);
|
|
47
|
+
const nextAncestors = name === undefined ? ancestors : [...ancestors, name];
|
|
48
|
+
const children = array(task.tasks ?? task.children);
|
|
49
|
+
const state = object(task.result) ?? object(task.state);
|
|
50
|
+
if (children !== undefined && children.length > 0) {
|
|
51
|
+
const errors = array(state?.errors);
|
|
52
|
+
const hooks = object(state?.hooks ?? task.hooks);
|
|
53
|
+
if (errors !== undefined && errors.length > 0)
|
|
54
|
+
lifecycleErrors.push(...errors);
|
|
55
|
+
if (task.hookErrors != null || state?.hookErrors != null ||
|
|
56
|
+
(hooks !== undefined && Object.values(hooks).some(value => value !== "pass" && value !== "passed"))) {
|
|
57
|
+
lifecycleErrors.push("suite-hook-error");
|
|
58
|
+
}
|
|
59
|
+
flattenTasks(children, file, nextAncestors, assertions, lifecycleErrors);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (task.type === "suite")
|
|
63
|
+
continue;
|
|
64
|
+
const status = state === undefined ? task.status ?? task.state : state.state ?? state.status;
|
|
65
|
+
const retryValue = state === undefined ? task.retry ?? task.retries : state.retry ?? state.retries;
|
|
66
|
+
const retryCount = number(state?.retryCount);
|
|
67
|
+
const repeatCount = number(state?.repeatCount);
|
|
68
|
+
// Native errors and hooks omit fixture cleanup/onTestFinished provenance.
|
|
69
|
+
// Even AssertionError with passing hooks is ambiguous without classified
|
|
70
|
+
// collector metadata, just as in Jest-compatible JSON.
|
|
71
|
+
const assertion = task.assertion === true || state?.assertion === true || state?.failureClass === "assertion-failed";
|
|
72
|
+
assertions.push({
|
|
73
|
+
name,
|
|
74
|
+
ancestorTitles: ancestors,
|
|
75
|
+
status: task.fails === true ? "xfail" : status,
|
|
76
|
+
attempts: number(task.attempts) ?? (retryCount !== undefined && repeatCount !== undefined ? retryCount + repeatCount + 1 : null),
|
|
77
|
+
retry: hasRetry(task) || (state !== undefined && hasRetry(state)) || (typeof retryValue === "number" && retryValue > 0),
|
|
78
|
+
assertion,
|
|
79
|
+
hooks: state?.hooks ?? task.hooks,
|
|
80
|
+
hookErrors: state?.hookErrors ?? task.hookErrors,
|
|
81
|
+
location: task.location
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function structuredReport(input) {
|
|
86
|
+
const value = typeof input === "string" ? parseJsonDocument(input) : input;
|
|
87
|
+
const report = object(value);
|
|
88
|
+
if (report === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
if (array(report.testResults) !== undefined) {
|
|
91
|
+
return report;
|
|
92
|
+
}
|
|
93
|
+
const testFiles = array(report.testFiles);
|
|
94
|
+
if (testFiles !== undefined && testFiles.some((file) => {
|
|
95
|
+
const value = object(file);
|
|
96
|
+
return value !== undefined && array(value.assertionResults) !== undefined;
|
|
97
|
+
})) {
|
|
98
|
+
return { ...report, testResults: testFiles };
|
|
99
|
+
}
|
|
100
|
+
const files = array(report.files ?? report.testFiles);
|
|
101
|
+
if (files === undefined)
|
|
102
|
+
return undefined;
|
|
103
|
+
const suites = [];
|
|
104
|
+
for (const rawFile of files) {
|
|
105
|
+
const file = object(rawFile);
|
|
106
|
+
if (file === undefined) {
|
|
107
|
+
suites.push(null);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const assertions = [];
|
|
111
|
+
const lifecycleErrors = [];
|
|
112
|
+
const tasks = array(file.tasks ?? file.children);
|
|
113
|
+
if (tasks !== undefined) {
|
|
114
|
+
flattenTasks(tasks, string(file.filepath ?? file.file ?? file.name) ?? "<vitest-file>", [], assertions, lifecycleErrors);
|
|
115
|
+
}
|
|
116
|
+
suites.push({
|
|
117
|
+
testFilePath: string(file.filepath ?? file.file ?? file.name) ?? "<vitest-file>",
|
|
118
|
+
assertionResults: assertions,
|
|
119
|
+
testExecError: file.error ?? file.hookErrors ?? object(file.result)?.errors,
|
|
120
|
+
runtimeError: lifecycleErrors
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return { ...report, testResults: suites };
|
|
124
|
+
}
|
|
125
|
+
function collectEvents(events, options, initialDiagnostics) {
|
|
126
|
+
const results = [];
|
|
127
|
+
const diagnostics = [...initialDiagnostics];
|
|
128
|
+
let completed = false;
|
|
129
|
+
let completionEvidence = [];
|
|
130
|
+
for (const raw of events) {
|
|
131
|
+
const value = object(raw);
|
|
132
|
+
if (value === undefined) {
|
|
133
|
+
diagnostics.push("malformed-event");
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const type = lower(value.type ?? value.event ?? value.kind);
|
|
137
|
+
if ((type === "complete" || type === "completed" || type === "finished") &&
|
|
138
|
+
value.completed !== false && value.interrupted !== true) {
|
|
139
|
+
completed = true;
|
|
140
|
+
completionEvidence = [terminalEvidence(value, options.executionId)];
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (type !== "test-end" && type !== "test:pass" && type !== "test:fail" &&
|
|
144
|
+
type !== "test:skip" && type !== "test:todo" && type !== "test-result") {
|
|
145
|
+
diagnostics.push(`unsupported-event:${type ?? "unknown"}`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const ancestors = array(value.ancestorTitles)?.filter((item) => typeof item === "string") ?? [];
|
|
149
|
+
const name = string(value.title ?? value.name) ??
|
|
150
|
+
(ancestors.length === 0 ? string(value.fullName) : undefined);
|
|
151
|
+
if (name === undefined) {
|
|
152
|
+
diagnostics.push("missing-check-id");
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const file = string(value.file ?? value.filepath) ?? "<vitest>";
|
|
156
|
+
const location = object(value.location);
|
|
157
|
+
const line = number(location?.line);
|
|
158
|
+
const column = number(location?.column);
|
|
159
|
+
let id;
|
|
160
|
+
try {
|
|
161
|
+
id = stableCheckId([
|
|
162
|
+
file,
|
|
163
|
+
...ancestors,
|
|
164
|
+
name,
|
|
165
|
+
...(line === undefined || column === undefined ? [] : [`@${line}:${column}`])
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
diagnostics.push("missing-check-id");
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const status = lower(value.status ?? value.state ?? (type === "test:pass" ? "passed" : type === "test:fail" ? "failed" : type === "test:skip" ? "skipped" : type === "test:todo" ? "todo" : undefined));
|
|
173
|
+
const refs = [`vitest:assertion:${id}`];
|
|
174
|
+
const observedAttempts = number(value.attempts);
|
|
175
|
+
const attemptCount = observedAttempts !== undefined && observedAttempts > 0 ? observedAttempts : null;
|
|
176
|
+
const hooks = object(value.hooks);
|
|
177
|
+
const hookFailure = value.hookErrors != null || (hooks !== undefined && Object.values(hooks).some(state => state !== "pass" && state !== "passed"));
|
|
178
|
+
if (hasRetry(value)) {
|
|
179
|
+
results.push(result(options, "vitest", id, "UNKNOWN", refs, "retry", attemptCount));
|
|
180
|
+
}
|
|
181
|
+
else if (hookFailure) {
|
|
182
|
+
results.push(result(options, "vitest", id, "UNKNOWN", refs, "hook-error", attemptCount));
|
|
183
|
+
}
|
|
184
|
+
else if (status === "passed" || status === "pass") {
|
|
185
|
+
results.push(result(options, "vitest", id, "PASS", refs, "none", attemptCount));
|
|
186
|
+
}
|
|
187
|
+
else if (status === "failed" || status === "fail") {
|
|
188
|
+
results.push(value.assertion === true || value.failureClass === "assertion-failed"
|
|
189
|
+
? result(options, "vitest", id, "FAIL", refs, "assertion-failed", attemptCount)
|
|
190
|
+
: result(options, "vitest", id, "UNKNOWN", refs, "infrastructure-error", attemptCount));
|
|
191
|
+
}
|
|
192
|
+
else if (status === "skipped" || status === "pending") {
|
|
193
|
+
results.push(result(options, "vitest", id, "UNKNOWN", refs, "skipped", attemptCount));
|
|
194
|
+
}
|
|
195
|
+
else if (status === "todo") {
|
|
196
|
+
results.push(result(options, "vitest", id, "UNKNOWN", refs, "todo", attemptCount));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
results.push(result(options, "vitest", id, "UNKNOWN", refs, "malformed-output", attemptCount));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return run(options, "vitest", results, completed, completionEvidence, diagnostics);
|
|
203
|
+
}
|
|
204
|
+
/** Collect Vitest reporter events or its Jest-compatible JSON output. */
|
|
205
|
+
export function collectVitestAcceptance(input, options) {
|
|
206
|
+
const events = eventInput(input);
|
|
207
|
+
if (events !== undefined && events.events.some((event) => {
|
|
208
|
+
const value = object(event);
|
|
209
|
+
const type = value === undefined ? undefined : lower(value.type ?? value.event ?? value.kind);
|
|
210
|
+
return type !== undefined && type.startsWith("test");
|
|
211
|
+
})) {
|
|
212
|
+
return collectEvents(events.events, options, events.diagnostics);
|
|
213
|
+
}
|
|
214
|
+
const report = structuredReport(input);
|
|
215
|
+
if (report === undefined)
|
|
216
|
+
return run(options, "vitest", [], false, [], ["malformed-output"]);
|
|
217
|
+
return mapFramework(collectJestAcceptance(report, options), options);
|
|
218
|
+
}
|
|
@@ -24,7 +24,9 @@ function validateBuiltEvidence(value) {
|
|
|
24
24
|
}
|
|
25
25
|
export function buildVerificationEvidence(input) {
|
|
26
26
|
return validateBuiltEvidence({
|
|
27
|
-
schemaVersion: EVIDENCE_SCHEMA_VERSION,
|
|
27
|
+
schemaVersion: input.taskContractHash === undefined && input.acceptance === undefined ? 3 : EVIDENCE_SCHEMA_VERSION,
|
|
28
|
+
...(input.taskContractHash === undefined ? {} : { taskContractHash: input.taskContractHash }),
|
|
29
|
+
...(input.acceptance === undefined ? {} : { acceptance: input.acceptance }),
|
|
28
30
|
taskId: input.taskId,
|
|
29
31
|
criterionId: input.criterionId,
|
|
30
32
|
commandId: input.command.id,
|
|
@@ -54,7 +56,7 @@ export function isPassingVerificationEvidence(command, evidence) {
|
|
|
54
56
|
return false;
|
|
55
57
|
}
|
|
56
58
|
return command.evidence.kind !== "test-count" ||
|
|
57
|
-
(evidence.schemaVersion
|
|
59
|
+
(evidence.schemaVersion >= 3 &&
|
|
58
60
|
evaluateTestCount(evidence.testCount, command.evidence.minimum).status === "PASS");
|
|
59
61
|
}
|
|
60
62
|
export class FileEvidenceStore {
|
|
@@ -64,6 +66,7 @@ export class FileEvidenceStore {
|
|
|
64
66
|
this.#root = root;
|
|
65
67
|
this.#anchorDirectory = anchorDirectory;
|
|
66
68
|
}
|
|
69
|
+
get root() { return this.#root; }
|
|
67
70
|
async save(value) {
|
|
68
71
|
const evidence = validateBuiltEvidence(value);
|
|
69
72
|
const content = `${JSON.stringify(evidence, null, 2)}\n`;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { taskContractHash } from "../task/contract.js";
|
|
2
|
+
import { replayAcceptance } from "./acceptance-replay.js";
|
|
1
3
|
import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
|
|
2
4
|
import { validateTaskAgainstConfig } from "../schema/validate.js";
|
|
3
5
|
import { collectChangeSurface } from "./change-surface.js";
|
|
@@ -23,7 +25,7 @@ function exitCategory(result) {
|
|
|
23
25
|
return result.exitCode === 0 ? "exit-zero" : "nonzero-exit";
|
|
24
26
|
}
|
|
25
27
|
function relevantCriteria(task, commandId) {
|
|
26
|
-
return task.criteria.filter((criterion) => criterion.verifierIds.includes(commandId));
|
|
28
|
+
return task.criteria.filter((criterion) => criterion.verifierIds.includes(commandId) || criterion.acceptance !== undefined);
|
|
27
29
|
}
|
|
28
30
|
function commandById(config, commandId) {
|
|
29
31
|
const command = config.verification.commands.find((candidate) => candidate.id === commandId);
|
|
@@ -59,6 +61,7 @@ export class VerificationService {
|
|
|
59
61
|
failureClass: result.failureClass,
|
|
60
62
|
sourceFingerprint,
|
|
61
63
|
toolVersions: this.#options.toolVersions ?? {},
|
|
64
|
+
...(task.goal === undefined && (task.contractRevision ?? 0) === 0 && task.criteria.every(c => c.acceptance === undefined) ? {} : { taskContractHash: taskContractHash(task) }),
|
|
62
65
|
config: this.#options.config
|
|
63
66
|
});
|
|
64
67
|
references.push(await this.#options.evidenceStore.save(evidence));
|
|
@@ -113,13 +116,14 @@ export class VerificationService {
|
|
|
113
116
|
throw verificationError("VERIFICATION_INPUT_INVALID", validation.errors[0]?.message ??
|
|
114
117
|
"Task and verification configuration are incompatible.");
|
|
115
118
|
}
|
|
116
|
-
const missingCriteria = validation.value.criteria.filter((criterion) => !this.#options.config.verification.commands.some(({ id, required }) => required && criterion.verifierIds.includes(id)));
|
|
119
|
+
const missingCriteria = validation.value.criteria.filter((criterion) => criterion.acceptance === undefined && !this.#options.config.verification.commands.some(({ id, required }) => required && criterion.verifierIds.includes(id)));
|
|
117
120
|
if (missingCriteria.length > 0) {
|
|
118
121
|
throw verificationError("VERIFICATION_UNKNOWN", `Criteria without required coverage: ${missingCriteria.map(({ id }) => id).join(", ")}. Configure at least one required verifier for each criterion and recreate the task; no verifier commands were run.`);
|
|
119
122
|
}
|
|
120
123
|
const reviewScope = await resolveReviewScope({
|
|
121
124
|
root: this.#options.root,
|
|
122
125
|
runner: this.#options.gitRunner,
|
|
126
|
+
...(this.#options.noChangePaths === undefined && stored.noChangePaths === undefined ? {} : { noChangePaths: this.#options.noChangePaths ?? stored.noChangePaths }),
|
|
123
127
|
...(this.#options.base === undefined ? {} : { base: this.#options.base })
|
|
124
128
|
});
|
|
125
129
|
const sourceFingerprint = await calculateSourceFingerprint(this.#options.root, reviewScope, this.#options.gitRunner);
|
|
@@ -133,29 +137,38 @@ export class VerificationService {
|
|
|
133
137
|
...mappedSelection,
|
|
134
138
|
verifierIds: [...new Set([...mappedSelection.verifierIds,
|
|
135
139
|
...this.#options.config.verification.commands
|
|
136
|
-
.filter(({ id, required }) => required && taskVerifierIds.has(id))
|
|
140
|
+
.filter(({ id, required }) => required && (validation.value.criteria.some(c => c.acceptance !== undefined) || taskVerifierIds.has(id)))
|
|
137
141
|
.map(({ id }) => id)])]
|
|
138
142
|
};
|
|
139
143
|
const results = [];
|
|
140
144
|
for (const commandId of selection.verifierIds) {
|
|
141
145
|
results.push(await this.#runCommand(validation.value, commandById(this.#options.config, commandId)));
|
|
142
146
|
}
|
|
143
|
-
|
|
147
|
+
const acceptance = validation.value.criteria.some(c => c.acceptance !== undefined && c.acceptance.mode !== "review-only")
|
|
148
|
+
? await replayAcceptance({ ...this.#options, task: validation.value, sourceFingerprint }) : [];
|
|
149
|
+
let status = aggregateVerificationStatus([...results, ...acceptance.map(a => ({ required: true, status: a.status }))]);
|
|
144
150
|
let sourceChanged = false;
|
|
145
151
|
const postflightScope = await resolveReviewScope({
|
|
146
152
|
root: this.#options.root,
|
|
147
153
|
runner: this.#options.gitRunner,
|
|
154
|
+
...(this.#options.noChangePaths === undefined && stored.noChangePaths === undefined ? {} : { noChangePaths: this.#options.noChangePaths ?? stored.noChangePaths }),
|
|
148
155
|
...(this.#options.base === undefined ? {} : { base: this.#options.base })
|
|
149
156
|
});
|
|
150
157
|
const postflightFingerprint = await calculateSourceFingerprint(this.#options.root, postflightScope, this.#options.gitRunner);
|
|
151
158
|
if (reviewScopeSignature(reviewScope) !== reviewScopeSignature(postflightScope) ||
|
|
152
|
-
sourceFingerprint !== postflightFingerprint
|
|
159
|
+
sourceFingerprint !== postflightFingerprint ||
|
|
160
|
+
taskContractHash((await this.#options.taskService.status({ taskId })).task) !== taskContractHash(stored.task)) {
|
|
153
161
|
status = "UNKNOWN";
|
|
154
162
|
sourceChanged = true;
|
|
155
163
|
}
|
|
164
|
+
const acceptanceEvidence = {};
|
|
165
|
+
for (const proof of acceptance)
|
|
166
|
+
acceptanceEvidence[proof.criterionId] = [...(acceptanceEvidence[proof.criterionId] ?? []), proof.reference];
|
|
167
|
+
if (status !== "PASS" && stored.status === "active" && acceptance.length > 0)
|
|
168
|
+
await this.#options.taskService.recordEvidence(taskId, acceptanceEvidence);
|
|
156
169
|
let signal = null;
|
|
157
170
|
if (status === "PASS") {
|
|
158
|
-
const taskEvidence = {};
|
|
171
|
+
const taskEvidence = { ...acceptanceEvidence };
|
|
159
172
|
for (const [index, result] of results.entries()) {
|
|
160
173
|
const command = commandById(this.#options.config, result.commandId);
|
|
161
174
|
const references = await this.#persistEvidence(validation.value, command, result.startedAt, result.finishedAt, result, sourceFingerprint);
|
|
@@ -175,7 +188,9 @@ export class VerificationService {
|
|
|
175
188
|
}
|
|
176
189
|
}
|
|
177
190
|
else {
|
|
178
|
-
const required = results.filter((result) => result.required)
|
|
191
|
+
const required = [...results.filter((result) => result.required), ...acceptance.map(a => ({ commandId: a.runnerId, required: true,
|
|
192
|
+
status: a.status, failureClass: a.failureClass, exitCode: null, timedOut: false, testCount: null,
|
|
193
|
+
diagnostic: a.criterionId + ":" + a.phase + ":" + a.failureClass, evidenceReferences: [a.reference], startedAt: "", finishedAt: "" }))];
|
|
179
194
|
const gating = required;
|
|
180
195
|
const failed = gating.find((result) => result.status !== "PASS");
|
|
181
196
|
if (failed === undefined) {
|
|
@@ -189,7 +204,7 @@ export class VerificationService {
|
|
|
189
204
|
diagnostics: "source changed during verification"
|
|
190
205
|
}));
|
|
191
206
|
signal = advanced.signal;
|
|
192
|
-
return { taskId, status, surface, selection, results, signal, reviewScope, sourceFingerprint };
|
|
207
|
+
return { taskId, status, surface, selection, results, signal, reviewScope, sourceFingerprint, acceptance };
|
|
193
208
|
}
|
|
194
209
|
const advanced = await this.#options.taskService.recordFailure(taskId, createFailureFingerprint({
|
|
195
210
|
commandId: failed.commandId,
|
|
@@ -200,6 +215,7 @@ export class VerificationService {
|
|
|
200
215
|
signal = advanced.signal;
|
|
201
216
|
}
|
|
202
217
|
return {
|
|
218
|
+
acceptance,
|
|
203
219
|
taskId,
|
|
204
220
|
status,
|
|
205
221
|
surface,
|
|
@@ -8,7 +8,7 @@ export async function calculateSourceFingerprint(root, scope, runner) {
|
|
|
8
8
|
if (scope.mode === "base") {
|
|
9
9
|
return sha256(JSON.stringify({
|
|
10
10
|
domain: "agent-ops-source-v1",
|
|
11
|
-
mode: "base",
|
|
11
|
+
mode: scope.noChange === true ? "goal-no-change" : "base",
|
|
12
12
|
head,
|
|
13
13
|
base: scope.resolvedBase,
|
|
14
14
|
paths: [...scope.changedFiles]
|
|
@@ -146,16 +146,20 @@ async function defaultTerminateWindowsTree(processId, graceMs) {
|
|
|
146
146
|
}
|
|
147
147
|
export class NodeVerificationProcessRunner {
|
|
148
148
|
#platform;
|
|
149
|
+
#processGroupId;
|
|
149
150
|
#terminateWindowsTree;
|
|
150
151
|
constructor(options = {}) {
|
|
151
152
|
this.#platform = options.platform ?? process.platform;
|
|
153
|
+
this.#processGroupId = options.processGroupId;
|
|
154
|
+
if (this.#processGroupId !== undefined && (!Number.isSafeInteger(this.#processGroupId) || this.#processGroupId < 1 || this.#platform === "win32"))
|
|
155
|
+
throw new Error("Registered proof group must be a positive POSIX process group ID.");
|
|
152
156
|
this.#terminateWindowsTree =
|
|
153
157
|
options.terminateWindowsTree ?? defaultTerminateWindowsTree;
|
|
154
158
|
}
|
|
155
159
|
start(request) {
|
|
156
160
|
const child = spawn(request.command, [...request.args], {
|
|
157
161
|
cwd: request.cwd,
|
|
158
|
-
detached: this.#platform !== "win32",
|
|
162
|
+
detached: this.#platform !== "win32" && this.#processGroupId === undefined,
|
|
159
163
|
env: {
|
|
160
164
|
...(request.replaceEnv === true ? {} : process.env),
|
|
161
165
|
...(request.env ?? {})
|
|
@@ -204,7 +208,7 @@ export class NodeVerificationProcessRunner {
|
|
|
204
208
|
await this.#terminateWindowsTree(processId, graceMs);
|
|
205
209
|
return;
|
|
206
210
|
}
|
|
207
|
-
await terminatePosixTree(processId, completion, graceMs);
|
|
211
|
+
await terminatePosixTree(this.#processGroupId ?? processId, completion, graceMs);
|
|
208
212
|
}
|
|
209
213
|
};
|
|
210
214
|
}
|
|
@@ -375,3 +379,10 @@ export async function runVerificationCommand(command, options) {
|
|
|
375
379
|
stderrTruncated: capturedStderr.truncated
|
|
376
380
|
};
|
|
377
381
|
}
|
|
382
|
+
/** CLI-only opt-in. The dormant entry sets this after the supervisor records its PID. */
|
|
383
|
+
export function registeredRunProofRunner() {
|
|
384
|
+
const value = process.env.AGENT_OPS_RUN_PROOF_PID;
|
|
385
|
+
if (value !== undefined && !/^[1-9][0-9]{0,9}$/u.test(value))
|
|
386
|
+
throw new Error("Invalid registered run proof PID.");
|
|
387
|
+
return new NodeVerificationProcessRunner(value === undefined ? {} : { processGroupId: Number(value) });
|
|
388
|
+
}
|
|
@@ -265,6 +265,16 @@ evidence. Give each editing conversation its own worktree instead:
|
|
|
265
265
|
Creating a subtask keeps the session attached to the top of its tree, and
|
|
266
266
|
each task keeps its own review record, so a parent and a subtask reviewed
|
|
267
267
|
on the same source do not overwrite each other.
|
|
268
|
+
- When writing subagents use separate worktrees, create each child task with
|
|
269
|
+
`--intent` before editing, then commit and verify locally. From the main
|
|
270
|
+
checkout run `agent-ops task advance --task <parent-id> --session <id> --yes`
|
|
271
|
+
through the trusted outer host runner. It integrates all child commits and
|
|
272
|
+
intents into the coordinator worktree, reruns each task's required verifier
|
|
273
|
+
on the same candidate, and performs one two-round tree review. Only an
|
|
274
|
+
explicit `NOT_RUN / scope-too-large` falls back to per-task two-round review.
|
|
275
|
+
After PASS it saves a complete receipt under `.git/agent-ops/receipts/` and
|
|
276
|
+
calls finish; a moved target may trigger one repeat of the final gate.
|
|
277
|
+
A direct finish of one child while siblings exist is refused.
|
|
268
278
|
- `agent-ops worktree finish <name>` merges by fast-forward only, one finish
|
|
269
279
|
at a time. When the main checkout is on the target branch the merge updates
|
|
270
280
|
its files, so uncommitted changes there block it. When the main checkout is
|
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Acceptance and Evidence
|
|
2
2
|
|
|
3
|
+
## EVIDENCE-CONTRACT-001
|
|
4
|
+
|
|
5
|
+
Executable acceptance MUST bind the current contract to immutable replay evidence.
|
|
6
|
+
|
|
7
|
+
- Trigger: Verifying or completing a typed acceptance contract.
|
|
8
|
+
- Action: Replay trusted checks against the immutable baseline and current candidate, then obtain fresh review.
|
|
9
|
+
- Evidence: Digest-bound phase results and current contract coverage enter both reviewer packets.
|
|
10
|
+
- Positive: Baseline assertion FAIL and candidate PASS prove a behavioral check.
|
|
11
|
+
- Negative: A fixture error is counted as red, or an old review proves revised criteria.
|
|
12
|
+
|
|
13
|
+
- `behavioral`: every designated red check fails by assertion on the baseline;
|
|
14
|
+
every required candidate check passes. `invariant`: required candidate checks
|
|
15
|
+
pass; baseline PASS is valid. `review-only`: two fresh reviewers discharge the
|
|
16
|
+
requirement against the original goal; no mechanical proof is claimed.
|
|
17
|
+
- Trusted repository `verification.acceptanceRunners` define argv, setup, product
|
|
18
|
+
build and test build. Tasks provide stable check IDs and explicit committed
|
|
19
|
+
test/fixture/helper materials. Each checkout installs and builds its own version
|
|
20
|
+
before candidate materials are overlaid. Tasks cannot override runner commands.
|
|
21
|
+
- Fixture, collection, build, timeout, cancellation, truncated output, missing or
|
|
22
|
+
duplicate IDs, retries and skips MUST NOT establish red or green. Complete raw
|
|
23
|
+
execution artifacts are digest-bound to the contract, materials and commits.
|
|
24
|
+
- Native Jest JSON and Vitest task errors lack complete test-call versus
|
|
25
|
+
hook/fixture-cleanup provenance. Failed rows MUST remain UNKNOWN unless a
|
|
26
|
+
reviewed repository collector supplies explicit assertion classification;
|
|
27
|
+
AssertionError names, matcher details and passing hook states are insufficient.
|
|
28
|
+
Green requires explicit single-attempt evidence: attempts, Jest invocations,
|
|
29
|
+
or both Vitest retryCount and repeatCount. Native Vitest JSON omits these
|
|
30
|
+
counters and cannot supply mechanical proof by itself. Pytest requires one
|
|
31
|
+
ordered setup/call/teardown lifecycle per nodeid; the Rust protocol requires
|
|
32
|
+
explicit attempts. Missing metadata MUST NOT default to a successful attempt.
|
|
33
|
+
- Baselines are immutable creation commits, or failed candidates for finding pins.
|
|
34
|
+
Revise/replan preserve the original goal, every mapped requirement and previous
|
|
35
|
+
contracts/evidence. New contracts require new proof and both fresh reviews.
|
|
36
|
+
- `task pin-finding` validates saved report digest/index provenance. Recurrence
|
|
37
|
+
explicitly names an existing pin. A sixth criterion returns a replan obligation
|
|
38
|
+
without changing state; the mapped replan must retain the pin and all requirements.
|
|
39
|
+
- Reviewer packets include original goal, revision history, coverage and verifier
|
|
40
|
+
materials. Review checks the assertions themselves and explicit fallback reasons.
|
|
41
|
+
Mechanical green never removes mandatory policy commands or either fresh review.
|
|
42
|
+
|
|
43
|
+
Task state writes version 2 and reads version 1. Evidence version 4 reads earlier
|
|
44
|
+
evidence; attestation version 3 binds contracts and coverage. Legacy criteria keep
|
|
45
|
+
the verifier rules below; typed or revised contracts cannot reuse an old review.
|
|
46
|
+
Coverage separates mechanical proof, review-only and legacy; a runner binding alone
|
|
47
|
+
is not proof. Explicit no-change proof names existing committed regular source files
|
|
48
|
+
at the frozen candidate; integration re-resolves them. Ordinary empty-diff review
|
|
49
|
+
and empty-branch direct finish remain rejected.
|
|
50
|
+
|
|
3
51
|
## EVIDENCE-CRITERION-001
|
|
4
52
|
|
|
5
53
|
Every acceptance criterion MUST map to observable evidence.
|
|
@@ -10,6 +58,16 @@ Every acceptance criterion MUST map to observable evidence.
|
|
|
10
58
|
- Positive: `criterion tests → npm test (354 passing tests)`.
|
|
11
59
|
- Negative: `The implementation looks correct, so no evidence is needed.`
|
|
12
60
|
|
|
61
|
+
## EVIDENCE-INTENT-001
|
|
62
|
+
|
|
63
|
+
Each child worktree MUST record its modification intent before editing.
|
|
64
|
+
|
|
65
|
+
- Trigger: A subagent will edit in its own worktree.
|
|
66
|
+
- Action: Use `task create --intent <text>` to record the user-approved behavior, constraints and integration points. The coordinator imports the intent with the child task and re-runs verification on the integrated candidate.
|
|
67
|
+
- Evidence: The child task and final receipt contain the intent, with final verifier evidence bound to the integrated candidate.
|
|
68
|
+
- Positive: `A child records its planned API change, commits it, and the final receipt includes that intent and integrated verification.`
|
|
69
|
+
- Negative: `A child edits first and supplies a retrospective one-line intent at delivery.`
|
|
70
|
+
|
|
13
71
|
## EVIDENCE-AGGREGATE-001
|
|
14
72
|
|
|
15
73
|
The verifier MUST return FAIL when a criterion is missing, duplicated, unknown, or has empty evidence.
|