@kylecheng3146/agent-ops 0.9.0 → 0.10.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/dist/packages/cli/src/args.js +48 -4
- package/dist/packages/cli/src/bin.js +10 -3
- package/dist/packages/cli/src/cli.js +16 -2
- package/dist/packages/cli/src/commands/escapes.js +109 -0
- package/dist/packages/cli/src/commands/review-show.js +1 -1
- package/dist/packages/cli/src/commands/review.js +60 -10
- package/dist/packages/cli/src/commands/task.js +41 -3
- package/dist/packages/cli/src/commands/update.js +16 -4
- package/dist/packages/cli/src/commands/verify.js +4 -1
- package/dist/packages/cli/src/debug-intake.js +1 -1
- package/dist/runtime/src/escape/link.js +317 -0
- package/dist/runtime/src/escape/origin.js +20 -0
- package/dist/runtime/src/escape/rules.js +252 -0
- package/dist/runtime/src/fs/temporary.js +10 -0
- package/dist/runtime/src/install/harness.js +2 -2
- package/dist/runtime/src/logging/local-log.js +44 -1
- package/dist/runtime/src/parallel/manage.js +51 -1
- package/dist/runtime/src/parallel/receipt.js +3 -1
- package/dist/runtime/src/review/execute.js +12 -6
- package/dist/runtime/src/review/packet.js +3 -1
- package/dist/runtime/src/review/probe.js +3 -2
- package/dist/runtime/src/review/runner.js +24 -3
- package/dist/runtime/src/review/slots.js +1 -1
- package/dist/runtime/src/schema/validate.js +6 -1
- package/dist/runtime/src/surface/discovery.js +79 -2
- package/dist/runtime/src/surface/schema.js +43 -2
- package/dist/runtime/src/task/completion.js +20 -0
- package/dist/runtime/src/task/contract.js +3 -1
- package/dist/runtime/src/task/debug.js +24 -1
- package/dist/runtime/src/task/service.js +58 -6
- package/dist/runtime/src/verify/acceptance-replay.js +24 -11
- package/docs/en/guides/configuration.md +16 -1
- package/docs/en/guides/task-debug.md +12 -0
- package/docs/en/spec/acceptance-and-evidence.md +100 -2
- package/docs/zh-TW/guides/configuration.md +14 -1
- package/docs/zh-TW/guides/task-debug.md +10 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +82 -3
- package/package.json +1 -1
- package/schemas/task.schema.json +8 -1
|
@@ -21,6 +21,7 @@ import { readWorktreeRecord } from "../parallel/service.js";
|
|
|
21
21
|
import { evaluateGuardrail } from "../guardrails/evaluate.js";
|
|
22
22
|
import { sameDebugText } from "./debug-intake.js";
|
|
23
23
|
import { nativeProcessIdentity } from "../run/transport.js";
|
|
24
|
+
import { declareEscape, LEDGER_DIRECTORY, removeEscapeRecord, resolveOrigin } from "../escape/link.js";
|
|
24
25
|
function fail(code, message) { throw new AgentOpsError(code, message); }
|
|
25
26
|
const bounded = (v) => typeof v === "string" && v.trim().length > 0 && v.length <= 4096 && !v.includes("\0");
|
|
26
27
|
const session = (v) => bounded(v) && v.length <= 256;
|
|
@@ -93,7 +94,8 @@ export class DebugService {
|
|
|
93
94
|
const records = await this.options.tasks.list();
|
|
94
95
|
const materials = new Set(records.flatMap(r => r.task.criteria.flatMap(c => c.acceptance?.bindings.flatMap(b => b.materials.map(m => m.path)) ?? [])));
|
|
95
96
|
const paths = new Set([...await collectBaseChangePaths(this.options.gitRunner, state.baselineCommit), ...(await collectChangeSurface(this.options.gitRunner)).paths]);
|
|
96
|
-
|
|
97
|
+
// The escape ledger is data about the fix, not a repair candidate.
|
|
98
|
+
const product = [...paths].filter(path => !materials.has(path) && !path.startsWith(LEDGER_DIRECTORY + "/") && !/(?:^|\/)(?:tests?|fixtures)(?:\/|$)|(?:\.test|\.spec)\./u.test(path)).sort();
|
|
97
99
|
const entries = [];
|
|
98
100
|
for (const path of product) {
|
|
99
101
|
try {
|
|
@@ -243,6 +245,27 @@ export class DebugService {
|
|
|
243
245
|
await this.#evidence(taskId, input.reference);
|
|
244
246
|
await this.#mutate(root, { ...root.debug, strategy: { from: input.from, to: input.to, reason: input.reason, references: [input.reference], afterRound: debugRounds(root.debug) }, checkpoint: null });
|
|
245
247
|
}
|
|
248
|
+
/** Escape Loop: where the confirmed cause came from, bound into the contract before review. */
|
|
249
|
+
async origin(taskId, input) {
|
|
250
|
+
const root = await this.root(taskId);
|
|
251
|
+
if (root.task.id !== taskId)
|
|
252
|
+
fail("ESCAPE_ORIGIN_INVALID", "Declare the origin on the original issue task.");
|
|
253
|
+
if (root.status !== "active" || root.debug.operation !== null)
|
|
254
|
+
fail("TASK_DEBUG_BUSY", "Origin needs an active idle issue.");
|
|
255
|
+
if (debugRemainingMs(root.debug) === 0)
|
|
256
|
+
fail("TASK_DEBUG_BUDGET_EXHAUSTED", "The original issue's active budget is exhausted.");
|
|
257
|
+
if (!root.debug.hypotheses.some(h => h.status === "confirmed"))
|
|
258
|
+
fail("ESCAPE_ORIGIN_EARLY", "Declare the origin only after discriminating evidence confirms a hypothesis.");
|
|
259
|
+
const origin = await resolveOrigin(this.options.gitRunner, input);
|
|
260
|
+
const record = await this.options.tasks.revise(taskId, { expectedContractHash: taskContractHash(root.task), origin, debugOrigin: true,
|
|
261
|
+
reason: `Declare issue origin: ${origin.kind === "escape-of" ? "escape-of " + origin.commit : origin.kind}.` });
|
|
262
|
+
if (origin.kind !== "escape-of") {
|
|
263
|
+
await removeEscapeRecord(this.options.root, taskId);
|
|
264
|
+
return { record };
|
|
265
|
+
}
|
|
266
|
+
return { record, escape: await declareEscape({ root: this.options.root, git: this.options.gitRunner, taskId, origin,
|
|
267
|
+
base: root.debug.baselineCommit, now: new Date().toISOString() }) };
|
|
268
|
+
}
|
|
246
269
|
async remaining(taskId) { return debugRemainingMs((await this.root(taskId)).debug); }
|
|
247
270
|
async #userEvent(sessionId, home, kind = "latest") {
|
|
248
271
|
const record = await readWorktreeRecord(this.options.root);
|
|
@@ -19,6 +19,9 @@ import { createDebugState, DEBUG_DEFAULTS } from "./debug-state.js";
|
|
|
19
19
|
import { DebugService } from "./debug.js";
|
|
20
20
|
import { sameDebugText } from "./debug-intake.js";
|
|
21
21
|
import { isCovers, parseSurface } from "../surface/schema.js";
|
|
22
|
+
import { declareEscape, resolveOrigin } from "../escape/link.js";
|
|
23
|
+
import { configProposal } from "../escape/rules.js";
|
|
24
|
+
import { logGateCatch } from "../logging/local-log.js";
|
|
22
25
|
const SESSION_ID_PATTERN = /^[^\0\r\n]{1,256}$/u;
|
|
23
26
|
function defaultTaskId() {
|
|
24
27
|
return `task-${randomUUID()}`;
|
|
@@ -130,6 +133,7 @@ export class TaskService {
|
|
|
130
133
|
id: this.#generateId(),
|
|
131
134
|
...(surface === undefined ? {} : { surface }),
|
|
132
135
|
...(input.requireSurface === true ? { surfaceRequired: true } : {}),
|
|
136
|
+
...(input.origin === undefined ? {} : { origin: structuredClone(input.origin) }),
|
|
133
137
|
title: input.title,
|
|
134
138
|
...(input.intent === undefined ? {} : { intent: input.intent }),
|
|
135
139
|
criteria,
|
|
@@ -149,6 +153,10 @@ export class TaskService {
|
|
|
149
153
|
throw taskError("TASK_DEBUG_DISABLED", "Task debug routing is explicitly disabled.");
|
|
150
154
|
debugSettings = config.features.debug ?? DEBUG_DEFAULTS;
|
|
151
155
|
task.debug = { rootTaskId: task.id, facts: [...(input.debug.facts ?? [])], unknowns: [...(input.debug.unknowns ?? [])] };
|
|
156
|
+
if (input.origin !== undefined)
|
|
157
|
+
throw taskError("ESCAPE_ORIGIN_DEBUG_ACTION", "A DEBUG issue declares its origin with task debug --debug-action origin after a confirmed hypothesis.");
|
|
158
|
+
if (input.parentTaskId === undefined)
|
|
159
|
+
task.originRequired = true;
|
|
152
160
|
}
|
|
153
161
|
const validation = validateTask(task);
|
|
154
162
|
if (!validation.ok) {
|
|
@@ -262,8 +270,16 @@ export class TaskService {
|
|
|
262
270
|
throw taskError("TASK_NOT_ACTIVE", "Only active tasks can revise acceptance.");
|
|
263
271
|
if (taskContractHash(previous.task) !== input.expectedContractHash)
|
|
264
272
|
throw taskError("TASK_CONTRACT_CHANGED", "Task contract changed.");
|
|
265
|
-
|
|
266
|
-
|
|
273
|
+
// With none of them, a revision only rebinds the task to the current configuration (checked below).
|
|
274
|
+
const rebindOnly = input.criteria === undefined && input.surface === undefined && input.origin === undefined;
|
|
275
|
+
if (rebindOnly && this.#completion === undefined)
|
|
276
|
+
throw taskError("TASK_REVISION_INVALID", "Revision requires criteria, a surface, an origin, or a combination.");
|
|
277
|
+
if (input.origin !== undefined && previous.task.debug !== undefined && input.debugOrigin !== true)
|
|
278
|
+
throw taskError("ESCAPE_ORIGIN_DEBUG_ACTION", "A DEBUG issue declares its origin with task debug --debug-action origin after a confirmed hypothesis.");
|
|
279
|
+
// Re-declaring the same origin only refreshes its ledger record; the contract and its proof stand.
|
|
280
|
+
if (input.criteria === undefined && input.surface === undefined && input.origin !== undefined &&
|
|
281
|
+
canonicalJson(input.origin) === canonicalJson(previous.task.origin ?? null))
|
|
282
|
+
return previous;
|
|
267
283
|
const surface = input.surface === undefined ? previous.task.surface : parseSurface(input.surface);
|
|
268
284
|
const baseline = input.baseline ?? previous.createdSourceCommit;
|
|
269
285
|
const criteria = input.criteria === undefined ? structuredClone(previous.task.criteria) : structuredClone(input.criteria).map(criterion => {
|
|
@@ -278,7 +294,8 @@ export class TaskService {
|
|
|
278
294
|
return { ...criterion, acceptance: { ...criterion.acceptance, baselineCommit: sha } };
|
|
279
295
|
});
|
|
280
296
|
const next = { ...previous.task, schemaVersion: TASK_SCHEMA_VERSION,
|
|
281
|
-
contractRevision: (previous.task.contractRevision ?? 0) + 1, criteria, ...(surface === undefined ? {} : { surface })
|
|
297
|
+
contractRevision: (previous.task.contractRevision ?? 0) + 1, criteria, ...(surface === undefined ? {} : { surface }),
|
|
298
|
+
...(input.origin === undefined ? {} : { origin: structuredClone(input.origin) }) };
|
|
282
299
|
const checked = validateTask(next);
|
|
283
300
|
if (!checked.ok)
|
|
284
301
|
throw taskError("TASK_INVALID", checked.errors[0]?.message ?? "Invalid criterion revision.");
|
|
@@ -293,6 +310,8 @@ export class TaskService {
|
|
|
293
310
|
throw taskError("TASK_INVALID", configured.errors[0]?.message ?? "Invalid binding.");
|
|
294
311
|
policyConfigHash = calculateConfigHash(config);
|
|
295
312
|
}
|
|
313
|
+
if (rebindOnly && policyConfigHash === previous.policyConfigHash)
|
|
314
|
+
throw taskError("TASK_REVISION_INVALID", "Nothing to revise: criteria, surface, origin and configuration are unchanged.");
|
|
296
315
|
if (criteria.some(c => c.acceptance !== undefined))
|
|
297
316
|
await this.#retainBaselines(next);
|
|
298
317
|
const now = assertTimestamp(this.#now());
|
|
@@ -680,7 +699,10 @@ export class TaskService {
|
|
|
680
699
|
async recordVerificationEvidence(snapshot, evidenceInput, sourceFingerprint) {
|
|
681
700
|
if (snapshot.status === "active") {
|
|
682
701
|
const result = await this.recordEvidence(snapshot.task.id, evidenceInput);
|
|
683
|
-
|
|
702
|
+
// A verified red turning green is a gate catch (advisory, fail-open).
|
|
703
|
+
if (await this.clearFailure(snapshot.task.id) &&
|
|
704
|
+
(await this.#completion?.loadConfig().catch(() => undefined))?.profiles.includes("advisory") === true)
|
|
705
|
+
await logGateCatch("VERIFY_RED_GREEN", snapshot.task.id);
|
|
684
706
|
return result;
|
|
685
707
|
}
|
|
686
708
|
const completion = this.#completion;
|
|
@@ -804,18 +826,48 @@ export class TaskService {
|
|
|
804
826
|
return structuredClone(advanced);
|
|
805
827
|
});
|
|
806
828
|
}
|
|
829
|
+
/** Escape Loop: resolve a declared commit-ish through this repository's Git (argv only). */
|
|
830
|
+
async resolveOrigin(value) {
|
|
831
|
+
if (this.#completion === undefined)
|
|
832
|
+
throw taskError("ESCAPE_CONTEXT_REQUIRED", "Escape linking requires repository context.");
|
|
833
|
+
return await resolveOrigin(this.#completion.gitRunner, value);
|
|
834
|
+
}
|
|
835
|
+
/** Link a normal task's escape-of origin and write its tracked ledger record. */
|
|
836
|
+
async declareEscape(taskId) {
|
|
837
|
+
const completion = this.#completion;
|
|
838
|
+
const record = await this.status({ taskId });
|
|
839
|
+
const origin = record.task.origin;
|
|
840
|
+
if (completion === undefined || origin?.kind !== "escape-of" || record.createdSourceCommit === undefined)
|
|
841
|
+
throw taskError("ESCAPE_CONTEXT_REQUIRED", "Escape linking needs an escape-of origin, a creation commit and repository context.");
|
|
842
|
+
return await declareEscape({ root: completion.root, git: completion.gitRunner, taskId, origin,
|
|
843
|
+
base: record.createdSourceCommit, now: assertTimestamp(this.#now()) });
|
|
844
|
+
}
|
|
845
|
+
/** Escape Loop R2: printed only, never written; the promoted check becomes a required verifier through update. */
|
|
846
|
+
async escapeConfigProposal(check) {
|
|
847
|
+
let config;
|
|
848
|
+
try {
|
|
849
|
+
config = await this.#completion?.loadConfig();
|
|
850
|
+
}
|
|
851
|
+
catch {
|
|
852
|
+
config = undefined;
|
|
853
|
+
}
|
|
854
|
+
return configProposal(check, config);
|
|
855
|
+
}
|
|
856
|
+
/** True when a recorded verification failure was cleared (red to green). */
|
|
807
857
|
async clearFailure(taskId) {
|
|
808
858
|
const now = assertTimestamp(this.#now());
|
|
809
|
-
await this.#store.mutate((state) => {
|
|
859
|
+
return await this.#store.mutate((state) => {
|
|
810
860
|
const current = findTask(state, taskId);
|
|
811
861
|
if (current.failureFingerprint === null) {
|
|
812
|
-
return;
|
|
862
|
+
return false;
|
|
813
863
|
}
|
|
814
864
|
replaceTask(state, {
|
|
815
865
|
...current,
|
|
816
866
|
updatedAt: now,
|
|
817
867
|
failureFingerprint: null
|
|
818
868
|
});
|
|
869
|
+
// A source change mid-run is not a red the code fixed.
|
|
870
|
+
return current.failureFingerprint.commandId !== "source-snapshot";
|
|
819
871
|
});
|
|
820
872
|
}
|
|
821
873
|
async coverage(taskId) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lstat, mkdir, mkdtemp, readFile, realpath,
|
|
1
|
+
import { lstat, mkdir, mkdtemp, readFile, realpath, writeFile, chmod } from "node:fs/promises";
|
|
2
2
|
import { join, delimiter, isAbsolute } from "node:path";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { writePrivateFile } from "../security/permissions.js";
|
|
@@ -6,6 +6,7 @@ import { redactSecrets } from "../security/redact.js";
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { canonicalJson } from "../config/hash.js";
|
|
8
8
|
import { sha256 } from "../fs/hash.js";
|
|
9
|
+
import { removeTemporary } from "../fs/temporary.js";
|
|
9
10
|
import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
|
|
10
11
|
import { criterionContractHash, taskContractHash } from "../task/contract.js";
|
|
11
12
|
import { aggregateAcceptanceRun, makeAcceptanceCheck, makeAcceptanceRun } from "./acceptance-protocol.js";
|
|
@@ -13,6 +14,8 @@ import { collectNodeAcceptance, collectJestAcceptance, collectVitestAcceptance,
|
|
|
13
14
|
import { buildVerificationEvidence } from "./evidence.js";
|
|
14
15
|
import { collectChangeSurface, resolveGitCommit } from "./change-surface.js";
|
|
15
16
|
import { runVerificationCommand } from "./spawn.js";
|
|
17
|
+
/** Limits for runner steps without their own timeoutMs: installs are slow, git plumbing is not. */
|
|
18
|
+
export const ACCEPTANCE_TIMEOUTS = { setup: 600_000, build: 300_000, testBuild: 300_000, run: 300_000, git: 60_000 };
|
|
16
19
|
async function executableIdentity(command, cwd, env) {
|
|
17
20
|
const paths = isAbsolute(command) ? [command] : command.includes("/") ? [join(cwd, command)] :
|
|
18
21
|
(env.PATH ?? "").split(delimiter).filter(Boolean).map(path => join(path, command));
|
|
@@ -113,13 +116,21 @@ export async function replayAcceptance(options) {
|
|
|
113
116
|
const temp = await mkdtemp(join(tmpdir(), "agent-ops-acceptance-"));
|
|
114
117
|
const checkout = join(temp, "repository");
|
|
115
118
|
const env = replayEnvironment(checkout, phase, executionId);
|
|
116
|
-
|
|
119
|
+
let timedOut;
|
|
120
|
+
const command = async (executable, args, cwd, stage, own) => {
|
|
117
121
|
const remaining = await options.remainingMs?.();
|
|
118
122
|
if (remaining === 0)
|
|
119
123
|
throw new AgentOpsError("TASK_DEBUG_BUDGET_EXHAUSTED", "The original issue's active budget is exhausted.");
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
const limit = own ?? ACCEPTANCE_TIMEOUTS[stage];
|
|
125
|
+
const result = await runVerificationCommand({ id: group.runner.id, command: executable, args: [...args], cwd: ".", required: true,
|
|
126
|
+
evidence: { kind: "exit-code" }, timeoutMs: Math.min(limit, remaining ?? Infinity) }, { cwd, runner: options.processRunner, env, replaceEnv: true,
|
|
122
127
|
outputLimitBytes: 1024 * 1024, ...(options.signal === undefined ? {} : { signal: options.signal }) });
|
|
128
|
+
if (result.failureClass === "timeout" && timedOut === undefined)
|
|
129
|
+
timedOut = stage === "git"
|
|
130
|
+
? `Runner ${group.runner.id} ${phase}: git checkout exceeded ${limit} ms.`
|
|
131
|
+
: `Runner ${group.runner.id} ${phase}: ${stage} step ${executable} exceeded ${limit} ms; raise ` +
|
|
132
|
+
(stage === "run" ? "verification.acceptanceRunners[].timeoutMs" : `the step's timeoutMs in verification.acceptanceRunners[].${stage}`) + ".";
|
|
133
|
+
return result;
|
|
123
134
|
};
|
|
124
135
|
let aggregate;
|
|
125
136
|
let failure = "replay-unavailable";
|
|
@@ -133,13 +144,14 @@ export async function replayAcceptance(options) {
|
|
|
133
144
|
try {
|
|
134
145
|
for (const [cwd, args] of [[temp, ["clone", "--no-hardlinks", "--quiet", "--", options.root, checkout]],
|
|
135
146
|
[checkout, ["fetch", "--quiet", "--", options.root, commit]], [checkout, ["checkout", "--detach", "--quiet", commit]]]) {
|
|
136
|
-
const result = await command("git", args, cwd);
|
|
147
|
+
const result = await command("git", args, cwd, "git");
|
|
137
148
|
if (result.status !== "PASS")
|
|
138
149
|
throw new Error("checkout-unavailable");
|
|
139
150
|
}
|
|
140
151
|
const cwd = group.runner.cwd === "." ? checkout : await resolveContainedPath(checkout, group.runner.cwd);
|
|
141
|
-
for (const step of [...(group.runner.setup ?? [])
|
|
142
|
-
|
|
152
|
+
for (const [stage, step] of [...(group.runner.setup ?? []).map(step => ["setup", step]),
|
|
153
|
+
...(group.runner.build ?? []).map(step => ["build", step])]) {
|
|
154
|
+
const result = await command(step.command, step.args, cwd, stage, step.timeoutMs);
|
|
143
155
|
steps.push({ command: step.command, args: step.args, status: result.status, failureClass: result.failureClass, stderr: result.stderr });
|
|
144
156
|
if (result.status !== "PASS")
|
|
145
157
|
throw new Error("setup-or-build-unavailable");
|
|
@@ -172,7 +184,7 @@ export async function replayAcceptance(options) {
|
|
|
172
184
|
await chmod(path, material.mode);
|
|
173
185
|
}
|
|
174
186
|
for (const step of group.runner.testBuild ?? []) {
|
|
175
|
-
const result = await command(step.command, step.args, cwd, step.timeoutMs);
|
|
187
|
+
const result = await command(step.command, step.args, cwd, "testBuild", step.timeoutMs);
|
|
176
188
|
steps.push({ command: step.command, args: step.args, status: result.status, failureClass: result.failureClass, stderr: result.stderr });
|
|
177
189
|
if (result.status !== "PASS")
|
|
178
190
|
throw new Error("test-build-unavailable");
|
|
@@ -194,7 +206,7 @@ export async function replayAcceptance(options) {
|
|
|
194
206
|
throw new Error("missing-executable");
|
|
195
207
|
executionDigest = sha256(canonicalJson({ commit, materialDigest, runner: group.runner, argv, dependencies,
|
|
196
208
|
executable: identity, platform: process.platform, arch: process.arch, environment: sha256(canonicalJson(env)) }));
|
|
197
|
-
output = await command(group.runner.command, argv, cwd, group.runner.timeoutMs);
|
|
209
|
+
output = await command(group.runner.command, argv, cwd, "run", group.runner.timeoutMs);
|
|
198
210
|
run = collected(group.runner, output, checkout, { executionId, phase,
|
|
199
211
|
frameworkVersion: group.runner.adapter === "node" ? process.version : "dependency-lock:" + sha256(canonicalJson(dependencies)) });
|
|
200
212
|
aggregate = aggregateAcceptanceRun(run, requested);
|
|
@@ -208,7 +220,7 @@ export async function replayAcceptance(options) {
|
|
|
208
220
|
failure = error instanceof AgentOpsError && error.code === "TASK_DEBUG_BUDGET_EXHAUSTED" ? "debug-budget-exhausted" : error instanceof Error && /^[a-z-]+$/u.test(error.message) ? error.message : "replay-unavailable";
|
|
209
221
|
}
|
|
210
222
|
finally {
|
|
211
|
-
await
|
|
223
|
+
await removeTemporary(temp);
|
|
212
224
|
}
|
|
213
225
|
const finishedAt = new Date().toISOString();
|
|
214
226
|
const artifactContent = redactSecrets(JSON.stringify({ schemaVersion: 1, executionId, phase, commit,
|
|
@@ -236,7 +248,8 @@ export async function replayAcceptance(options) {
|
|
|
236
248
|
taskContractHash: taskContractHash(options.task), acceptance: { criterionContractHash: criterionContractHash(criterion),
|
|
237
249
|
bindingHash: sha256(canonicalJson({ runnerId: group.runner.id, materials: group.binding.materials })), phase, commit, pairedCommit: phase === "baseline" ? candidate : group.baseline, materialDigest, executionDigest, executionArtifact, checks } });
|
|
238
250
|
const reference = await options.evidenceStore.save(evidence);
|
|
239
|
-
reports.push({ criterionId: criterion.id, runnerId: group.runner.id, phase, status, failureClass: evidence.failureClass, reference
|
|
251
|
+
reports.push({ criterionId: criterion.id, runnerId: group.runner.id, phase, status, failureClass: evidence.failureClass, reference,
|
|
252
|
+
...(timedOut === undefined || status === "PASS" ? {} : { detail: timedOut }) });
|
|
240
253
|
}
|
|
241
254
|
}
|
|
242
255
|
}
|
|
@@ -115,7 +115,14 @@ Capability checks and model starts are reported on stderr, including under
|
|
|
115
115
|
`--json`; stdout remains one final JSON envelope and raw reviewer output is
|
|
116
116
|
never streamed. SIGINT or SIGTERM terminates the active reviewer process tree,
|
|
117
117
|
does not advance the fallback chain, and never writes an attestation. An
|
|
118
|
-
exhausted timeout chain reports `timeout`, not `missing-cli`.
|
|
118
|
+
exhausted timeout chain reports `timeout`, not `missing-cli`. Each reviewer
|
|
119
|
+
round may take 30 minutes by default (`reviewRoles[].timeoutMs` overrides it);
|
|
120
|
+
a reviewer that prints nothing for the stall window is still stopped early.
|
|
121
|
+
|
|
122
|
+
Acceptance runner steps without their own `timeoutMs` get 10 minutes for
|
|
123
|
+
`setup`, 5 minutes each for `build`, `testBuild` and the runner itself, and
|
|
124
|
+
1 minute for the git checkout. A step that times out is reported as
|
|
125
|
+
`UNKNOWN` with the step, its limit and the `timeoutMs` field that raises it.
|
|
119
126
|
|
|
120
127
|
The task ID is explicit and its criteria are always used; no session fallback
|
|
121
128
|
or criterion filter can replace the original task requirements. Review rechecks
|
|
@@ -200,6 +207,14 @@ evidence. Give each editing conversation its own worktree instead:
|
|
|
200
207
|
`.env` or `local.properties`); a file Git already checked out is never
|
|
201
208
|
overwritten. Trust is inherited only when the main checkout is trusted and
|
|
202
209
|
the worktree's effective config is identical.
|
|
210
|
+
- An approved `agent-ops update` in the main checkout copies the new config
|
|
211
|
+
into every session worktree whose copy is still the previously trusted one,
|
|
212
|
+
and trusts it for the same commands; the plan lists the session worktrees
|
|
213
|
+
before you confirm. A hand-edited worktree config is left alone, and run
|
|
214
|
+
worktrees keep their own authorization. A task created under the old config is
|
|
215
|
+
not rebound automatically: review names the
|
|
216
|
+
`agent-ops task revise --task <id> --expected-contract <hash> --reason <text>`
|
|
217
|
+
that rebinds it (criteria and surface unchanged), then verify again.
|
|
203
218
|
- `setup` commands run in the new worktree (default timeout ten minutes) for
|
|
204
219
|
what Git does not carry, such as installed dependencies. They are part of
|
|
205
220
|
the trusted config, so they need no separate approval; a failing step
|
|
@@ -57,6 +57,18 @@ Use `task debug --task <id> --debug-action <action>`:
|
|
|
57
57
|
new reference and `reason`.
|
|
58
58
|
- `strategy --payload-file <json>` records a different `from`/`to` method,
|
|
59
59
|
substantive `reason` and discriminating `reference`. Only one switch is allowed.
|
|
60
|
+
- `origin --payload-file <json>` declares where the bug came from once a
|
|
61
|
+
hypothesis is confirmed: `{"kind": "escape-of", "commit": "<commit>", "label":
|
|
62
|
+
"<earliest gate>"}` (optional `"receipt"`) or `{"kind": "pre-existing" |
|
|
63
|
+
"external", "reason": "..."}`. It revises the contract, so re-verify and
|
|
64
|
+
re-record the confirmed hypothesis before review; finish refuses
|
|
65
|
+
`ESCAPE_ORIGIN_MISSING` without it. `escape-of` prints the link state and
|
|
66
|
+
`git blame` hints and writes `.agent-ops-escapes/<task-id>.json`, which is
|
|
67
|
+
committed with the fix (see `EVIDENCE-ESCAPE-001`). A surface label also
|
|
68
|
+
needs a tracked `.agent-ops-escapes/rules/<rule-id>.json` rule that replays
|
|
69
|
+
at the escaped commit, and a criteria/verify label needs `"promote":
|
|
70
|
+
"<runnerId:checkId>"`; finish refuses `ESCAPE_RULE_UNPROVEN` otherwise (see
|
|
71
|
+
`EVIDENCE-ESCAPE-002`).
|
|
60
72
|
- `checkpoint --session <id>` saves the current bound source/policy/stop cause.
|
|
61
73
|
- `resume --session <id>` recovers a matching owned worktree checkpoint. Mismatches
|
|
62
74
|
require human confirmation and remain preserved; this command cannot waive them.
|
|
@@ -104,13 +104,22 @@ Every new task MUST record a surface audit of where else its change lands before
|
|
|
104
104
|
such as `order:`, `user_`, `menu.`); compound identifiers (snake_case,
|
|
105
105
|
camelCase or containing digits, so prose words are not queries) and string
|
|
106
106
|
literals from added and removed lines (at least 6 characters; skipped as too
|
|
107
|
-
generic above
|
|
107
|
+
generic above 10 file hits, changed files included). Every hit outside the diff and the surface lists stops review.
|
|
108
|
+
- A derived query (diff token, template fragment or hunk-header name) whose hits
|
|
109
|
+
are incidental may be dismissed once in the surface's optional `dismissed`
|
|
110
|
+
list (`{query, reason}`, reason at most 256 characters, "out of scope" is
|
|
111
|
+
not a reason) instead of one exclusion per file. Recorded axis queries and
|
|
112
|
+
escape-rule hits cannot be dismissed, nor can a query reaching fewer than 4
|
|
113
|
+
files outside the diff (the likeliest sibling signal); at most 128
|
|
114
|
+
dismissals. Reviewers see each dismissal with its hit count, flagged when it
|
|
115
|
+
hid a file an earlier `SURFACE_UNCLASSIFIED` block named.
|
|
108
116
|
Results are cached by HEAD, base, diff, query set and config hash.
|
|
109
117
|
- Review returns NOT_RUN with: `SURFACE_MISSING` (no or unparseable surface;
|
|
110
118
|
also refused at create for non-DEBUG tasks), `SURFACE_UNCLASSIFIED`,
|
|
111
119
|
`SURFACE_QUERY_TOO_BROAD` (one recorded query over 200 files),
|
|
112
120
|
`SURFACE_EXCLUSION_TOO_BROAD` (one glob over 10 files, or any file in the
|
|
113
|
-
diff), `
|
|
121
|
+
diff), `SURFACE_DISMISSAL_TOO_BROAD` (over 128 dismissals),
|
|
122
|
+
`SURFACE_DISMISSAL_INVALID` (a recorded or rare query), `SCENARIO_MISSING`, `SURFACE_BUDGET_EXCEEDED` (120 s total, never
|
|
114
123
|
silently skipped) or `SURFACE_UNASSIGNED` (children's covers differ from the
|
|
115
124
|
parent's included list; checked by every review, including advance and batch).
|
|
116
125
|
- A surface revision is a contract revision: fresh evidence and two fresh
|
|
@@ -122,6 +131,95 @@ Every new task MUST record a surface audit of where else its change lands before
|
|
|
122
131
|
both rounds' residual risks and non-blocking findings, references it in the
|
|
123
132
|
git note and prints the residual risks.
|
|
124
133
|
|
|
134
|
+
## EVIDENCE-ESCAPE-001
|
|
135
|
+
|
|
136
|
+
Every DEBUG fix MUST declare where its bug came from before review, and a declared escape MUST be linked, labelled and recorded mechanically.
|
|
137
|
+
|
|
138
|
+
- Trigger: A DEBUG hypothesis is confirmed, or a normal task fixes a bug that passed earlier gates.
|
|
139
|
+
- Action: Declare `origin` with `task debug --debug-action origin --payload-file <json>`: `{"kind": "escape-of", "commit": "<commit>", "label": "<label>"}` (optional `"receipt"`), or `{"kind": "pre-existing" | "external", "reason": "..."}`. A normal task may opt in with `task create|revise --escape-of <commit> --escape-label <label> [--escape-receipt <id>]`. Commit the `.agent-ops-escapes/<task-id>.json` record with the fix.
|
|
140
|
+
- Evidence: `origin` is bound into the task contract hash; finish, `task complete` and the completion gate refuse a DEBUG task created after this rule without it as `ESCAPE_ORIGIN_MISSING`; both reviewers receive the link state, labels, facts and residual risks under the `escape-label` criterion.
|
|
141
|
+
- Positive: `Hypothesis confirmed; origin escape-of 864dbbe2 label surface; replay marks the missed page surface-unseen; the record is committed with the fix.`
|
|
142
|
+
- Negative: `Finish a DEBUG fix without an origin, or relabel a surface-excluded miss as review because the reviewer "should have noticed".`
|
|
143
|
+
|
|
144
|
+
- The declaration comes after a confirmed hypothesis (`ESCAPE_ORIGIN_EARLY`
|
|
145
|
+
otherwise), inside the DEBUG budget. It is a contract revision: re-verify,
|
|
146
|
+
re-record the confirmed hypothesis with the new evidence, then review.
|
|
147
|
+
Re-declaring replaces the record. The output lists `git blame` hint commits
|
|
148
|
+
for lines the fix changed or removed; blame is a hint only, because an
|
|
149
|
+
omission bug lives in lines nobody wrote.
|
|
150
|
+
- Finish receipts stay local in the Git common directory. Schema 2 records
|
|
151
|
+
every commit in target..candidate and a per-file diff fingerprint (blob ids
|
|
152
|
+
and hunk positions dropped, so a squash merge keeps it); schema 1 receipts
|
|
153
|
+
still load and link by exact head only.
|
|
154
|
+
- Link states are counted separately: `gated` (a receipt matched),
|
|
155
|
+
`unresolved` with a reason (`receipt-not-local` when a Git note or
|
|
156
|
+
`--escape-receipt` names a receipt this machine lacks, `ambiguous-receipt`,
|
|
157
|
+
`link-unavailable`) and `ungated` (no receipt and no note). Resolution
|
|
158
|
+
order: `--escape-receipt` override, the commit inside a receipt's recorded
|
|
159
|
+
range (schema 1: exact head), per-file fingerprint match, then unresolved or
|
|
160
|
+
ungated.
|
|
161
|
+
- Label: one primary label, the earliest gate that could have caught the
|
|
162
|
+
escape — `requirement`, `surface`, `scenario`, `criteria`, `verify`,
|
|
163
|
+
`review` — or `flagged` (a residual risk or finding already said it) or
|
|
164
|
+
`out-of-model`. The agent proposes it; `git grep -F` replay of the linked
|
|
165
|
+
receipt's recorded surface queries at its candidate marks each file the fix
|
|
166
|
+
touched as included, excluded, hit, unseen or absent, and `surface-excluded`
|
|
167
|
+
or `surface-unseen` overrides any later label. The two reviewers adjudicate.
|
|
168
|
+
- The ledger is git-tracked and data only (commits, paths, labels, reasons,
|
|
169
|
+
counts; never file contents or transcripts), one file per task so parallel
|
|
170
|
+
worktrees never conflict. Gate catches (`SURFACE_*` and `SCENARIO_MISSING`
|
|
171
|
+
review blocks, review FAIL, verify red to green) append advisory local-log
|
|
172
|
+
`gate-catch` events with only the code and task id; logging fails open,
|
|
173
|
+
while the origin requirement fails closed.
|
|
174
|
+
- Scope R1 records. Rules learned from escapes, replay proof, backtests,
|
|
175
|
+
`--escape-fix` and an `agent-ops escapes` report are R2
|
|
176
|
+
([EVIDENCE-ESCAPE-002](#evidence-escape-002)).
|
|
177
|
+
|
|
178
|
+
## EVIDENCE-ESCAPE-002
|
|
179
|
+
|
|
180
|
+
A surface escape MUST become a proven, tracked rule before its fix finishes, and learned rules MUST only tighten the surface gate.
|
|
181
|
+
|
|
182
|
+
- Trigger: A declared escape-of origin (EVIDENCE-ESCAPE-001), or a past escape backfilled with `task create --escape-of <commit> --escape-label <label> --escape-fix <fix-commit>`.
|
|
183
|
+
- Action: For a `surface`, `surface-unseen` or `surface-excluded` label, commit a rule `.agent-ops-escapes/rules/<rule-id>.json` shaped `{"id", "escape", "label", "trigger": {"paths", "tokens"}, "expand": {"paths", "queries"}, "noExclude"}` and re-declare the origin. For `criteria` or `verify`, name the fixing task's red-to-green check with `--escape-promote <runnerId:checkId>` (DEBUG payload `"promote"`). Other labels record a lesson.
|
|
184
|
+
- Evidence: Finish, `task complete` and the completion gate re-run the proof and refuse `ESCAPE_RULE_UNPROVEN`, `ESCAPE_RULE_TOO_BROAD` or `SURFACE_BUDGET_EXCEEDED`; the ledger record stores the proving rule, its content hash and its backtest fires, commits and rate.
|
|
185
|
+
- Positive: `Escape surface-unseen; rule image-size-siblings fires on the escaped commit, its query hits both missed sibling pages there, and fires on 2 of the last 200 commits.`
|
|
186
|
+
- Negative: `Finish a surface escape whose rule never fires on the escaped commit, or add a rule field that excludes paths.`
|
|
187
|
+
|
|
188
|
+
- Rules are data only and need no trust: triggers and expands become argv
|
|
189
|
+
`git grep -F` queries and path globs, never shell or regex. The strict schema
|
|
190
|
+
has no field that removes a query, adds an exclusion or relaxes any gate.
|
|
191
|
+
An invalid rule fails closed as review NOT_RUN `ESCAPE_RULE_INVALID`; a
|
|
192
|
+
symlinked or non-regular rules directory or file as `ESCAPE_RULE_UNSAFE`.
|
|
193
|
+
- Surface gate: a rule fires only when the task diff matches a trigger path
|
|
194
|
+
glob or a changed line contains a trigger token. Every expand hit of a fired
|
|
195
|
+
rule must be in the diff, included, UNKNOWN or excluded, else
|
|
196
|
+
`SURFACE_UNCLASSIFIED`; with `noExclude` an excluded hit is
|
|
197
|
+
`ESCAPE_RULE_NO_EXCLUDE`. Fires and included/excluded counts per rule go to
|
|
198
|
+
local-log `escape-rule` events (fail-open).
|
|
199
|
+
- Proof: the rule must name the escaped commit, fire on that commit's own diff
|
|
200
|
+
and, replayed at that commit, hit every missed path that existed there.
|
|
201
|
+
Missed paths come from the fix commit with `--escape-fix`, otherwise from
|
|
202
|
+
the task diff. A `criteria`/`verify` escape needs its promoted check to be a
|
|
203
|
+
`redCheckIds` entry of a proven behavioral criterion of the fixing task
|
|
204
|
+
(red baseline, green candidate); the output prints a config proposal for a
|
|
205
|
+
new required verifier, and agent-ops never writes `.agent-ops/config.json`.
|
|
206
|
+
`scenario`, `review`, `requirement`, `flagged` and `out-of-model` record an
|
|
207
|
+
unproven lesson with a reason and do not block. No review or test ever runs
|
|
208
|
+
at the escaped commit.
|
|
209
|
+
- Backtest: the trigger is matched against the diffs of the last 200
|
|
210
|
+
first-parent commits in one argv `git log`; a fire rate above 30% is
|
|
211
|
+
`ESCAPE_RULE_TOO_BROAD`. It is time-budgeted (over budget is
|
|
212
|
+
`SURFACE_BUDGET_EXCEEDED`, never a silent pass) and cached by HEAD and rule
|
|
213
|
+
hash. Editing a proving rule makes the ledger record stale until re-declared.
|
|
214
|
+
- `agent-ops escapes [--json]` is a read-only, fail-open report: misses per
|
|
215
|
+
label from the ledger, `gate-catch` codes from the local log, origin mix,
|
|
216
|
+
link-state mix with unresolved reasons, and per-rule fire/included/excluded
|
|
217
|
+
counts with time windows, as raw counts with n and `insufficient-data`
|
|
218
|
+
when n < 10. Rules with at least 10 fires and no included hit are listed as
|
|
219
|
+
retirement candidates; removal is always a reviewed commit, never automatic.
|
|
220
|
+
- R2 supersedes the phase-2 `surface.aliases` config (now tracked rules) and
|
|
221
|
+
`surface stats` (now `agent-ops escapes`).
|
|
222
|
+
|
|
125
223
|
## EVIDENCE-AGGREGATE-001
|
|
126
224
|
|
|
127
225
|
The verifier MUST return FAIL when a criterion is missing, duplicated, unknown, or has empty evidence.
|
|
@@ -99,7 +99,13 @@ diagnostic。
|
|
|
99
99
|
Capability check 與模型啟動進度都寫到 stderr,包括 `--json` 模式;stdout
|
|
100
100
|
仍只有最終 JSON envelope,且不會串流 reviewer 原始輸出。SIGINT 或 SIGTERM
|
|
101
101
|
會終止目前 reviewer 的完整 process tree、不進入 fallback,也不寫入
|
|
102
|
-
attestation。整條 chain 逾時時回報 `timeout`,不會誤報 `missing-cli
|
|
102
|
+
attestation。整條 chain 逾時時回報 `timeout`,不會誤報 `missing-cli`。每輪
|
|
103
|
+
reviewer 預設最多 30 分鐘(可用 `reviewRoles[].timeoutMs` 覆寫);在 stall 視窗內
|
|
104
|
+
完全沒有輸出的 reviewer 仍會提早停止。
|
|
105
|
+
|
|
106
|
+
Acceptance runner 的步驟若未設定自己的 `timeoutMs`,`setup` 為 10 分鐘,
|
|
107
|
+
`build`、`testBuild` 與 runner 本身各 5 分鐘,git checkout 為 1 分鐘。逾時的步驟
|
|
108
|
+
回報為 `UNKNOWN`,並指出是哪個步驟、上限多少,以及要調整哪個 `timeoutMs` 欄位。
|
|
103
109
|
|
|
104
110
|
task ID 必須明確指定,且永遠使用該 task 的原始 criteria;不能用 session fallback
|
|
105
111
|
或 criterion filter 取代需求。review 會在 adversarial session 前、以及寫入證據前
|
|
@@ -174,6 +180,13 @@ fingerprint,因此兩個對話修改同一個 checkout 會互相作廢對方
|
|
|
174
180
|
`.worktreeinclude`(gitignore 語法,例如 `.env` 或 `local.properties`)比對到
|
|
175
181
|
的檔案;Git 已 checkout 的檔案一律不覆寫。只有主 checkout 已 trusted 且
|
|
176
182
|
worktree 的 effective config 完全相同時才繼承 trust。
|
|
183
|
+
- 在主 checkout 核准的 `agent-ops update` 會把新 config 複製到副本仍是先前已
|
|
184
|
+
信任版本的每個 session worktree,並以相同指令信任它;確認前 plan 會列出這些
|
|
185
|
+
session worktree。手動修改過的 worktree config 不會被覆寫,run 的 worktree 也
|
|
186
|
+
保留自己的授權。舊 config 下建立的 task 不會自動重新綁定:
|
|
187
|
+
review 會列出可重新綁定的
|
|
188
|
+
`agent-ops task revise --task <id> --expected-contract <hash> --reason <text>`
|
|
189
|
+
(criteria 與 surface 不變),之後再重新 verify。
|
|
177
190
|
- `setup` 指令在新 worktree 中執行(預設 timeout 十分鐘),處理 Git 不會帶過去
|
|
178
191
|
的東西,例如已安裝的相依套件。它屬於 trusted config 的一部分,不需另外核准;
|
|
179
192
|
任一步驟失敗會移除該 worktree。
|
|
@@ -49,6 +49,16 @@ Claude、Codex 與 agy 都使用可信 native session lifecycle,在 `UserPromp
|
|
|
49
49
|
才封存 UNKNOWN environment receipt;這段經過時間仍會保留,不會退還不明時間。
|
|
50
50
|
測試材料修改不算修復輪;兩輪停滯需要一次實質策略切換,再兩輪停滯停止。
|
|
51
51
|
|
|
52
|
+
假設確認後以 `origin --payload-file <json>` 宣告 bug 來源:`{"kind": "escape-of",
|
|
53
|
+
"commit": "<commit>", "label": "<最早關卡>"}`(可選 `"receipt"`),或 `{"kind":
|
|
54
|
+
"pre-existing" | "external", "reason": "..."}`。它會修訂 contract,因此 review 前要重新
|
|
55
|
+
verify 並重新記錄已確認的假設;沒有它時 finish 以 `ESCAPE_ORIGIN_MISSING` 拒絕。
|
|
56
|
+
`escape-of` 會印出連結狀態與 `git blame` 提示,並寫入 `.agent-ops-escapes/<task-id>.json`,
|
|
57
|
+
與修復一起 commit(見 `EVIDENCE-ESCAPE-001`)。surface 標記還需要一條在被逃逸 commit
|
|
58
|
+
重放成立、受追蹤的 `.agent-ops-escapes/rules/<rule-id>.json` 規則;criteria/verify 標記需要
|
|
59
|
+
`"promote": "<runnerId:checkId>"`;否則 finish 以 `ESCAPE_RULE_UNPROVEN` 拒絕(見
|
|
60
|
+
`EVIDENCE-ESCAPE-002`)。
|
|
61
|
+
|
|
52
62
|
checkpoint 保存 task/session/source/contract/config/cause;resume 只接受同一
|
|
53
63
|
擁有者 worktree 的匹配 checkpoint,保留 dirty 內容及預算。失配需要人類確認。
|
|
54
64
|
時間耗盡後明確「繼續」可透過 `extend --event-id latest` 讀取原生人類事件,
|
|
@@ -73,13 +73,20 @@ English source version: 2026-10-06. Revalidate: when the English specification c
|
|
|
73
73
|
不以 task 文字組 regex),由便宜的來源先跑:記錄的查詢;diff hunk header 的外層
|
|
74
74
|
函式名;template literal 固定片段(至少 4 字元且以分隔符界定,例如 `order:`、
|
|
75
75
|
`user_`、`menu.`);新增與刪除行的複合識別字(snake_case、camelCase 或含數字,
|
|
76
|
-
因此一般文字單字不會成為查詢)與字串常值(至少 6 字元;命中超過
|
|
77
|
-
|
|
76
|
+
因此一般文字單字不會成為查詢)與字串常值(至少 6 字元;命中超過 10 個檔案〈含
|
|
77
|
+
變更檔案〉視為太通用而略過)。diff 與 surface 清單之外的任何命中都會擋下 review。
|
|
78
|
+
- 衍生查詢(diff 字詞、template 片段或 hunk header 函式名)的命中若只是巧合,可在
|
|
79
|
+
surface 選用的 `dismissed` 清單中以一筆 `{query, reason}` 排除(理由至多 256
|
|
80
|
+
字元,「out of scope」不是理由),不必逐檔排除。記錄的軸查詢與 escape rule 命中
|
|
81
|
+
不能排除;diff 以外命中少於 4 個檔案的查詢(最可能是兄弟頁面線索)也不能排除;
|
|
82
|
+
至多 128 筆。Reviewer 會看到每筆排除與其命中數,若它隱藏了先前
|
|
83
|
+
`SURFACE_UNCLASSIFIED` 阻擋所列的檔案則會標示出來。
|
|
78
84
|
結果以 HEAD、base、diff、查詢集合與 config hash 快取。
|
|
79
85
|
- Review 以 NOT_RUN 回報:`SURFACE_MISSING`(沒有或無法解析的 surface;非 DEBUG
|
|
80
86
|
task 建立時也會拒絕)、`SURFACE_UNCLASSIFIED`、`SURFACE_QUERY_TOO_BROAD`(單一
|
|
81
87
|
記錄查詢超過 200 個檔案)、`SURFACE_EXCLUSION_TOO_BROAD`(單一 glob 超過 10 個
|
|
82
|
-
檔案,或包含 diff 內的檔案)、`
|
|
88
|
+
檔案,或包含 diff 內的檔案)、`SURFACE_DISMISSAL_TOO_BROAD`(超過 128 筆排除)、
|
|
89
|
+
`SURFACE_DISMISSAL_INVALID`(記錄的查詢或稀有查詢)、`SCENARIO_MISSING`、`SURFACE_BUDGET_EXCEEDED`
|
|
83
90
|
(總計 120 秒,絕不默默略過)或 `SURFACE_UNASSIGNED`(子 task covers 與父 task
|
|
84
91
|
included 不一致;每次 review 都檢查,包含 advance 與 batch)。
|
|
85
92
|
- 修訂 surface 等同修訂 contract:需要新的證據與兩次新的 review。在
|
|
@@ -90,6 +97,78 @@ English source version: 2026-10-06. Revalidate: when the English specification c
|
|
|
90
97
|
- 一般 `worktree finish` 也會寫入與 advance 相同的本機 receipt,包含兩輪的 residual
|
|
91
98
|
risks 與 non-blocking findings,在 git note 引用它並印出 residual risks。
|
|
92
99
|
|
|
100
|
+
## EVIDENCE-ESCAPE-001
|
|
101
|
+
|
|
102
|
+
每個 DEBUG 修復 MUST 在 review 前宣告 bug 的來源;宣告的逃逸 MUST 以機械方式連結、標記並記錄。
|
|
103
|
+
|
|
104
|
+
- Trigger: DEBUG 假設已確認,或一般 task 修復一個通過了先前關卡的 bug。
|
|
105
|
+
- Action: 以 `task debug --debug-action origin --payload-file <json>` 宣告 `origin`:`{"kind": "escape-of", "commit": "<commit>", "label": "<label>"}`(可選 `"receipt"`),或 `{"kind": "pre-existing" | "external", "reason": "..."}`。一般 task 可用 `task create|revise --escape-of <commit> --escape-label <label> [--escape-receipt <id>]` 選擇加入。把 `.agent-ops-escapes/<task-id>.json` 記錄與修復一起 commit。
|
|
106
|
+
- Evidence: `origin` 綁進 task contract hash;本規則上線後建立的 DEBUG task 沒有它時,finish、`task complete` 與 completion gate 以 `ESCAPE_ORIGIN_MISSING` 拒絕;兩位 reviewer 在 `escape-label` 條件下收到連結狀態、標記、事實與 residual risks。
|
|
107
|
+
- Positive: `假設已確認;origin escape-of 864dbbe2 label surface;重放把漏掉的頁面標為 surface-unseen;記錄與修復一起 commit。`
|
|
108
|
+
- Negative: `DEBUG 修復沒有 origin 就 finish,或把 surface-excluded 的漏網改標成 review,理由是 reviewer「應該注意到」。`
|
|
109
|
+
|
|
110
|
+
- 宣告必須在假設確認之後(否則 `ESCAPE_ORIGIN_EARLY`),並計入 DEBUG 預算。它是一次
|
|
111
|
+
contract revision:重新 verify、以新證據重新記錄已確認的假設,然後 review。重新宣告會
|
|
112
|
+
取代記錄。輸出會列出修復所改或刪行的 `git blame` 提示 commit;blame 只是提示,因為遺漏
|
|
113
|
+
型 bug 存在於沒人寫過的行。
|
|
114
|
+
- finish receipt 留在本機 Git common directory。Schema 2 記錄 target..candidate 的每個
|
|
115
|
+
commit 與逐檔 diff fingerprint(去掉 blob id 與 hunk 位置,squash merge 後仍相同);
|
|
116
|
+
schema 1 receipt 仍可載入,但只以 exact head 連結。
|
|
117
|
+
- 連結狀態分開計數:`gated`(找到 receipt)、帶理由的 `unresolved`(Git note 或
|
|
118
|
+
`--escape-receipt` 指向本機沒有的 receipt 時為 `receipt-not-local`,另有
|
|
119
|
+
`ambiguous-receipt`、`link-unavailable`)與 `ungated`(沒有 receipt 也沒有 note)。
|
|
120
|
+
解析順序:`--escape-receipt` 覆寫、commit 位於某 receipt 記錄的範圍內(schema 1:exact
|
|
121
|
+
head)、逐檔 fingerprint 相符,最後才是 unresolved 或 ungated。
|
|
122
|
+
- 標記:單一主要標記,取最早能抓到此逃逸的關卡 —— `requirement`、`surface`、
|
|
123
|
+
`scenario`、`criteria`、`verify`、`review` —— 或 `flagged`(residual risk 或 finding 已
|
|
124
|
+
提過)或 `out-of-model`。由 agent 提議;以 `git grep -F` 在被連結 receipt 的 candidate
|
|
125
|
+
上重放其記錄的 surface 查詢,把修復碰到的每個檔標為 included、excluded、hit、unseen 或
|
|
126
|
+
absent,`surface-excluded` 或 `surface-unseen` 會覆寫任何較晚的標記。兩位 reviewer 裁決。
|
|
127
|
+
- ledger 受 git 追蹤且只含資料(commit、路徑、標記、理由、計數;絕不含檔案內容或對話紀錄),
|
|
128
|
+
每個 task 一個檔案,平行 worktree 不會衝突。關卡攔截(`SURFACE_*` 與 `SCENARIO_MISSING`
|
|
129
|
+
review 阻擋、review FAIL、verify 由紅轉綠)會附加僅含 code 與 task id 的參考性 local-log
|
|
130
|
+
`gate-catch` 事件;記錄失敗時 fail-open,origin 要求則 fail-closed。
|
|
131
|
+
- 範圍 R1 只負責記錄。由逃逸學到的規則、重放證明、回測、`--escape-fix` 與
|
|
132
|
+
`agent-ops escapes` 報表屬於 R2([EVIDENCE-ESCAPE-002](#evidence-escape-002))。
|
|
133
|
+
|
|
134
|
+
## EVIDENCE-ESCAPE-002
|
|
135
|
+
|
|
136
|
+
surface 逃逸 MUST 在修復 finish 前變成已證明、受追蹤的規則;學到的規則 MUST 只能收緊 surface gate。
|
|
137
|
+
|
|
138
|
+
- Trigger: 已宣告的 escape-of origin(EVIDENCE-ESCAPE-001),或以 `task create --escape-of <commit> --escape-label <label> --escape-fix <fix-commit>` 回填的過去逃逸。
|
|
139
|
+
- Action: 標記為 `surface`、`surface-unseen` 或 `surface-excluded` 時,commit 一條規則 `.agent-ops-escapes/rules/<rule-id>.json`,形狀為 `{"id", "escape", "label", "trigger": {"paths", "tokens"}, "expand": {"paths", "queries"}, "noExclude"}`,並重新宣告 origin。`criteria` 或 `verify` 以 `--escape-promote <runnerId:checkId>`(DEBUG payload `"promote"`)指名修復 task 由紅轉綠的 check。其他標記記錄為 lesson。
|
|
140
|
+
- Evidence: finish、`task complete` 與 completion gate 會重跑證明,並以 `ESCAPE_RULE_UNPROVEN`、`ESCAPE_RULE_TOO_BROAD` 或 `SURFACE_BUDGET_EXCEEDED` 拒絕;ledger 記錄保存證明用規則、其內容 hash 與回測的 fires、commits 與比率。
|
|
141
|
+
- Positive: `逃逸 surface-unseen;規則 image-size-siblings 在被逃逸 commit 上觸發,其查詢在該 commit 命中兩個漏掉的兄弟頁面,且在最近 200 個 commit 中只觸發 2 次。`
|
|
142
|
+
- Negative: `規則從未在被逃逸 commit 上觸發就 finish surface 逃逸,或新增可排除路徑的規則欄位。`
|
|
143
|
+
|
|
144
|
+
- 規則只含資料且不需要 trust:trigger 與 expand 變成 argv `git grep -F` 查詢與路徑
|
|
145
|
+
glob,絕不經過 shell 或 regex。嚴格 schema 沒有任何能移除查詢、新增排除或放寬關卡的
|
|
146
|
+
欄位。無效規則以 review NOT_RUN `ESCAPE_RULE_INVALID` fail-closed;symlink 或非一般
|
|
147
|
+
檔案的規則目錄或檔案則為 `ESCAPE_RULE_UNSAFE`。
|
|
148
|
+
- Surface gate:只有 task diff 符合 trigger 路徑 glob,或某個變更行包含 trigger token
|
|
149
|
+
時,規則才會觸發。已觸發規則的每個 expand 命中都必須位於 diff、included、UNKNOWN 或
|
|
150
|
+
excluded,否則為 `SURFACE_UNCLASSIFIED`;有 `noExclude` 時,被排除的命中為
|
|
151
|
+
`ESCAPE_RULE_NO_EXCLUDE`。每條規則的觸發與 included/excluded 計數寫入 local-log
|
|
152
|
+
`escape-rule` 事件(fail-open)。
|
|
153
|
+
- 證明:規則必須指名被逃逸 commit、在該 commit 自身的 diff 上觸發,並在該 commit 重放時
|
|
154
|
+
命中當時已存在的每個漏掉路徑。有 `--escape-fix` 時漏掉路徑取自 fix commit,否則取自
|
|
155
|
+
task diff。`criteria`/`verify` 逃逸需要其 promoted check 是修復 task 某個已證明
|
|
156
|
+
behavioral 條件的 `redCheckIds` 項目(baseline 紅、candidate 綠);輸出會印出新增
|
|
157
|
+
required verifier 的 config 提案,agent-ops 絕不寫入 `.agent-ops/config.json`。
|
|
158
|
+
`scenario`、`review`、`requirement`、`flagged` 與 `out-of-model` 記錄帶理由的未證明
|
|
159
|
+
lesson,不阻擋。絕不在被逃逸 commit 上執行 review 或測試。
|
|
160
|
+
- 回測:以一次 argv `git log` 把 trigger 比對最近 200 個 first-parent commit 的 diff;
|
|
161
|
+
觸發比率超過 30% 為 `ESCAPE_RULE_TOO_BROAD`。回測有時間預算(超出為
|
|
162
|
+
`SURFACE_BUDGET_EXCEEDED`,絕不默默通過),並以 HEAD 與規則 hash 快取。編輯證明用規則
|
|
163
|
+
會讓 ledger 記錄過期,直到重新宣告。
|
|
164
|
+
- `agent-ops escapes [--json]` 是唯讀、fail-open 的報表:來自 ledger 的各標記漏網數、
|
|
165
|
+
來自 local log 的 `gate-catch` code、origin 組成、帶 unresolved 理由的連結狀態組成,
|
|
166
|
+
以及每條規則的觸發/included/excluded 計數與時間窗,皆為附 n 的原始計數,n < 10 時標為
|
|
167
|
+
`insufficient-data`。至少觸發 10 次且從無 included 命中的規則列為退役候選;移除一律
|
|
168
|
+
經過審查的 commit,絕不自動。
|
|
169
|
+
- R2 取代 phase-2 的 `surface.aliases` 設定(改為受追蹤規則)與 `surface stats`
|
|
170
|
+
(改為 `agent-ops escapes`)。
|
|
171
|
+
|
|
93
172
|
## EVIDENCE-AGGREGATE-001
|
|
94
173
|
|
|
95
174
|
驗證器 MUST 在條件缺少、重複、未知或證據為空時回傳 FAIL。
|
package/package.json
CHANGED