@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.
Files changed (103) hide show
  1. package/README.md +64 -0
  2. package/dist/packages/cli/src/args.js +214 -10
  3. package/dist/packages/cli/src/bin.js +77 -17
  4. package/dist/packages/cli/src/cli.js +42 -10
  5. package/dist/packages/cli/src/commands/advance.js +173 -0
  6. package/dist/packages/cli/src/commands/review-show.js +189 -0
  7. package/dist/packages/cli/src/commands/review.js +201 -34
  8. package/dist/packages/cli/src/commands/run.js +111 -0
  9. package/dist/packages/cli/src/commands/task.js +62 -17
  10. package/dist/packages/cli/src/commands/verify.js +1 -0
  11. package/dist/packages/cli/src/context.js +63 -1
  12. package/dist/packages/cli/src/hook-process.js +31 -1
  13. package/dist/packages/cli/src/owned-run-step.js +65 -0
  14. package/dist/packages/cli/src/parallel-deps.js +18 -5
  15. package/dist/packages/cli/src/run-deps.js +314 -0
  16. package/dist/packages/cli/src/run-entry.js +943 -0
  17. package/dist/packages/cli/src/run-setup-entry.js +32 -0
  18. package/dist/packages/cli/src/run-step-entry.js +58 -0
  19. package/dist/runtime/src/config/explain.js +6 -0
  20. package/dist/runtime/src/config/hash.js +1 -1
  21. package/dist/runtime/src/config/merge.js +9 -1
  22. package/dist/runtime/src/contracts.js +2 -2
  23. package/dist/runtime/src/hooks/completion-gate.js +8 -2
  24. package/dist/runtime/src/install/doctor.js +2 -0
  25. package/dist/runtime/src/install/harness.js +1 -1
  26. package/dist/runtime/src/install/preauth.js +10 -1
  27. package/dist/runtime/src/parallel/finish.js +217 -39
  28. package/dist/runtime/src/parallel/guard.js +5 -1
  29. package/dist/runtime/src/parallel/integrate.js +416 -0
  30. package/dist/runtime/src/parallel/manage.js +4 -1
  31. package/dist/runtime/src/parallel/receipt.js +153 -0
  32. package/dist/runtime/src/parallel/service.js +45 -3
  33. package/dist/runtime/src/review/attestation.js +60 -11
  34. package/dist/runtime/src/review/execute.js +24 -1
  35. package/dist/runtime/src/review/packet.js +35 -1
  36. package/dist/runtime/src/review/render.js +71 -0
  37. package/dist/runtime/src/review/runner.js +12 -1
  38. package/dist/runtime/src/review/scope.js +18 -1
  39. package/dist/runtime/src/review/slots.js +35 -10
  40. package/dist/runtime/src/run/authorization.js +60 -0
  41. package/dist/runtime/src/run/budget.js +30 -0
  42. package/dist/runtime/src/run/controls.js +139 -0
  43. package/dist/runtime/src/run/convergence.js +177 -0
  44. package/dist/runtime/src/run/hosts/claude.js +180 -0
  45. package/dist/runtime/src/run/hosts/codex.js +279 -0
  46. package/dist/runtime/src/run/hosts/index.js +4 -0
  47. package/dist/runtime/src/run/hosts/protocol.js +254 -0
  48. package/dist/runtime/src/run/hosts/types.js +1 -0
  49. package/dist/runtime/src/run/hosts/util.js +136 -0
  50. package/dist/runtime/src/run/integration.js +204 -0
  51. package/dist/runtime/src/run/macOS.js +189 -0
  52. package/dist/runtime/src/run/ownership.js +22 -0
  53. package/dist/runtime/src/run/planning.js +25 -0
  54. package/dist/runtime/src/run/policy-transition.js +1076 -0
  55. package/dist/runtime/src/run/policy.js +176 -0
  56. package/dist/runtime/src/run/ratchet.js +71 -0
  57. package/dist/runtime/src/run/scheduler.js +260 -0
  58. package/dist/runtime/src/run/service.js +425 -0
  59. package/dist/runtime/src/run/supervisor.js +635 -0
  60. package/dist/runtime/src/run/transport.js +159 -0
  61. package/dist/runtime/src/run/types.js +1 -0
  62. package/dist/runtime/src/run/usage.js +81 -0
  63. package/dist/runtime/src/run/verification.js +64 -0
  64. package/dist/runtime/src/run/worker-gate.js +48 -0
  65. package/dist/runtime/src/schema/acceptance.js +91 -0
  66. package/dist/runtime/src/schema/validate.js +72 -7
  67. package/dist/runtime/src/task/completion.js +26 -6
  68. package/dist/runtime/src/task/contract.js +17 -0
  69. package/dist/runtime/src/task/pin-finding.js +81 -0
  70. package/dist/runtime/src/task/render.js +3 -0
  71. package/dist/runtime/src/task/service.js +361 -10
  72. package/dist/runtime/src/task/store.js +57 -7
  73. package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
  74. package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
  75. package/dist/runtime/src/verify/acceptance-replay.js +239 -0
  76. package/dist/runtime/src/verify/adapters/index.js +5 -0
  77. package/dist/runtime/src/verify/adapters/jest.js +142 -0
  78. package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
  79. package/dist/runtime/src/verify/adapters/node.js +254 -0
  80. package/dist/runtime/src/verify/adapters/pytest.js +188 -0
  81. package/dist/runtime/src/verify/adapters/rust.js +84 -0
  82. package/dist/runtime/src/verify/adapters/shared.js +82 -0
  83. package/dist/runtime/src/verify/adapters/vitest.js +218 -0
  84. package/dist/runtime/src/verify/evidence.js +5 -2
  85. package/dist/runtime/src/verify/service.js +24 -8
  86. package/dist/runtime/src/verify/source-fingerprint.js +1 -1
  87. package/dist/runtime/src/verify/spawn.js +13 -2
  88. package/docs/en/guides/configuration.md +10 -0
  89. package/docs/en/spec/acceptance-and-evidence.md +58 -0
  90. package/docs/en/spec/delegation.md +4 -4
  91. package/docs/en/spec/loop-engineering.md +49 -0
  92. package/docs/en/spec/review.md +24 -2
  93. package/docs/zh-TW/guides/configuration.md +9 -0
  94. package/docs/zh-TW/spec/acceptance-and-evidence.md +31 -1
  95. package/docs/zh-TW/spec/delegation.md +4 -4
  96. package/docs/zh-TW/spec/loop-engineering.md +22 -0
  97. package/docs/zh-TW/spec/review.md +24 -2
  98. package/package.json +1 -1
  99. package/schemas/config.schema.json +221 -19
  100. package/schemas/evidence.schema.json +129 -6
  101. package/schemas/task.schema.json +268 -6
  102. package/templates/acceptance/agent_ops_acceptance.rs +36 -0
  103. 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 === EVIDENCE_SCHEMA_VERSION &&
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
- let status = aggregateVerificationStatus(results);
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.