@kylecheng3146/agent-ops 0.9.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/packages/cli/src/args.js +58 -14
- package/dist/packages/cli/src/bin.js +10 -3
- package/dist/packages/cli/src/cli.js +30 -6
- package/dist/packages/cli/src/commands/advance.js +2 -1
- package/dist/packages/cli/src/commands/escapes.js +109 -0
- package/dist/packages/cli/src/commands/parallel.js +1 -1
- package/dist/packages/cli/src/commands/review-show.js +1 -1
- package/dist/packages/cli/src/commands/review.js +63 -11
- package/dist/packages/cli/src/commands/run.js +10 -3
- 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/context.js +22 -4
- package/dist/packages/cli/src/debug-intake.js +1 -1
- package/dist/packages/cli/src/office-entry.js +8 -4
- package/dist/packages/cli/src/owned-run-step.js +4 -4
- package/dist/packages/cli/src/parallel-deps.js +0 -2
- package/dist/packages/cli/src/run-deps.js +12 -9
- package/dist/packages/cli/src/run-entry.js +76 -17
- package/dist/packages/cli/src/run-setup-entry.js +2 -2
- 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 +7 -4
- package/dist/runtime/src/logging/local-log.js +44 -1
- package/dist/runtime/src/office/scene.js +1 -1
- package/dist/runtime/src/office/server.js +43 -16
- package/dist/runtime/src/office/snapshot.js +1 -1
- package/dist/runtime/src/parallel/finish.js +139 -65
- package/dist/runtime/src/parallel/manage.js +51 -1
- package/dist/runtime/src/parallel/receipt.js +3 -1
- package/dist/runtime/src/parallel/service.js +5 -1
- package/dist/runtime/src/review/execute.js +20 -12
- package/dist/runtime/src/review/packet.js +20 -7
- package/dist/runtime/src/review/probe.js +3 -2
- package/dist/runtime/src/review/runner.js +26 -3
- package/dist/runtime/src/review/slots.js +1 -1
- package/dist/runtime/src/run/controls.js +99 -1
- package/dist/runtime/src/run/hosts/codex.js +28 -1
- package/dist/runtime/src/run/hosts/protocol.js +5 -2
- package/dist/runtime/src/run/integration.js +35 -13
- package/dist/runtime/src/run/policy.js +65 -10
- package/dist/runtime/src/run/scheduler.js +10 -5
- package/dist/runtime/src/run/service.js +68 -21
- package/dist/runtime/src/run/supervisor.js +11 -2
- package/dist/runtime/src/run/transport.js +1 -0
- package/dist/runtime/src/schema/validate.js +6 -1
- package/dist/runtime/src/security/permissions.js +85 -0
- 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-state.js +9 -3
- package/dist/runtime/src/task/debug-user-event.js +82 -17
- package/dist/runtime/src/task/debug.js +59 -16
- 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 +24 -5
- package/docs/en/guides/task-debug.md +26 -2
- package/docs/en/spec/acceptance-and-evidence.md +104 -2
- package/docs/en/spec/harness-adapters.md +26 -1
- package/docs/en/spec/loop-engineering.md +34 -6
- package/docs/en/spec/review.md +6 -0
- package/docs/zh-TW/guides/configuration.md +20 -4
- package/docs/zh-TW/guides/task-debug.md +19 -2
- package/docs/zh-TW/spec/acceptance-and-evidence.md +84 -4
- package/docs/zh-TW/spec/harness-adapters.md +21 -1
- package/docs/zh-TW/spec/loop-engineering.md +9 -3
- package/docs/zh-TW/spec/review.md +5 -0
- package/package.json +1 -1
- package/schemas/task.schema.json +8 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open, readdir, realpath } from "node:fs/promises";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { isAbsolute, join, relative, sep } from "node:path";
|
|
4
5
|
import { sha256 } from "../fs/hash.js";
|
|
5
6
|
import { AgentOpsError } from "../fs/paths.js";
|
|
6
7
|
export function debugUserControl(text) {
|
|
7
8
|
const value = text.trim();
|
|
9
|
+
if (/^(?:不要給預算時間(?:\s+直接完整驗證與review)?|取消時間限制|no time limit)[。.!!]?$/iu.test(value))
|
|
10
|
+
return "unlimited";
|
|
8
11
|
if (/^(?:暫停|暂停|先暫停|先暂停|停止|stop|pause)[。.!!]?$/iu.test(value))
|
|
9
12
|
return "pause";
|
|
10
13
|
if (/^(?:先)?只診斷(?:,等我補失敗時證據)?[。.!!]?$/u.test(value) || /^(?:先)?只诊断[。.!!]?$/u.test(value) || /^diagnos(?:e|is)[ -]only[.!]?$/iu.test(value))
|
|
@@ -37,6 +40,57 @@ function codexUserText(row) {
|
|
|
37
40
|
catch { /* An ambiguous reply cannot authorize a control change. */ }
|
|
38
41
|
return undefined;
|
|
39
42
|
}
|
|
43
|
+
/** Bound each row, not the transcript: assistant/tool output must not hide later human input. */
|
|
44
|
+
async function* nativeLines(path) {
|
|
45
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
46
|
+
try {
|
|
47
|
+
if (!(await file.stat()).isFile())
|
|
48
|
+
return;
|
|
49
|
+
let pending = Buffer.alloc(0), oversized = false, index = 0;
|
|
50
|
+
const started = Date.now();
|
|
51
|
+
for await (const chunk of file.createReadStream({ autoClose: false })) {
|
|
52
|
+
if (Date.now() - started > 10_000)
|
|
53
|
+
throw new Error("Native transcript read timed out");
|
|
54
|
+
const bytes = chunk;
|
|
55
|
+
let start = 0;
|
|
56
|
+
while (start < bytes.length) {
|
|
57
|
+
const end = bytes.indexOf(10, start), stop = end < 0 ? bytes.length : end;
|
|
58
|
+
if (!oversized) {
|
|
59
|
+
if (pending.length + stop - start > 1024 * 1024) {
|
|
60
|
+
pending = Buffer.alloc(0);
|
|
61
|
+
oversized = true;
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
pending = Buffer.concat([pending, bytes.subarray(start, stop)]);
|
|
65
|
+
}
|
|
66
|
+
if (end < 0)
|
|
67
|
+
break;
|
|
68
|
+
if (!oversized)
|
|
69
|
+
yield { line: pending.toString("utf8"), index };
|
|
70
|
+
index++;
|
|
71
|
+
pending = Buffer.alloc(0);
|
|
72
|
+
oversized = false;
|
|
73
|
+
start = end + 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!oversized && pending.length > 0)
|
|
77
|
+
yield { line: pending.toString("utf8"), index };
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await file.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function ownedNativeSession(root, worktreeRoot, sessionId) {
|
|
84
|
+
if (process.env.CODEX_THREAD_ID !== sessionId || worktreeRoot === undefined)
|
|
85
|
+
return false;
|
|
86
|
+
const { readWorktreeRecord } = await import("../parallel/service.js");
|
|
87
|
+
const record = await readWorktreeRecord(worktreeRoot);
|
|
88
|
+
if (record === null || record.sessionId !== sessionId || record.agentId !== undefined || record.runId !== undefined)
|
|
89
|
+
return false;
|
|
90
|
+
const [main, worktree, recordMain, recordPath] = await Promise.all([root, worktreeRoot, record.mainRoot, record.path].map(path => realpath(path)));
|
|
91
|
+
const inside = relative(join(main, ".worktrees"), worktree);
|
|
92
|
+
return main === recordMain && worktree === recordPath && inside.length > 0 && !inside.startsWith("..") && !isAbsolute(inside);
|
|
93
|
+
}
|
|
40
94
|
/** Host transcripts are the user-event authority; CLI actor/text fields are never accepted. */
|
|
41
95
|
/** `worktreeRoot` is the session's owned worktree: Claude stamps each row with the shell's cwd, which moves there after `cd`. */
|
|
42
96
|
export async function readDebugUserEvent(root, sessionId, home = homedir(), kind = "latest", worktreeRoot) {
|
|
@@ -59,35 +113,46 @@ export async function readDebugUserEvent(root, sessionId, home = homedir(), kind
|
|
|
59
113
|
catch { /* The host need not be installed. */ }
|
|
60
114
|
}
|
|
61
115
|
await visit(codex, 3);
|
|
116
|
+
const owned = await ownedNativeSession(root, worktreeRoot, sessionId).catch(() => false);
|
|
62
117
|
for (const path of paths) {
|
|
63
118
|
try {
|
|
64
119
|
const stat = await lstat(path), canonical = await realpath(path);
|
|
65
120
|
const fromHome = relative(canonicalHome, canonical);
|
|
66
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024 * 1024 || fromHome.startsWith("..") || isAbsolute(fromHome))
|
|
67
|
-
continue;
|
|
68
|
-
const lines = (await readFile(path, "utf8")).split("\n");
|
|
69
|
-
const rows = lines.map(line => { try {
|
|
70
|
-
return JSON.parse(line);
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
return null;
|
|
74
|
-
} });
|
|
75
121
|
const codexSource = path.startsWith(codex + sep);
|
|
76
|
-
if (
|
|
122
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (!codexSource && stat.size > 16 * 1024 * 1024) || fromHome.startsWith("..") || isAbsolute(fromHome))
|
|
77
123
|
continue;
|
|
78
|
-
|
|
79
|
-
|
|
124
|
+
let matchedSession = false, event;
|
|
125
|
+
for await (const { line, index } of nativeLines(path)) {
|
|
126
|
+
let row;
|
|
127
|
+
try {
|
|
128
|
+
row = JSON.parse(line);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (codexSource && row?.type === "session_meta") {
|
|
134
|
+
if (row.payload?.id !== sessionId || (row.payload?.cwd !== root && !owned)) {
|
|
135
|
+
event = undefined;
|
|
136
|
+
matchedSession = false;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
matchedSession = true;
|
|
140
|
+
}
|
|
80
141
|
const text = codexSource ? codexUserText(row) :
|
|
81
142
|
row?.type === "user" && row.sessionId === sessionId && (row.cwd === root || (worktreeRoot !== undefined && row.cwd === worktreeRoot)) && row.message?.role === "user" &&
|
|
82
143
|
row.isMeta !== true && (row.origin === undefined || row.origin?.kind === "human") ? row.message.content : undefined;
|
|
83
144
|
if (typeof text !== "string" || text.length > 64 * 1024 || (kind === "control" && debugUserControl(text) === null))
|
|
84
145
|
continue;
|
|
85
146
|
const timestamp = Date.parse(row.timestamp);
|
|
86
|
-
if (!Number.isFinite(timestamp))
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
147
|
+
if (!Number.isFinite(timestamp)) {
|
|
148
|
+
event = undefined;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const sourceDigest = sha256(line);
|
|
152
|
+
event = { id: sha256(relative(home, path) + ":" + index + ":" + sourceDigest), sourceDigest, text, timestamp };
|
|
90
153
|
}
|
|
154
|
+
if (event !== undefined && (!codexSource || matchedSession))
|
|
155
|
+
return event;
|
|
91
156
|
}
|
|
92
157
|
catch { /* An unavailable native source cannot grant time. */ }
|
|
93
158
|
}
|
|
@@ -14,13 +14,14 @@ import { resolveReviewScope } from "../review/scope.js";
|
|
|
14
14
|
import { runVerificationCommand } from "../verify/spawn.js";
|
|
15
15
|
import { checkTaskVerificationEvidence } from "./completion.js";
|
|
16
16
|
import { taskContractHash, criterionContractHash } from "./contract.js";
|
|
17
|
-
import { DEBUG_EXTENSION_MS, debugProofDigest, debugRemainingMs, debugRounds, debugStopCause } from "./debug-state.js";
|
|
17
|
+
import { DEBUG_EXTENSION_MS, debugSpentMs, debugProofDigest, debugRemainingMs, debugRounds, debugStopCause } from "./debug-state.js";
|
|
18
18
|
import { FileTaskStore } from "./store.js";
|
|
19
19
|
import { debugUserControl, readDebugUserEvent } from "./debug-user-event.js";
|
|
20
20
|
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 {
|
|
@@ -142,7 +144,13 @@ export class DebugService {
|
|
|
142
144
|
await this.#mutate(root, next);
|
|
143
145
|
return await this.root(root.task.id);
|
|
144
146
|
}
|
|
145
|
-
|
|
147
|
+
/**
|
|
148
|
+
* `sealed` admits a strategy switch's citation of this issue's own sealed
|
|
149
|
+
* regression history: a scope revision stales every current reference while
|
|
150
|
+
* verification stays stopped until the switch is recorded. Hypotheses and
|
|
151
|
+
* delivery still need evidence for the current contract and candidate.
|
|
152
|
+
*/
|
|
153
|
+
async #evidence(taskId, reference, sealed = false) {
|
|
146
154
|
const { task, config, sourceFingerprint } = await this.#scope(taskId);
|
|
147
155
|
const store = new FileEvidenceStore(this.options.root, this.options.root);
|
|
148
156
|
const checked = validateEvidence(await store.load(reference));
|
|
@@ -152,9 +160,16 @@ export class DebugService {
|
|
|
152
160
|
const root = await this.root(taskId);
|
|
153
161
|
const head = await this.options.gitRunner.run(["rev-parse", "HEAD"]);
|
|
154
162
|
const candidate = new TextDecoder().decode(head.stdout).trim();
|
|
155
|
-
|
|
156
|
-
e.sourceFingerprint
|
|
157
|
-
|
|
163
|
+
const current = p !== undefined && e.taskContractHash === taskContractHash(task.task) && e.configHash === calculateConfigHash(config) &&
|
|
164
|
+
e.sourceFingerprint === sourceFingerprint && p.pairedCommit === (p.phase === "baseline" ? candidate : root.debug.baselineCommit) &&
|
|
165
|
+
p.commit === (p.phase === "baseline" ? root.debug.baselineCommit : candidate);
|
|
166
|
+
const historical = sealed && p !== undefined && (p.phase === "baseline" ? p.commit : p.pairedCommit) === root.debug.baselineCommit &&
|
|
167
|
+
root.debug.receipts.some(r => r.kind === "verify" && r.taskId === taskId && r.references.includes(reference));
|
|
168
|
+
if (head.exitCode !== 0 || e.taskId !== taskId || p === undefined || !(current || historical) ||
|
|
169
|
+
!task.task.criteria.some(c => c.id === e.criterionId && c.acceptance?.mode === "behavioral" && c.acceptance.baselineCommit === root.debug.baselineCommit &&
|
|
170
|
+
(current
|
|
171
|
+
? p.criterionContractHash === criterionContractHash(c) && c.acceptance.bindings.some(b => b.runnerId === e.commandId && sha256(canonicalJson({ runnerId: b.runnerId, materials: b.materials })) === p.bindingHash)
|
|
172
|
+
: c.acceptance.bindings.some(b => b.runnerId === e.commandId))))
|
|
158
173
|
return fail("TASK_DEBUG_EVIDENCE_STALE", "Evidence must discriminate the original issue at its immutable baseline and current candidate.");
|
|
159
174
|
const source = await readPrivateFile(join(this.options.root, p.executionArtifact), this.options.root);
|
|
160
175
|
if (source === null || p.executionArtifact !== ".agent-ops/tasks/acceptance/" + sha256(source) + ".json")
|
|
@@ -240,9 +255,30 @@ export class DebugService {
|
|
|
240
255
|
fail("TASK_DEBUG_STRATEGY_INVALID", "Document a substantive different verification method and evidence-backed reason.");
|
|
241
256
|
if (debugStopCause(root.debug) !== "strategy-required" || root.debug.strategy !== null || root.debug.operation !== null)
|
|
242
257
|
fail("TASK_DEBUG_STRATEGY_NOT_ALLOWED", "Exactly one strategy switch is allowed after two stagnant repair rounds.");
|
|
243
|
-
await this.#evidence(taskId, input.reference);
|
|
258
|
+
await this.#evidence(taskId, input.reference, true);
|
|
244
259
|
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
260
|
}
|
|
261
|
+
/** Escape Loop: where the confirmed cause came from, bound into the contract before review. */
|
|
262
|
+
async origin(taskId, input) {
|
|
263
|
+
const root = await this.root(taskId);
|
|
264
|
+
if (root.task.id !== taskId)
|
|
265
|
+
fail("ESCAPE_ORIGIN_INVALID", "Declare the origin on the original issue task.");
|
|
266
|
+
if (root.status !== "active" || root.debug.operation !== null)
|
|
267
|
+
fail("TASK_DEBUG_BUSY", "Origin needs an active idle issue.");
|
|
268
|
+
if (debugRemainingMs(root.debug) === 0)
|
|
269
|
+
fail("TASK_DEBUG_BUDGET_EXHAUSTED", "The original issue's active budget is exhausted.");
|
|
270
|
+
if (!root.debug.hypotheses.some(h => h.status === "confirmed"))
|
|
271
|
+
fail("ESCAPE_ORIGIN_EARLY", "Declare the origin only after discriminating evidence confirms a hypothesis.");
|
|
272
|
+
const origin = await resolveOrigin(this.options.gitRunner, input);
|
|
273
|
+
const record = await this.options.tasks.revise(taskId, { expectedContractHash: taskContractHash(root.task), origin, debugOrigin: true,
|
|
274
|
+
reason: `Declare issue origin: ${origin.kind === "escape-of" ? "escape-of " + origin.commit : origin.kind}.` });
|
|
275
|
+
if (origin.kind !== "escape-of") {
|
|
276
|
+
await removeEscapeRecord(this.options.root, taskId);
|
|
277
|
+
return { record };
|
|
278
|
+
}
|
|
279
|
+
return { record, escape: await declareEscape({ root: this.options.root, git: this.options.gitRunner, taskId, origin,
|
|
280
|
+
base: root.debug.baselineCommit, now: new Date().toISOString() }) };
|
|
281
|
+
}
|
|
246
282
|
async remaining(taskId) { return debugRemainingMs((await this.root(taskId)).debug); }
|
|
247
283
|
async #userEvent(sessionId, home, kind = "latest") {
|
|
248
284
|
const record = await readWorktreeRecord(this.options.root);
|
|
@@ -277,9 +313,9 @@ export class DebugService {
|
|
|
277
313
|
const attached = await this.options.tasks.status({ sessionId });
|
|
278
314
|
if (attached.task.debug?.rootTaskId !== root.task.id || root.status !== "active")
|
|
279
315
|
fail("TASK_DEBUG_CONTROL_INVALID", "Human control must belong to the active attached original issue.");
|
|
280
|
-
if (action === "resume" && debugRemainingMs(root.debug) === 0) {
|
|
316
|
+
if (action === "unlimited" || ((action === "resume" || action === "repair") && debugRemainingMs(root.debug) === 0)) {
|
|
281
317
|
try {
|
|
282
|
-
await this.extend(taskId, sessionId, event.id);
|
|
318
|
+
await this.extend(taskId, sessionId, event.id, home);
|
|
283
319
|
}
|
|
284
320
|
catch (error) {
|
|
285
321
|
if (!(error instanceof AgentOpsError) || !["TASK_DEBUG_GRANT_INVALID", "TASK_DEBUG_GRANT_UNSUPPORTED"].includes(error.code))
|
|
@@ -351,10 +387,14 @@ export class DebugService {
|
|
|
351
387
|
// EVIDENCE-SURFACE-001: intake may start without a surface, the first repair may not.
|
|
352
388
|
if (kind === "verify" && task.task.surfaceRequired === true && task.task.surface === undefined && productDigest !== sha256(canonicalJson([])))
|
|
353
389
|
fail("SURFACE_MISSING", "Add the surface audit with task revise --surface-file before verifying the first repair.");
|
|
354
|
-
const
|
|
390
|
+
const previousVerify = root.debug.receipts.filter(r => r.kind === "verify" && (r.outcome === "PASS" || r.outcome === "FAIL")).at(-1);
|
|
391
|
+
const previousCandidate = previousVerify?.productDigest ?? sha256("[]");
|
|
355
392
|
const cause = debugStopCause(root.debug);
|
|
356
393
|
// Recovery reruns the original verifier; changed product content still needs fresh proof.
|
|
357
|
-
|
|
394
|
+
// At the round limit an unchanged PASS candidate may be re-verified (not a new round), e.g. after an
|
|
395
|
+
// escape-ledger update moved the source fingerprint; a FAIL cannot be re-run into proof at the cap.
|
|
396
|
+
if (cause !== null && !(["environment", "permission"].includes(cause) && (kind === "investigate" || kind === "verify")) &&
|
|
397
|
+
!(cause === "iterations" && productDigest === previousCandidate && (kind === "review" || (kind === "verify" && previousVerify?.outcome === "PASS"))))
|
|
358
398
|
fail("TASK_DEBUG_STOP_REQUIRED", `Runtime stop predicate: ${cause}. Record a checkpoint; resume alone cannot reset it.`);
|
|
359
399
|
const processIdentity = await nativeProcessIdentity(process.pid);
|
|
360
400
|
if (processIdentity === null)
|
|
@@ -564,17 +604,20 @@ export class DebugService {
|
|
|
564
604
|
fail("TASK_DEBUG_RECOVERY_CONFIRMATION_REQUIRED", "Owned debug checkpoint/path/source/contract/config mismatch. Preserve the checkout and obtain user confirmation before recovery.");
|
|
565
605
|
await this.options.tasks.attach(sessionId, root.task.id);
|
|
566
606
|
}
|
|
567
|
-
async extend(taskId, sessionId, eventId) {
|
|
607
|
+
async extend(taskId, sessionId, eventId, home) {
|
|
568
608
|
const root = await this.root(taskId), attached = await this.options.tasks.status({ sessionId });
|
|
569
609
|
if (attached.task.debug?.rootTaskId !== root.task.id || root.debug.operation !== null)
|
|
570
610
|
fail("TASK_DEBUG_GRANT_INVALID", "Extension belongs to the original issue and its native session; a running operation cannot be extended.");
|
|
571
|
-
const event = await this.#userEvent(sessionId);
|
|
611
|
+
const event = await this.#userEvent(sessionId, home);
|
|
572
612
|
if (eventId !== "latest" && eventId !== event.id)
|
|
573
613
|
fail("TASK_DEBUG_GRANT_INVALID", "The requested event is not the current native human event.");
|
|
574
614
|
if (root.debug.grants.some(g => g.eventId === event.id))
|
|
575
615
|
return;
|
|
576
|
-
const otherCause = debugStopCause({ ...root.debug, consumedMs: 0, budgetMs: 86_400_000,
|
|
616
|
+
const otherCause = debugStopCause({ ...root.debug, consumedMs: 0, foreground: null, operation: null, budgetMs: 86_400_000,
|
|
577
617
|
...(root.debug.control === undefined ? {} : { control: { ...root.debug.control, paused: false } }) });
|
|
618
|
+
const unlimited = debugUserControl(event.text) === "unlimited";
|
|
619
|
+
if (unlimited && (root.status !== "active" || event.timestamp < Date.parse(root.createdAt)))
|
|
620
|
+
fail("TASK_DEBUG_GRANT_INVALID", "Only a current native human event can waive the active original issue time limit.");
|
|
578
621
|
const foregroundActive = root.debug.foreground !== undefined && root.debug.foreground !== null;
|
|
579
622
|
const remainingAtEvent = debugRemainingMs(root.debug, event.timestamp);
|
|
580
623
|
const sealedAtCandidates = [Date.parse(root.updatedAt), root.debug.receipts.at(-1)?.finishedAt ?? Number.NaN].filter(Number.isFinite);
|
|
@@ -582,8 +625,8 @@ export class DebugService {
|
|
|
582
625
|
const exhaustedAtEvent = foregroundActive
|
|
583
626
|
? remainingAtEvent === 0
|
|
584
627
|
: debugRemainingMs(root.debug) === 0 && event.timestamp > lastSealedAt;
|
|
585
|
-
if (!exhaustedAtEvent || (otherCause !== null && !["environment", "permission"].includes(otherCause)) ||
|
|
628
|
+
if ((!unlimited && !exhaustedAtEvent) || (otherCause !== null && !["environment", "permission"].includes(otherCause)) || !["resume", "repair", "unlimited"].includes(debugUserControl(event.text) ?? ""))
|
|
586
629
|
fail("TASK_DEBUG_GRANT_INVALID", "Only a contemporaneous human continue event extends exhausted time; other blockers remain authoritative.");
|
|
587
|
-
await this.#mutate(root, { ...root.debug, grants: [...root.debug.grants, { eventId: event.id, sessionId, sourceDigest: event.sourceDigest, grantedMs: DEBUG_EXTENSION_MS }], checkpoint: null });
|
|
630
|
+
await this.#mutate(root, { ...root.debug, grants: [...root.debug.grants, { eventId: event.id, sessionId, sourceDigest: event.sourceDigest, grantedMs: unlimited ? null : DEBUG_EXTENSION_MS, spentAtGrantMs: debugSpentMs(root.debug, event.timestamp) }], checkpoint: null });
|
|
588
631
|
}
|
|
589
632
|
}
|
|
@@ -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
|
|
@@ -260,7 +275,7 @@ evidence. Give each editing conversation its own worktree instead:
|
|
|
260
275
|
(Claude Code refuses it inside an isolated worktree session). `agent-ops
|
|
261
276
|
worktree finish <name>` completes every non-archived task in the worktree,
|
|
262
277
|
subtasks before their parents, each against the base its PASS review
|
|
263
|
-
recorded (the worktree's base when none was recorded), before
|
|
278
|
+
recorded (the worktree's base when none was recorded), before merging.
|
|
264
279
|
If one cannot complete, nothing merges and the error names the task.
|
|
265
280
|
Creating a subtask keeps the session attached to the top of its tree, and
|
|
266
281
|
each task keeps its own review record, so a parent and a subtask reviewed
|
|
@@ -280,9 +295,13 @@ evidence. Give each editing conversation its own worktree instead:
|
|
|
280
295
|
its files, so uncommitted changes there block it. When the main checkout is
|
|
281
296
|
on another branch or detached, only the target's ref moves: the checkout, its
|
|
282
297
|
files and its uncommitted changes are left alone. A target checked out in
|
|
283
|
-
another worktree is refused, naming that path. If the target moved, it rebases
|
|
284
|
-
|
|
285
|
-
the
|
|
298
|
+
another worktree is refused, naming that path. If the target moved, it rebases the worktree and
|
|
299
|
+
stops with `WORKTREE_REBASED` before completing any task: earlier evidence
|
|
300
|
+
covered the old base, not the combination that would merge. Verify and
|
|
301
|
+
review again with the printed `--base`, then finish again; a task that was
|
|
302
|
+
already complete cannot take new evidence, so cover the rebased change with
|
|
303
|
+
a follow-up task. A conflict is reset and reported with the
|
|
304
|
+
`refs/notes/agent-ops` intent of the work that landed first. The note
|
|
286
305
|
records every task of the worktree, subtasks indented under their parent,
|
|
287
306
|
and a `session: <id>` line naming the session that finished it. Finish
|
|
288
307
|
also resets that session's gate baseline to the merged source, so the
|