@kylecheng3146/agent-ops 0.8.6 → 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 +56 -4
- package/dist/packages/cli/src/bin.js +10 -3
- package/dist/packages/cli/src/cli.js +18 -2
- package/dist/packages/cli/src/commands/escapes.js +109 -0
- package/dist/packages/cli/src/commands/parallel.js +5 -0
- package/dist/packages/cli/src/commands/review-show.js +1 -1
- package/dist/packages/cli/src/commands/review.js +145 -4
- package/dist/packages/cli/src/commands/task.js +58 -7
- 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 +2 -2
- package/dist/packages/cli/src/run-entry.js +7 -5
- 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 +61 -1
- package/dist/runtime/src/parallel/finish.js +20 -3
- package/dist/runtime/src/parallel/manage.js +51 -1
- package/dist/runtime/src/parallel/receipt.js +9 -1
- package/dist/runtime/src/review/execute.js +12 -6
- package/dist/runtime/src/review/packet.js +15 -1
- package/dist/runtime/src/review/probe.js +3 -2
- package/dist/runtime/src/review/runner.js +45 -0
- package/dist/runtime/src/review/slots.js +1 -1
- package/dist/runtime/src/run/planning.js +5 -2
- package/dist/runtime/src/schema/validate.js +17 -1
- package/dist/runtime/src/surface/discovery.js +298 -0
- package/dist/runtime/src/surface/schema.js +168 -0
- package/dist/runtime/src/task/completion.js +20 -0
- package/dist/runtime/src/task/contract.js +5 -1
- package/dist/runtime/src/task/debug.js +27 -1
- package/dist/runtime/src/task/service.js +72 -5
- 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 +152 -0
- package/docs/en/spec/judgment.md +7 -0
- package/docs/en/spec/loop-engineering.md +2 -2
- package/docs/en/spec/review.md +5 -0
- 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 +127 -0
- package/docs/zh-TW/spec/judgment.md +6 -0
- package/docs/zh-TW/spec/loop-engineering.md +2 -2
- package/docs/zh-TW/spec/review.md +1 -0
- package/package.json +1 -1
- package/schemas/task.schema.json +14 -0
|
@@ -5,6 +5,8 @@ import { isPassingVerificationEvidence } from "../verify/evidence.js";
|
|
|
5
5
|
import { taskContractHash } from "./contract.js";
|
|
6
6
|
import { acceptanceCoverage, coverageDigest } from "../verify/acceptance-coverage.js";
|
|
7
7
|
import { loadDebugProof, debugProofDigest, debugRemainingMs } from "./debug-state.js";
|
|
8
|
+
import { escapeCompletionProblem } from "../escape/link.js";
|
|
9
|
+
import { RULES_DIRECTORY } from "../escape/rules.js";
|
|
8
10
|
export async function checkTaskCompletionEvidence(stored, options) {
|
|
9
11
|
const debug = stored.task.debug === undefined ? undefined : await loadDebugProof(options.root, stored.task.debug.rootTaskId);
|
|
10
12
|
if (debug?.control?.paused === true || debug?.control?.mode === "diagnose")
|
|
@@ -28,6 +30,24 @@ export async function checkTaskCompletionEvidence(stored, options) {
|
|
|
28
30
|
attestation?.debugProofs?.[stored.task.id] !== debugProofDigest(debug))
|
|
29
31
|
return { status: "FAIL", code: "TASK_DEBUG_PROOF_REQUIRED", remedy: "Confirm the original cause with current discriminating regression proof, then obtain two fresh independent reviews bound to that debug proof." };
|
|
30
32
|
}
|
|
33
|
+
// Fails closed: the Escape Loop needs every DEBUG fix to say where the bug came from.
|
|
34
|
+
if (stored.task.originRequired === true && stored.task.origin === undefined)
|
|
35
|
+
return { status: "FAIL", code: "ESCAPE_ORIGIN_MISSING", remedy: "Declare the issue origin with task debug --debug-action origin --payload-file <json> (escape-of <commit> + label, pre-existing or external + reason) after the hypothesis is confirmed, then re-verify, re-record the hypothesis and obtain review." };
|
|
36
|
+
// Escape Loop R2: a declared escape finishes only with its proof (rule replay + backtest, or a promoted red-to-green check).
|
|
37
|
+
const origin = stored.task.origin, base = debug?.baselineCommit ?? stored.createdSourceCommit;
|
|
38
|
+
if (origin?.kind === "escape-of" && base !== undefined) {
|
|
39
|
+
const escape = await escapeCompletionProblem({ root: options.root, taskId: stored.task.id, origin, base });
|
|
40
|
+
if (escape !== null && "code" in escape)
|
|
41
|
+
return { status: "FAIL", code: escape.code, remedy: `${escape.reason} Add or fix a tracked ${RULES_DIRECTORY}/<id>.json rule, re-declare the origin and commit the record.` };
|
|
42
|
+
if (escape !== null) {
|
|
43
|
+
const [runnerId, checkId] = escape.check.split(":");
|
|
44
|
+
const proven = new Set((await acceptanceCoverage(stored, options.config, options.sourceFingerprint, options.evidenceStore))
|
|
45
|
+
.filter(row => row.status === "proven").map(row => row.criterionId));
|
|
46
|
+
if (!stored.task.criteria.some(c => proven.has(c.id) && c.acceptance?.mode === "behavioral" &&
|
|
47
|
+
c.acceptance.bindings.some(b => b.runnerId === runnerId && (b.redCheckIds ?? []).includes(checkId))))
|
|
48
|
+
return { status: "FAIL", code: "ESCAPE_RULE_UNPROVEN", remedy: `The promoted check ${escape.check} must be a redCheckId of a proven behavioral criterion of this task (red baseline, green candidate).` };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
31
51
|
if (attestation === null || attestation.taskId !== stored.task.id ||
|
|
32
52
|
((stored.task.criteria.some(c => c.acceptance !== undefined) || (stored.task.contractRevision ?? 0) > 0 || stored.task.goal !== undefined) &&
|
|
33
53
|
(attestation.schemaVersion !== 3 || attestation.taskContracts?.[stored.task.id] !== taskContractHash(stored.task))) ||
|
|
@@ -9,7 +9,11 @@ export function criterionContractHash(criterion) {
|
|
|
9
9
|
export function taskContractHash(task) {
|
|
10
10
|
return sha256(canonicalJson({ goalHash: goalHash(task), title: task.title,
|
|
11
11
|
intent: task.intent ?? null, revision: task.contractRevision ?? 0,
|
|
12
|
-
criteria: task.criteria.map(criterionContractHash), ...(task.debug === undefined ? {} : { debug: task.debug })
|
|
12
|
+
criteria: task.criteria.map(criterionContractHash), ...(task.debug === undefined ? {} : { debug: task.debug }),
|
|
13
|
+
...(task.surface === undefined ? {} : { surface: sha256(canonicalJson(task.surface)) }),
|
|
14
|
+
...(task.surfaceRequired === undefined ? {} : { surfaceRequired: true }),
|
|
15
|
+
...(task.origin === undefined ? {} : { origin: task.origin }),
|
|
16
|
+
...(task.originRequired === undefined ? {} : { originRequired: true }) }));
|
|
13
17
|
}
|
|
14
18
|
export function treeContractHash(tasks) {
|
|
15
19
|
return sha256(canonicalJson(tasks.map(task => ({ id: task.id, hash: taskContractHash(task) }))
|
|
@@ -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);
|
|
@@ -348,6 +371,9 @@ export class DebugService {
|
|
|
348
371
|
if (root.debug.receipts.length >= 256)
|
|
349
372
|
fail("TASK_DEBUG_HISTORY_LIMIT", "Execution history is full; retain it and report the blocker.");
|
|
350
373
|
const productDigest = await this.#productDigest(root.debug);
|
|
374
|
+
// EVIDENCE-SURFACE-001: intake may start without a surface, the first repair may not.
|
|
375
|
+
if (kind === "verify" && task.task.surfaceRequired === true && task.task.surface === undefined && productDigest !== sha256(canonicalJson([])))
|
|
376
|
+
fail("SURFACE_MISSING", "Add the surface audit with task revise --surface-file before verifying the first repair.");
|
|
351
377
|
const previousCandidate = root.debug.receipts.filter(r => r.kind === "verify" && (r.outcome === "PASS" || r.outcome === "FAIL")).at(-1)?.productDigest ?? sha256("[]");
|
|
352
378
|
const cause = debugStopCause(root.debug);
|
|
353
379
|
// Recovery reruns the original verifier; changed product content still needs fresh proof.
|
|
@@ -18,6 +18,10 @@ import { commitIdentity } from "../schema/acceptance.js";
|
|
|
18
18
|
import { createDebugState, DEBUG_DEFAULTS } from "./debug-state.js";
|
|
19
19
|
import { DebugService } from "./debug.js";
|
|
20
20
|
import { sameDebugText } from "./debug-intake.js";
|
|
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";
|
|
21
25
|
const SESSION_ID_PATTERN = /^[^\0\r\n]{1,256}$/u;
|
|
22
26
|
function defaultTaskId() {
|
|
23
27
|
return `task-${randomUUID()}`;
|
|
@@ -123,9 +127,13 @@ export class TaskService {
|
|
|
123
127
|
const criteria = structuredClone(input.criteria).map(criterion => ({ ...criterion,
|
|
124
128
|
...(criterion.acceptance === undefined ? {} : { acceptance: { ...criterion.acceptance,
|
|
125
129
|
baselineCommit: criterion.acceptance.baselineCommit ?? createdSourceCommit } }) }));
|
|
130
|
+
const surface = input.surface === undefined ? undefined : parseSurface(input.surface);
|
|
126
131
|
const task = {
|
|
127
132
|
schemaVersion: TASK_SCHEMA_VERSION,
|
|
128
133
|
id: this.#generateId(),
|
|
134
|
+
...(surface === undefined ? {} : { surface }),
|
|
135
|
+
...(input.requireSurface === true ? { surfaceRequired: true } : {}),
|
|
136
|
+
...(input.origin === undefined ? {} : { origin: structuredClone(input.origin) }),
|
|
129
137
|
title: input.title,
|
|
130
138
|
...(input.intent === undefined ? {} : { intent: input.intent }),
|
|
131
139
|
criteria,
|
|
@@ -145,6 +153,10 @@ export class TaskService {
|
|
|
145
153
|
throw taskError("TASK_DEBUG_DISABLED", "Task debug routing is explicitly disabled.");
|
|
146
154
|
debugSettings = config.features.debug ?? DEBUG_DEFAULTS;
|
|
147
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;
|
|
148
160
|
}
|
|
149
161
|
const validation = validateTask(task);
|
|
150
162
|
if (!validation.ok) {
|
|
@@ -186,6 +198,14 @@ export class TaskService {
|
|
|
186
198
|
validation.value.debug = structuredClone(parent.task.debug);
|
|
187
199
|
}
|
|
188
200
|
}
|
|
201
|
+
if (input.requireSurface === true && validation.value.debug === undefined) {
|
|
202
|
+
const parentSurface = state.tasks.find(record => record.task.id === input.parentTaskId)?.task.surface;
|
|
203
|
+
const surfacedParent = parentSurface !== undefined && !isCovers(parentSurface);
|
|
204
|
+
if (surface === undefined || isCovers(surface) !== surfacedParent)
|
|
205
|
+
throw taskError("SURFACE_MISSING", surfacedParent
|
|
206
|
+
? "A subtask of a surfaced parent declares --surface-file {\"covers\": [surface-id]}."
|
|
207
|
+
: "A new task needs --surface-file with the four-axis surface audit (EVIDENCE-SURFACE-001).");
|
|
208
|
+
}
|
|
189
209
|
const record = {
|
|
190
210
|
task: validation.value,
|
|
191
211
|
...(input.debug === undefined ? {} : { debug: createDebugState(task.id, createdSourceCommit, debugSettings, this.#completion.root) }),
|
|
@@ -250,8 +270,19 @@ export class TaskService {
|
|
|
250
270
|
throw taskError("TASK_NOT_ACTIVE", "Only active tasks can revise acceptance.");
|
|
251
271
|
if (taskContractHash(previous.task) !== input.expectedContractHash)
|
|
252
272
|
throw taskError("TASK_CONTRACT_CHANGED", "Task contract changed.");
|
|
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;
|
|
283
|
+
const surface = input.surface === undefined ? previous.task.surface : parseSurface(input.surface);
|
|
253
284
|
const baseline = input.baseline ?? previous.createdSourceCommit;
|
|
254
|
-
const criteria = structuredClone(input.criteria).map(criterion => {
|
|
285
|
+
const criteria = input.criteria === undefined ? structuredClone(previous.task.criteria) : structuredClone(input.criteria).map(criterion => {
|
|
255
286
|
if (criterion.acceptance === undefined)
|
|
256
287
|
return criterion;
|
|
257
288
|
const sha = criterion.acceptance.baselineCommit ?? baseline;
|
|
@@ -263,7 +294,8 @@ export class TaskService {
|
|
|
263
294
|
return { ...criterion, acceptance: { ...criterion.acceptance, baselineCommit: sha } };
|
|
264
295
|
});
|
|
265
296
|
const next = { ...previous.task, schemaVersion: TASK_SCHEMA_VERSION,
|
|
266
|
-
contractRevision: (previous.task.contractRevision ?? 0) + 1, criteria }
|
|
297
|
+
contractRevision: (previous.task.contractRevision ?? 0) + 1, criteria, ...(surface === undefined ? {} : { surface }),
|
|
298
|
+
...(input.origin === undefined ? {} : { origin: structuredClone(input.origin) }) };
|
|
267
299
|
const checked = validateTask(next);
|
|
268
300
|
if (!checked.ok)
|
|
269
301
|
throw taskError("TASK_INVALID", checked.errors[0]?.message ?? "Invalid criterion revision.");
|
|
@@ -278,6 +310,8 @@ export class TaskService {
|
|
|
278
310
|
throw taskError("TASK_INVALID", configured.errors[0]?.message ?? "Invalid binding.");
|
|
279
311
|
policyConfigHash = calculateConfigHash(config);
|
|
280
312
|
}
|
|
313
|
+
if (rebindOnly && policyConfigHash === previous.policyConfigHash)
|
|
314
|
+
throw taskError("TASK_REVISION_INVALID", "Nothing to revise: criteria, surface, origin and configuration are unchanged.");
|
|
281
315
|
if (criteria.some(c => c.acceptance !== undefined))
|
|
282
316
|
await this.#retainBaselines(next);
|
|
283
317
|
const now = assertTimestamp(this.#now());
|
|
@@ -665,7 +699,10 @@ export class TaskService {
|
|
|
665
699
|
async recordVerificationEvidence(snapshot, evidenceInput, sourceFingerprint) {
|
|
666
700
|
if (snapshot.status === "active") {
|
|
667
701
|
const result = await this.recordEvidence(snapshot.task.id, evidenceInput);
|
|
668
|
-
|
|
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);
|
|
669
706
|
return result;
|
|
670
707
|
}
|
|
671
708
|
const completion = this.#completion;
|
|
@@ -789,18 +826,48 @@ export class TaskService {
|
|
|
789
826
|
return structuredClone(advanced);
|
|
790
827
|
});
|
|
791
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). */
|
|
792
857
|
async clearFailure(taskId) {
|
|
793
858
|
const now = assertTimestamp(this.#now());
|
|
794
|
-
await this.#store.mutate((state) => {
|
|
859
|
+
return await this.#store.mutate((state) => {
|
|
795
860
|
const current = findTask(state, taskId);
|
|
796
861
|
if (current.failureFingerprint === null) {
|
|
797
|
-
return;
|
|
862
|
+
return false;
|
|
798
863
|
}
|
|
799
864
|
replaceTask(state, {
|
|
800
865
|
...current,
|
|
801
866
|
updatedAt: now,
|
|
802
867
|
failureFingerprint: null
|
|
803
868
|
});
|
|
869
|
+
// A source change mid-run is not a red the code fixed.
|
|
870
|
+
return current.failureFingerprint.commandId !== "source-snapshot";
|
|
804
871
|
});
|
|
805
872
|
}
|
|
806
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.
|
|
@@ -68,6 +68,158 @@ Each child worktree MUST record its modification intent before editing.
|
|
|
68
68
|
- Positive: `A child records its planned API change, commits it, and the final receipt includes that intent and integrated verification.`
|
|
69
69
|
- Negative: `A child edits first and supplies a retrospective one-line intent at delivery.`
|
|
70
70
|
|
|
71
|
+
## EVIDENCE-SURFACE-001
|
|
72
|
+
|
|
73
|
+
Every new task MUST record a surface audit of where else its change lands before writing criteria, and review MUST NOT run while discovery finds an unclassified place.
|
|
74
|
+
|
|
75
|
+
- Trigger: Creating a task (`task create`, a run plan, or before a DEBUG task's first repair), revising its surface, or reviewing it.
|
|
76
|
+
- Action: Search four axes with recorded fixed-string queries or `n/a: reason`, classify every hit as included, excluded (glob + reason) or UNKNOWN, give every surface row an observable `behavior`, and declare each shared contract compatible or breaking. Pass it with `task create|revise --surface-file <json>`.
|
|
77
|
+
- Evidence: The surface is stored on the task, bound into its contract hash with revision history, replayed by `git grep -F` before review, and handed to both reviewers under the `surface-coverage` criterion.
|
|
78
|
+
- Positive: `A diff token hits three copy-pasted sibling pages; the audit includes them with their behavior before review runs.`
|
|
79
|
+
- Negative: `Exclude src/** because it is "out of scope", or list only the files the diff already touches.`
|
|
80
|
+
|
|
81
|
+
- Axes: `dependents` (importers, callers, readers), `duplicates` (copied
|
|
82
|
+
markup, strings, logic that imports nothing), `otherPaths` (other routes,
|
|
83
|
+
modes, entry points or re-entries to the same concept) and `sharedState`
|
|
84
|
+
(storage, cache, messages, external APIs, client storage, issued URLs). The
|
|
85
|
+
design is stack-agnostic: the same axes cover frontend, backend, database,
|
|
86
|
+
cache and message flows.
|
|
87
|
+
- `included` rows carry `id`, `path` glob and `behavior`; `excluded` rows carry
|
|
88
|
+
`glob` and a reason saying why the surface is unaffected ("out of scope" is
|
|
89
|
+
not a reason); `unknown` rows name a reader the repository cannot see.
|
|
90
|
+
`contracts` declare `compatible` or `breaking`. A single surface is allowed.
|
|
91
|
+
- `valueChanges` `[{producer, kind, change, consumers, indirectConsumers}]`
|
|
92
|
+
list two hops whenever a value's semantics change (clear, redact, default,
|
|
93
|
+
normalize, unit/format).
|
|
94
|
+
- `scenarios` cover five generic dimensions — `initialState` (empty vs
|
|
95
|
+
existing), `redo` (reload, retry, redelivery, idempotency), `timeBoundary`
|
|
96
|
+
(day, TTL, timezone), `identitySwitch` (store, role, tenant) and
|
|
97
|
+
`concurrency` — each `{applies: true, behavior}` or `{na: reason}`.
|
|
98
|
+
- A parent holds the surface; its children declare `{"covers": [surface-id]}`
|
|
99
|
+
and their union must equal the parent's included list.
|
|
100
|
+
- Before any reviewer runs, discovery replays with `git grep -F` through argv
|
|
101
|
+
(never a shell or a regex built from task text), cheapest source first:
|
|
102
|
+
recorded queries; enclosing function names from diff hunk headers;
|
|
103
|
+
template-literal fixed fragments (at least 4 characters, separator-bounded,
|
|
104
|
+
such as `order:`, `user_`, `menu.`); compound identifiers (snake_case,
|
|
105
|
+
camelCase or containing digits, so prose words are not queries) and string
|
|
106
|
+
literals from added and removed lines (at least 6 characters; skipped as too
|
|
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.
|
|
116
|
+
Results are cached by HEAD, base, diff, query set and config hash.
|
|
117
|
+
- Review returns NOT_RUN with: `SURFACE_MISSING` (no or unparseable surface;
|
|
118
|
+
also refused at create for non-DEBUG tasks), `SURFACE_UNCLASSIFIED`,
|
|
119
|
+
`SURFACE_QUERY_TOO_BROAD` (one recorded query over 200 files),
|
|
120
|
+
`SURFACE_EXCLUSION_TOO_BROAD` (one glob over 10 files, or any file in the
|
|
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
|
|
123
|
+
silently skipped) or `SURFACE_UNASSIGNED` (children's covers differ from the
|
|
124
|
+
parent's included list; checked by every review, including advance and batch).
|
|
125
|
+
- A surface revision is a contract revision: fresh evidence and two fresh
|
|
126
|
+
reviews. Exclusions added after an `SURFACE_UNCLASSIFIED` block are flagged
|
|
127
|
+
to reviewers as post-block exclusions.
|
|
128
|
+
- Tasks created before this rule shipped are grandfathered. Advisory local-log
|
|
129
|
+
metrics per discovery source are fail-open and never evidence.
|
|
130
|
+
- Plain `worktree finish` writes the same local receipt as advance, including
|
|
131
|
+
both rounds' residual risks and non-blocking findings, references it in the
|
|
132
|
+
git note and prints the residual risks.
|
|
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
|
+
|
|
71
223
|
## EVIDENCE-AGGREGATE-001
|
|
72
224
|
|
|
73
225
|
The verifier MUST return FAIL when a criterion is missing, duplicated, unknown, or has empty evidence.
|
package/docs/en/spec/judgment.md
CHANGED
|
@@ -10,6 +10,13 @@ The operator MUST stop when requirements, authorization, or a required decision
|
|
|
10
10
|
- Positive: `Ask whether project or user scope is intended before applying installation.`
|
|
11
11
|
- Negative: `Assume project scope because it is more convenient.`
|
|
12
12
|
|
|
13
|
+
Surface audit triggers ([EVIDENCE-SURFACE-001](acceptance-and-evidence.md#evidence-surface-001)) also stop before editing:
|
|
14
|
+
|
|
15
|
+
- A `breaking` change to a shared contract with any UNKNOWN reader.
|
|
16
|
+
- A discovered surface excluded as "intentionally different": send the human
|
|
17
|
+
one consolidated behavior table (surface, current behavior, proposed
|
|
18
|
+
behavior, reason) and wait for the decision.
|
|
19
|
+
|
|
13
20
|
## JUDGMENT-RETRY-001
|
|
14
21
|
|
|
15
22
|
The operator MUST change approach after the same failure repeats without new information.
|
|
@@ -53,10 +53,10 @@ This module defines the bounded loop used to plan, implement, verify, and hand o
|
|
|
53
53
|
|
|
54
54
|
## LOOP-START-001
|
|
55
55
|
|
|
56
|
-
The operator MUST state acceptance criteria before editing and MUST stop when each criterion has evidence.
|
|
56
|
+
The operator MUST audit the change's surfaces, then state acceptance criteria before editing, and MUST stop when each criterion has evidence.
|
|
57
57
|
|
|
58
58
|
- Trigger: Starting a multi-step implementation or debugging loop.
|
|
59
|
-
- Action: Record 2–5 observable criteria, then inspect only the files
|
|
59
|
+
- Action: Record the surface audit ([EVIDENCE-SURFACE-001](acceptance-and-evidence.md#evidence-surface-001)), then 2–5 observable criteria, then inspect only the files within the audited scope that the current step needs.
|
|
60
60
|
- Evidence: The task record and final report link each criterion to a command or read-back.
|
|
61
61
|
- Positive: `Criteria: tests pass; package builds; changed files are read back.`
|
|
62
62
|
- Negative: `I changed the repository and will decide what success means afterward.`
|
package/docs/en/spec/review.md
CHANGED
|
@@ -84,6 +84,11 @@ successful refutation MUST make the run FAIL.
|
|
|
84
84
|
- Negative: `Manufacture a refutation so the challenge looks effective.`
|
|
85
85
|
- Note: With one configured target the same target is invoked again in a fresh
|
|
86
86
|
session. A FAIL is already terminal and is never re-checked.
|
|
87
|
+
- Note: For a surfaced task the second round also hunts unlisted surfaces and
|
|
88
|
+
scenarios under `surface-coverage`: it checks the five fixed scenario
|
|
89
|
+
dimensions and may run at most 3 new `git grep -F` queries per axis. Round 1
|
|
90
|
+
replays the recorded queries and reads every listed `valueChanges` consumer;
|
|
91
|
+
this is the only exception to "do not survey the repository".
|
|
87
92
|
|
|
88
93
|
## REVIEW-HOST-001
|
|
89
94
|
|