@kylecheng3146/agent-ops 0.5.3 → 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 (93) hide show
  1. package/README.md +48 -0
  2. package/dist/packages/cli/src/args.js +165 -2
  3. package/dist/packages/cli/src/bin.js +64 -17
  4. package/dist/packages/cli/src/cli.js +33 -6
  5. package/dist/packages/cli/src/commands/advance.js +23 -3
  6. package/dist/packages/cli/src/commands/review-show.js +1 -1
  7. package/dist/packages/cli/src/commands/review.js +100 -9
  8. package/dist/packages/cli/src/commands/run.js +111 -0
  9. package/dist/packages/cli/src/commands/task.js +61 -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/parallel/finish.js +85 -22
  26. package/dist/runtime/src/parallel/guard.js +5 -1
  27. package/dist/runtime/src/parallel/integrate.js +288 -10
  28. package/dist/runtime/src/parallel/manage.js +4 -1
  29. package/dist/runtime/src/parallel/receipt.js +71 -5
  30. package/dist/runtime/src/parallel/service.js +41 -1
  31. package/dist/runtime/src/review/attestation.js +36 -11
  32. package/dist/runtime/src/review/execute.js +24 -1
  33. package/dist/runtime/src/review/packet.js +35 -1
  34. package/dist/runtime/src/review/runner.js +12 -1
  35. package/dist/runtime/src/review/scope.js +18 -1
  36. package/dist/runtime/src/review/slots.js +35 -10
  37. package/dist/runtime/src/run/authorization.js +60 -0
  38. package/dist/runtime/src/run/budget.js +30 -0
  39. package/dist/runtime/src/run/controls.js +139 -0
  40. package/dist/runtime/src/run/convergence.js +177 -0
  41. package/dist/runtime/src/run/hosts/claude.js +180 -0
  42. package/dist/runtime/src/run/hosts/codex.js +279 -0
  43. package/dist/runtime/src/run/hosts/index.js +4 -0
  44. package/dist/runtime/src/run/hosts/protocol.js +254 -0
  45. package/dist/runtime/src/run/hosts/types.js +1 -0
  46. package/dist/runtime/src/run/hosts/util.js +136 -0
  47. package/dist/runtime/src/run/integration.js +204 -0
  48. package/dist/runtime/src/run/macOS.js +189 -0
  49. package/dist/runtime/src/run/ownership.js +22 -0
  50. package/dist/runtime/src/run/planning.js +25 -0
  51. package/dist/runtime/src/run/policy-transition.js +1076 -0
  52. package/dist/runtime/src/run/policy.js +176 -0
  53. package/dist/runtime/src/run/ratchet.js +71 -0
  54. package/dist/runtime/src/run/scheduler.js +260 -0
  55. package/dist/runtime/src/run/service.js +425 -0
  56. package/dist/runtime/src/run/supervisor.js +635 -0
  57. package/dist/runtime/src/run/transport.js +159 -0
  58. package/dist/runtime/src/run/types.js +1 -0
  59. package/dist/runtime/src/run/usage.js +81 -0
  60. package/dist/runtime/src/run/verification.js +64 -0
  61. package/dist/runtime/src/run/worker-gate.js +48 -0
  62. package/dist/runtime/src/schema/acceptance.js +91 -0
  63. package/dist/runtime/src/schema/validate.js +67 -7
  64. package/dist/runtime/src/task/completion.js +14 -2
  65. package/dist/runtime/src/task/contract.js +17 -0
  66. package/dist/runtime/src/task/pin-finding.js +81 -0
  67. package/dist/runtime/src/task/service.js +316 -13
  68. package/dist/runtime/src/task/store.js +57 -7
  69. package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
  70. package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
  71. package/dist/runtime/src/verify/acceptance-replay.js +239 -0
  72. package/dist/runtime/src/verify/adapters/index.js +5 -0
  73. package/dist/runtime/src/verify/adapters/jest.js +142 -0
  74. package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
  75. package/dist/runtime/src/verify/adapters/node.js +254 -0
  76. package/dist/runtime/src/verify/adapters/pytest.js +188 -0
  77. package/dist/runtime/src/verify/adapters/rust.js +84 -0
  78. package/dist/runtime/src/verify/adapters/shared.js +82 -0
  79. package/dist/runtime/src/verify/adapters/vitest.js +218 -0
  80. package/dist/runtime/src/verify/evidence.js +5 -2
  81. package/dist/runtime/src/verify/service.js +24 -8
  82. package/dist/runtime/src/verify/source-fingerprint.js +1 -1
  83. package/dist/runtime/src/verify/spawn.js +13 -2
  84. package/docs/en/spec/acceptance-and-evidence.md +48 -0
  85. package/docs/en/spec/loop-engineering.md +49 -0
  86. package/docs/zh-TW/spec/acceptance-and-evidence.md +21 -1
  87. package/docs/zh-TW/spec/loop-engineering.md +22 -0
  88. package/package.json +1 -1
  89. package/schemas/config.schema.json +221 -19
  90. package/schemas/evidence.schema.json +129 -6
  91. package/schemas/task.schema.json +265 -6
  92. package/templates/acceptance/agent_ops_acceptance.rs +36 -0
  93. package/templates/acceptance/agent_ops_pytest.py +46 -0
@@ -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
+ }
@@ -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.
@@ -1,5 +1,54 @@
1
1
  # Loop Engineering
2
2
 
3
+ ## LOOP-RUN-001
4
+
5
+ `run` MUST preserve the original user goal and require current agent-ops proof.
6
+
7
+ - Trigger: Starting, repairing or recovering a native goal run.
8
+ - Action: Bind writers to current leases and require final verification, dual review and receipt proof.
9
+ - Evidence: The run ledger and sealed receipt bind the current candidate and original goal.
10
+ - Positive: Recovering interrupted cleanup preserves the target and saved final proof.
11
+ - Negative: Native goal completion alone changes run state to complete.
12
+
13
+ - The macOS supervisor registers Claude/Codex native identities before activating
14
+ goals. Same-host writers use isolated worktrees and current generation leases;
15
+ the coordinator counts toward the maximum two writers. Dependencies become
16
+ available only through frozen deliveries; child baselines follow those commits.
17
+ - The default 60-minute budget counts the union of active intervals, including
18
+ setup, proof, review and integration. Resume keeps elapsed time and usage epochs.
19
+ Missing counters remain UNKNOWN; partial usage is not a complete cost estimate.
20
+ - Questions pause work and require an explicit answer. Repair preserves the goal
21
+ and records failures; two identical rounds without progress stop that worker
22
+ and its dependents. Independent tasks remain eligible. Review findings require
23
+ regression pins or recorded review-only fallback before another final gate.
24
+ - Native completion is observational. Finalization requires mandatory verification,
25
+ both fresh reviews, completed task state and a receipt bound to the target.
26
+ Integration seals the candidate before moving the target and records task,
27
+ receipt, note and cleanup progress. Recovery validates that seal and does not
28
+ repeat a successful target mutation. Inconsistent targets remain blocked.
29
+ - Stop persists disabled continuation before confirming registered process-group
30
+ death. Crash recovery cannot replace a live or unidentified writer. Dirty
31
+ checkouts, version drift and restart storms preserve the scene for diagnosis.
32
+ Reboot/login changes require explicit resume; no global host policy is changed.
33
+ Startup process identities are saved before the native handshake; a stopped,
34
+ unopened Codex session may initialize afresh only after process death is proved.
35
+
36
+ - Setup and native-issued local proof commands MUST register dormant process
37
+ groups before execution; Stop also reconciles their descendants. Interrupted
38
+ setup requires explicit resume and retains its checkout and failure artifact.
39
+ - A coordinator may assess additional fixed non-dangerous capabilities only under
40
+ a run-scoped policy bound to the original repository trust, executable runtime
41
+ and remaining budget. Explicit native or organization denial cannot be bypassed.
42
+ Policy changes fence all writers, preserve frozen deliveries and journal contract
43
+ synchronization before new generations acquire leases. This never grants global
44
+ repository trust. Native decisions retain redacted command scope and rationale;
45
+ absent provider evidence remains UNKNOWN.
46
+ Explicit resume renews expired authorization from the remaining budget and
47
+ reconciles immutable renewal lineage before policy synchronization. An answer
48
+ uses the same recovery checks; neither action can revive an explicit denial.
49
+ - Convergence compares actual per-check statuses and outstanding finding pins.
50
+ Source fingerprints, timing and diagnostics alone cannot establish progress.
51
+
3
52
  This module defines the bounded loop used to plan, implement, verify, and hand off work.
4
53
 
5
54
  ## LOOP-START-001
@@ -1,6 +1,26 @@
1
1
  # 驗收與證據
2
2
 
3
- English source version: 2026-09-27. Revalidate: when the English specification changes.
3
+ ## EVIDENCE-CONTRACT-001
4
+
5
+ 可執行驗收 MUST 將目前契約綁定到不可變的 replay 證據。
6
+
7
+ - Trigger: 驗證或完成 typed 驗收契約。
8
+ - Action: 在固定 baseline 與目前 candidate replay 授權 checks,再取得 fresh review。
9
+ - Evidence: 綁定摘要的 phase results 與目前 coverage 進入兩輪 reviewer packet。
10
+ - Positive: baseline 斷言 FAIL、candidate PASS 證明 behavioral check。
11
+ - Negative: 把 fixture error 算 red,或用舊 review 證明修訂後 criteria。
12
+
13
+ - `behavioral`:指定 red checks 在 baseline 都因斷言失敗,candidate 的必要 checks 全部通過。`invariant`:candidate 的必要 checks 通過,baseline 原本 PASS 有效。`review-only`:兩輪 fresh review 對照原始目標驗收,不宣稱機械證明。
14
+ - repo 授權的 `verification.acceptanceRunners` 定義 argv、setup、產品與 test build。task 提供穩定 check IDs 與明列且已 commit 的 test/fixture/helper;兩側各自安裝並 build 其版本,再覆蓋 candidate 材料。task 不能改 runner 命令。
15
+ - fixture、collection、build、timeout、取消、輸出截斷、缺少或重複 ID、retry、skip MUST NOT 算 red 或 green。完整 execution artifacts 以摘要綁定契約、材料與 commits。
16
+ - 原生 Jest JSON 與 Vitest task errors 無法完整區分 test-call 斷言與 hook/fixture cleanup。除非已 review 的 repo collector 提供明確斷言分類,失敗列 MUST 保持 UNKNOWN;AssertionError 名稱、matcher 資料與 pass 的 hook 狀態都不足。green 需要明確的單次 attempt 證據:attempts、Jest invocations,或 Vitest retryCount 與 repeatCount 兩者。原生 Vitest JSON 省略這些 counters,不能單獨提供機械證明。pytest 每個 nodeid 必須有一組依序的 setup/call/teardown;Rust protocol 必須提供 attempts。缺 metadata MUST NOT 預設為一次成功。
17
+ - baseline 固定為建立 commit;finding pin 使用被 FAIL 的 candidate。revise/replan 保留原始目標、全部映射要求與先前契約/證據。新契約必須重新取證並取得兩輪 fresh review。
18
+ - `task pin-finding` 驗證已保存 report 的 digest/index 來源。recurrence 明確引用既有 pin。第六條回傳 replan 義務且不修改狀態;映射後必須保留 pin 與全部要求。
19
+ - reviewer packet 包含原始目標、修訂歷史、coverage 與 verifier 材料,審查斷言本身與明確的 fallback 理由。機械 green 不會省略必要 policy commands 或任一 fresh review。
20
+
21
+ task state 寫入 version 2、讀取 version 1;evidence version 4 讀取舊證據,attestation version 3 綁定契約與 coverage。legacy criteria 維持下列 verifier 規則;typed/revised 契約不能沿用舊 review。coverage 分列機械證明、review-only、legacy;runner 綁定本身不算證明。明確 no-change proof 列出固定 candidate 上已 commit 的 regular source files;整合時重新解析支持路徑。一般 empty-diff review 與 empty-branch direct finish 仍拒絕。
22
+
23
+ English source version: 2026-10-06. Revalidate: when the English specification changes.
4
24
 
5
25
  ## EVIDENCE-CRITERION-001
6
26
 
@@ -1,5 +1,27 @@
1
1
  # 迴圈工程
2
2
 
3
+ ## LOOP-RUN-001
4
+
5
+ `run` MUST 保留原始使用者目標,並要求目前有效的 agent-ops 證據。
6
+
7
+ - Trigger: 啟動、修復或恢復 native goal run。
8
+ - Action: writer 綁定目前 lease,並要求最終 verify、雙 review 與 receipt 證據。
9
+ - Evidence: run ledger 與封存 receipt 綁定目前 candidate 及原始目標。
10
+ - Positive: 恢復未完成清理時,保留 target 與已保存的 final proof。
11
+ - Negative: 只因 native goal completion 就把 run 標為 complete。
12
+
13
+ - macOS supervisor 在啟動 goal 前保存 Claude/Codex native 身份。同 host writers 使用獨立 worktrees 與目前 generation lease;coordinator 計入最多兩個 writer。依賴以固定交付提供,child baseline 在依賴 commits 進入 checkout 後建立。
14
+ - 預設 60 分鐘預算計算 active intervals 聯集,包含 setup、verify、review、整合。resume 保留時間與 usage epochs;缺少計數維持 UNKNOWN,partial usage 不算完整成本。
15
+ - 實質疑問暫停並要求明確回答。修復保留目標與失敗紀錄;連續兩次相同失敗且無進展停止該 worker 及其依賴,獨立 task 仍可執行。review findings 在下次 final gate 前必須有回歸 pin 或記錄 review-only fallback。
16
+ - native completion 僅是觀察。完成必須有必要驗證、兩輪 fresh review、完成的 task state、綁定 target 的 receipt。整合在移動 target 前封存 candidate,記錄 task、receipt、note、cleanup 進度;恢復驗證封存證據,不重做成功的 target mutation。不一致的 target 維持 blocked。
17
+ - Stop 先保存禁止續跑,再確認受管 process group 死亡。crash recovery 不得取代仍存活或身份不明的 writer。dirty checkout、版本漂移、restart storm 保留現場。重開機/login 改變必須明確 resume;不修改全域 host policy。
18
+ - Native handshake 前先保存啟動程序身份;尚未開啟的 Codex session 必須證明程序死亡,才可在新 lease 重新初始化。
19
+
20
+ - Setup 與 native agent 發起的 local proof MUST 先登記 dormant process group 再執行;Stop 也要取消子程序。中斷的 setup 必須明確 resume,保留 checkout 與失敗 artifact。
21
+ - Coordinator 只能在綁定原始 repository trust、可執行 runtime、剩餘預算的 run-scoped policy 下,評估新增固定且非危險的能力。不得繞過 native 或組織的明確拒絕。Policy 變更先停止所有 writer,保留固定交付,以 journal 記錄 contract 同步,再啟動新 generation;不授予永久 repository trust。Native decision 保存遮罩後的命令範圍與理由,provider 未提供的證據維持 UNKNOWN。
22
+ - 明確 resume 從剩餘預算續期過期授權,並先核對不可變 renewal lineage,再同步 policy。回答問題也沿用相同恢復檢查;兩者皆不得復活已被明確拒絕的工作。
23
+ - 收斂判斷比較實際 per-check 狀態及未解除的 finding pins;source fingerprint、時間或診斷文字本身不能算進展。
24
+
3
25
  English source version: 2026-07-23. Revalidate: when the English specification changes.
4
26
 
5
27
  ## LOOP-START-001
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylecheng3146/agent-ops",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Evidence-driven development loops for agy, Codex, Claude Code, and opencode",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",