@kylecheng3146/agent-ops 0.5.3 → 0.7.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 +103 -1
- package/dist/packages/cli/src/args.js +183 -3
- package/dist/packages/cli/src/bin.js +76 -19
- package/dist/packages/cli/src/cli.js +53 -8
- package/dist/packages/cli/src/codex-loop-process.js +19 -2
- package/dist/packages/cli/src/commands/advance.js +42 -5
- package/dist/packages/cli/src/commands/init.js +2 -0
- package/dist/packages/cli/src/commands/office.js +20 -0
- package/dist/packages/cli/src/commands/review-show.js +1 -1
- package/dist/packages/cli/src/commands/review.js +100 -9
- package/dist/packages/cli/src/commands/run.js +115 -0
- package/dist/packages/cli/src/commands/task.js +61 -17
- package/dist/packages/cli/src/commands/update.js +8 -0
- package/dist/packages/cli/src/commands/verify.js +1 -0
- package/dist/packages/cli/src/context.js +64 -1
- package/dist/packages/cli/src/hook-entry.js +2 -1
- package/dist/packages/cli/src/hook-process.js +54 -3
- package/dist/packages/cli/src/loop-entry.js +2 -1
- package/dist/packages/cli/src/office-entry.js +230 -0
- 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 +321 -0
- package/dist/packages/cli/src/run-entry.js +952 -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/packages/cli/src/wizard.js +14 -2
- package/dist/runtime/src/adapters/agy/config.js +4 -3
- package/dist/runtime/src/adapters/agy/events.js +9 -0
- package/dist/runtime/src/adapters/claude/config.js +3 -3
- package/dist/runtime/src/adapters/claude/events.js +8 -0
- package/dist/runtime/src/adapters/codex/config.js +3 -3
- package/dist/runtime/src/adapters/codex/events.js +8 -0
- package/dist/runtime/src/adapters/opencode/config.js +5 -4
- package/dist/runtime/src/adapters/opencode/events.js +8 -0
- package/dist/runtime/src/config/explain.js +11 -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/codex-loop.js +4 -0
- 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 +44 -1
- package/dist/runtime/src/install/plan.js +16 -11
- package/dist/runtime/src/install/preauth.js +44 -18
- package/dist/runtime/src/install/profiles.js +8 -2
- package/dist/runtime/src/install/update.js +1 -0
- package/dist/runtime/src/office/collect.js +233 -0
- package/dist/runtime/src/office/page.js +873 -0
- package/dist/runtime/src/office/scene.js +168 -0
- package/dist/runtime/src/office/server.js +139 -0
- package/dist/runtime/src/office/sessions.js +115 -0
- package/dist/runtime/src/office/snapshot.js +107 -0
- package/dist/runtime/src/parallel/finish.js +85 -22
- package/dist/runtime/src/parallel/guard.js +12 -2
- package/dist/runtime/src/parallel/integrate.js +288 -10
- package/dist/runtime/src/parallel/manage.js +4 -1
- package/dist/runtime/src/parallel/receipt.js +71 -5
- package/dist/runtime/src/parallel/service.js +41 -1
- package/dist/runtime/src/review/attestation.js +36 -11
- package/dist/runtime/src/review/execute.js +24 -1
- package/dist/runtime/src/review/packet.js +35 -1
- 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/phase.js +37 -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 +433 -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 +80 -9
- package/dist/runtime/src/task/completion.js +14 -2
- package/dist/runtime/src/task/contract.js +17 -0
- package/dist/runtime/src/task/pin-finding.js +81 -0
- package/dist/runtime/src/task/service.js +316 -13
- 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/spec/acceptance-and-evidence.md +48 -0
- package/docs/en/spec/harness-adapters.md +113 -1
- package/docs/en/spec/loop-engineering.md +49 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +21 -1
- package/docs/zh-TW/spec/harness-adapters.md +93 -1
- package/docs/zh-TW/spec/loop-engineering.md +22 -0
- package/package.json +1 -1
- package/schemas/config.schema.json +230 -19
- package/schemas/evidence.schema.json +129 -6
- package/schemas/task.schema.json +265 -6
- package/templates/acceptance/agent_ops_acceptance.rs +36 -0
- 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
|
|
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
|
+
}
|
|
@@ -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.
|
|
@@ -41,7 +41,7 @@ capabilities and MUST track generated source as one whole-file artifact.
|
|
|
41
41
|
- Action: For opencode, manage `.opencode/plugins/agent-ops.js` in a project or `.config/opencode/plugins/agent-ops.js` at user scope (`$XDG_CONFIG_HOME/opencode/plugins/agent-ops.js` when configured inside the managed user root, or `$OPENCODE_CONFIG_DIR/plugins/agent-ops.js` when that native config directory is configured), leave `opencode.json` untouched, and deduplicate a project `AGENTS.md` contribution by path.
|
|
42
42
|
- Evidence: The manifest contains the plugin hash, the generated source contains only the selected hook registrations, and shared project markers occur once.
|
|
43
43
|
- Positive: `codex,opencode` produces one project AGENTS route and one hashed opencode plugin.
|
|
44
|
-
- Negative: `Add an opencode.json instructions entry or register a plugin for a core-only profile.`
|
|
44
|
+
- Negative: `Add an opencode.json instructions entry or register a plugin for a core-only profile with Office disabled.`
|
|
45
45
|
|
|
46
46
|
## HARNESS-ADAPTER-004
|
|
47
47
|
|
|
@@ -106,6 +106,7 @@ The current registration matrix is intentionally asymmetric:
|
|
|
106
106
|
|
|
107
107
|
| Capability | agy | Codex | Claude Code | OpenCode |
|
|
108
108
|
| --- | --- | --- | --- | --- |
|
|
109
|
+
| office-presence (Preview) | degraded | degraded | supported | degraded |
|
|
109
110
|
| lifecycle-summary | degraded | supported | supported | degraded |
|
|
110
111
|
| command-policy | supported | unknown | supported | supported |
|
|
111
112
|
| completion-gate | supported | unsupported | unsupported | unsupported |
|
|
@@ -137,3 +138,114 @@ stores only bounded local event metadata, returns bounded redacted session
|
|
|
137
138
|
context, and preserves local goal, state, telemetry, and Codex TOML files on
|
|
138
139
|
update or uninstall. A clearly parsed `[features]` / `hooks = false` in an
|
|
139
140
|
existing Codex configuration MUST reject loop planning before any write.
|
|
141
|
+
|
|
142
|
+
## HARNESS-ADAPTER-007
|
|
143
|
+
|
|
144
|
+
The `run` profile MUST be opt-in, select `core` and `loop` with it, and add only
|
|
145
|
+
the `auto-run` capability. With it, the Claude Code and Codex managed rules hand
|
|
146
|
+
a change that needs more than five acceptance criteria to `agent-ops run`; five
|
|
147
|
+
or fewer stay in the session. agy rules, and every rule file without `auto-run`,
|
|
148
|
+
MUST stay byte-identical to the rules without the profile.
|
|
149
|
+
|
|
150
|
+
- Trigger: A project selects `run`, and a Claude Code or Codex session meets a
|
|
151
|
+
change that needs more than five acceptance criteria.
|
|
152
|
+
- Action: From the main checkout and before any `task create`, the agent writes
|
|
153
|
+
the user's prompt verbatim and its proposed acceptance criteria, marked as
|
|
154
|
+
proposals, to the gitignored `.agent-ops/state/run-goal.md`, then starts
|
|
155
|
+
`agent-ops run --goal-file .agent-ops/state/run-goal.md --host <claude|codex> --wait`
|
|
156
|
+
as a background shell command. Codex starts it like review: an outer request
|
|
157
|
+
with `sandbox_permissions: "require_escalated"` and
|
|
158
|
+
`env -u CODEX_SANDBOX_NETWORK_DISABLED`. An awaiting-input result is relayed
|
|
159
|
+
to the user and answered with `agent-ops run respond`. A start refused with
|
|
160
|
+
`RUN_TARGET_REQUIRED`, `RUN_BACKGROUND_UNSUPPORTED`, `RUN_TARGET_DIRTY` or
|
|
161
|
+
`WORKTREE_NESTED` is reported in one line naming the code and falls back to
|
|
162
|
+
the subtask flow; `RUN_REPO_UNTRUSTED` stops and asks the user. A run that
|
|
163
|
+
ends without completing (blocked, budget exhausted, stopped, failing review)
|
|
164
|
+
is reported with its `run status` state and code, its last `run logs` events,
|
|
165
|
+
and the exact `agent-ops run resume <id>` and `agent-ops run stop <id>`
|
|
166
|
+
commands, and is never resumed automatically or taken over by the session.
|
|
167
|
+
With worktree auto mode, the Claude `PreToolUse` worktree guard allows a
|
|
168
|
+
write to exactly `.agent-ops/state/run-goal.md` in the main checkout without
|
|
169
|
+
creating a worktree. Confirmed init/update pre-authorizes `agent-ops run`:
|
|
170
|
+
`Bash(agent-ops run *)` for Claude Code, and an escalated prefix rule for
|
|
171
|
+
`env -u CODEX_SANDBOX_NETWORK_DISABLED agent-ops run` for Codex.
|
|
172
|
+
- Evidence: Profile, managed-rule digest, pre-authorization, and worktree-guard
|
|
173
|
+
tests cover the resolved profiles, the unchanged rules, the added entries, and
|
|
174
|
+
the single exempt path.
|
|
175
|
+
- Positive: `A Claude Code session with the run profile writes .agent-ops/state/run-goal.md in the main checkout and starts agent-ops run --host claude --wait in the background.`
|
|
176
|
+
- Negative: `Split an eight-criteria change into subtasks while the run profile is active, add auto-run text to GEMINI.md, or resume a blocked run without the user.`
|
|
177
|
+
|
|
178
|
+
## HARNESS-ADAPTER-008
|
|
179
|
+
|
|
180
|
+
`agent-ops office` (Preview) MUST show progress read-only from agent-ops state alone. The
|
|
181
|
+
supervisor records an optional phase per run and worker (`planning`,
|
|
182
|
+
`implementing`, `verifying`, `reviewing`, `integrating`) at deterministic
|
|
183
|
+
transitions, and each task's latest verify and review result with passed/total
|
|
184
|
+
criteria. Run state written before phases existed MUST still validate and show
|
|
185
|
+
as `unknown`. Changed files come from each worktree's own `git diff` against its
|
|
186
|
+
base; narration uses paths only (`docs/**` is "writing docs", `tests/**` is
|
|
187
|
+
"writing tests", otherwise "editing <file>"). The office MUST NOT read native
|
|
188
|
+
Claude or Codex transcripts or file contents.
|
|
189
|
+
|
|
190
|
+
- Office Preview is opt-in via `features.office.enabled`. Init MUST offer an
|
|
191
|
+
off-by-default choice; interactive update MUST default to the existing choice.
|
|
192
|
+
Non-interactive update MUST preserve it unless `--office on|off` is supplied.
|
|
193
|
+
Older configs without the optional field remain valid and mean disabled.
|
|
194
|
+
Enabling Office MUST install its presence hooks even with only `core`; this
|
|
195
|
+
MUST NOT opt into command policy, Stop verification or lifecycle summaries.
|
|
196
|
+
Office-only OpenCode runtime failure MUST remain fail-open; adding Office to
|
|
197
|
+
command policy MUST preserve policy denial and unavailable-runtime behavior.
|
|
198
|
+
Agy startup is per invocation, OpenCode startup is app-scoped, and Codex
|
|
199
|
+
Stop delivery is unavailable in some execution modes; stale presence expires
|
|
200
|
+
using the bounded hook-activity timeout rather than assuming native Stop.
|
|
201
|
+
Features retain their existing whole-object layer precedence. Disabled hooks
|
|
202
|
+
MUST NOT record Office presence or start/open Office; the explicit command
|
|
203
|
+
MUST explain how to enable it. A running server MUST close after observing a
|
|
204
|
+
disabled choice on its next 15-second lifecycle check.
|
|
205
|
+
- Trigger: When enabled, a managed ordinary session starts or reports activity, or a user runs
|
|
206
|
+
`agent-ops office`, `agent-ops run start` or `agent-ops run status`.
|
|
207
|
+
- Action: The command reuses the live server recorded in
|
|
208
|
+
`<git common dir>/agent-ops/office.json`, or starts one, and prints its URL.
|
|
209
|
+
Ordinary session hooks also start or reuse it, including before a task or
|
|
210
|
+
worktree exists. Only the successful new-server owner opens the browser;
|
|
211
|
+
other sessions share the same Office. Session metadata contains bounded
|
|
212
|
+
identifiers, known work state and activity times, never native transcripts,
|
|
213
|
+
prompts or tool/file contents. Startup and observation failures are advisory.
|
|
214
|
+
The server binds
|
|
215
|
+
127.0.0.1 on a random port, requires the URL's unguessable token, refuses any
|
|
216
|
+
Host other than that address and port, answers every non-GET with 405, and
|
|
217
|
+
exits 10 minutes after neither a run nor an ordinary session is active.
|
|
218
|
+
Ordinary-session liveness follows hooks, with stale presence expiring after
|
|
219
|
+
30 minutes without activity; it does not inspect native host processes.
|
|
220
|
+
Its single inline page uses cozy cream, oak and sage in-code 16-color scene
|
|
221
|
+
sprites with no image assets. Each 32×48 character uses its own fixed skin,
|
|
222
|
+
hair and clothing palette, four-direction walking, and localized role labels.
|
|
223
|
+
The viewer's badge displays `You` in English and `你` in Chinese; its overview size matches the largest room's
|
|
224
|
+
characters. The fixed-viewport overview
|
|
225
|
+
fills the available page below the header and shows all furnished work-unit
|
|
226
|
+
rooms, shrinking rooms as necessary without document scrolling. A shared
|
|
227
|
+
hallway connects the rooms. The user controls a local Supervisor with arrow
|
|
228
|
+
keys or WASD, can walk into every room and return to the overview, without
|
|
229
|
+
model requests or network mutations. Each run team shares a room; each ordinary session has its own,
|
|
230
|
+
and running reviews remain visible. Clicking a room enters a larger detail
|
|
231
|
+
view with a return control. Every room contains planning, development,
|
|
232
|
+
verification, review and integration areas. Characters walk to their known
|
|
233
|
+
phase's zone, use light phase-driven animation, and have short name/status
|
|
234
|
+
labels; unknown state is not inferred from dialogue. Whiteboards show task
|
|
235
|
+
criteria progress, verify/review outcomes and pending questions, with `!` for
|
|
236
|
+
unanswered questions. Clicking a whiteboard or progress board opens the room
|
|
237
|
+
work list; workstations and phase areas show the known work in that phase.
|
|
238
|
+
Pending markers open complete questions with the existing copyable commands.
|
|
239
|
+
The same paged views are available with `L` (work list), `1–5` (phases) and
|
|
240
|
+
`Q` (questions). Clicking an actor opens bounded, paged status details
|
|
241
|
+
with copyable commands. Completed rooms are available through Recently
|
|
242
|
+
completed for at most two hours. Chinese/English switching remembers the
|
|
243
|
+
choice, and keyboard navigation and reduced motion are supported. A failure
|
|
244
|
+
to start the office never fails a hook, `run start` or `run status`.
|
|
245
|
+
- Evidence: Phase, snapshot, server, scene and CLI tests cover legacy state,
|
|
246
|
+
aggregation and narration, ordinary-session registration and startup-once,
|
|
247
|
+
the two-hour completion boundary, token/Host/method guards, reuse and the
|
|
248
|
+
injected-clock idle exit, viewport/room navigation, localization, and the
|
|
249
|
+
printed URL. A browser read-back checks the rendered single-page layout.
|
|
250
|
+
- Positive: `An ordinary session opens the shared Office, where its room is visible before it creates a worktree; entering a team room shows the coordinator at the verification bench and a whiteboard with 2 of 6 criteria passed.`
|
|
251
|
+
- Negative: `Serve the office on 0.0.0.0, accept a POST, read a worker's transcript to narrate it, or bundle a PNG sprite sheet.`
|
|
@@ -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
|