@kylecheng3146/agent-ops 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/cli/src/args.js +48 -4
- package/dist/packages/cli/src/bin.js +10 -3
- package/dist/packages/cli/src/cli.js +16 -2
- package/dist/packages/cli/src/commands/escapes.js +109 -0
- package/dist/packages/cli/src/commands/review-show.js +1 -1
- package/dist/packages/cli/src/commands/review.js +60 -10
- package/dist/packages/cli/src/commands/task.js +41 -3
- package/dist/packages/cli/src/commands/update.js +16 -4
- package/dist/packages/cli/src/commands/verify.js +4 -1
- package/dist/packages/cli/src/debug-intake.js +1 -1
- package/dist/runtime/src/escape/link.js +317 -0
- package/dist/runtime/src/escape/origin.js +20 -0
- package/dist/runtime/src/escape/rules.js +252 -0
- package/dist/runtime/src/fs/temporary.js +10 -0
- package/dist/runtime/src/install/harness.js +2 -2
- package/dist/runtime/src/logging/local-log.js +44 -1
- package/dist/runtime/src/parallel/manage.js +51 -1
- package/dist/runtime/src/parallel/receipt.js +3 -1
- package/dist/runtime/src/review/execute.js +12 -6
- package/dist/runtime/src/review/packet.js +3 -1
- package/dist/runtime/src/review/probe.js +3 -2
- package/dist/runtime/src/review/runner.js +24 -3
- package/dist/runtime/src/review/slots.js +1 -1
- package/dist/runtime/src/schema/validate.js +6 -1
- package/dist/runtime/src/surface/discovery.js +79 -2
- package/dist/runtime/src/surface/schema.js +43 -2
- package/dist/runtime/src/task/completion.js +20 -0
- package/dist/runtime/src/task/contract.js +3 -1
- package/dist/runtime/src/task/debug.js +24 -1
- package/dist/runtime/src/task/service.js +58 -6
- package/dist/runtime/src/verify/acceptance-replay.js +24 -11
- package/docs/en/guides/configuration.md +16 -1
- package/docs/en/guides/task-debug.md +12 -0
- package/docs/en/spec/acceptance-and-evidence.md +100 -2
- package/docs/zh-TW/guides/configuration.md +14 -1
- package/docs/zh-TW/guides/task-debug.md +10 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +82 -3
- package/package.json +1 -1
- package/schemas/task.schema.json +8 -1
|
@@ -13,7 +13,8 @@ export const COMMAND_NAMES = [
|
|
|
13
13
|
"allow-stop",
|
|
14
14
|
"worktree",
|
|
15
15
|
"run",
|
|
16
|
-
"office"
|
|
16
|
+
"office",
|
|
17
|
+
"escapes"
|
|
17
18
|
];
|
|
18
19
|
const COMMAND_SET = new Set(COMMAND_NAMES);
|
|
19
20
|
const SCOPES = new Set(["project", "user"]);
|
|
@@ -91,6 +92,11 @@ export function parseArgs(argv) {
|
|
|
91
92
|
const criterionFiles = [];
|
|
92
93
|
let criteriaFile;
|
|
93
94
|
let surfaceFile;
|
|
95
|
+
let escapeOf;
|
|
96
|
+
let escapeLabel;
|
|
97
|
+
let escapeReceipt;
|
|
98
|
+
let escapeFix;
|
|
99
|
+
let escapePromote;
|
|
94
100
|
let planFile;
|
|
95
101
|
let expectedContract;
|
|
96
102
|
let expectedTreeContract;
|
|
@@ -231,7 +237,7 @@ export function parseArgs(argv) {
|
|
|
231
237
|
if (debugAction !== undefined)
|
|
232
238
|
duplicate(token);
|
|
233
239
|
const value = readOptionValue(argv, index++, token);
|
|
234
|
-
if (!["status", "hypothesis", "strategy", "investigate", "checkpoint", "resume", "extend"].includes(value))
|
|
240
|
+
if (!["status", "hypothesis", "strategy", "investigate", "checkpoint", "resume", "extend", "origin"].includes(value))
|
|
235
241
|
invalidValue(token, value);
|
|
236
242
|
debugAction = value;
|
|
237
243
|
break;
|
|
@@ -270,6 +276,31 @@ export function parseArgs(argv) {
|
|
|
270
276
|
duplicate(token);
|
|
271
277
|
surfaceFile = readOptionValue(argv, index++, token);
|
|
272
278
|
break;
|
|
279
|
+
case "--escape-of":
|
|
280
|
+
if (escapeOf !== undefined)
|
|
281
|
+
duplicate(token);
|
|
282
|
+
escapeOf = readOptionValue(argv, index++, token);
|
|
283
|
+
break;
|
|
284
|
+
case "--escape-label":
|
|
285
|
+
if (escapeLabel !== undefined)
|
|
286
|
+
duplicate(token);
|
|
287
|
+
escapeLabel = readOptionValue(argv, index++, token);
|
|
288
|
+
break;
|
|
289
|
+
case "--escape-receipt":
|
|
290
|
+
if (escapeReceipt !== undefined)
|
|
291
|
+
duplicate(token);
|
|
292
|
+
escapeReceipt = readOptionValue(argv, index++, token);
|
|
293
|
+
break;
|
|
294
|
+
case "--escape-fix":
|
|
295
|
+
if (escapeFix !== undefined)
|
|
296
|
+
duplicate(token);
|
|
297
|
+
escapeFix = readOptionValue(argv, index++, token);
|
|
298
|
+
break;
|
|
299
|
+
case "--escape-promote":
|
|
300
|
+
if (escapePromote !== undefined)
|
|
301
|
+
duplicate(token);
|
|
302
|
+
escapePromote = readOptionValue(argv, index++, token);
|
|
303
|
+
break;
|
|
273
304
|
case "--plan-file":
|
|
274
305
|
if (planFile !== undefined)
|
|
275
306
|
duplicate(token);
|
|
@@ -687,7 +718,7 @@ export function parseArgs(argv) {
|
|
|
687
718
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Debug operation options apply only to task debug.");
|
|
688
719
|
if (command === "task" && action === "debug" && (taskId === undefined || debugAction === undefined || title !== undefined || intent !== undefined ||
|
|
689
720
|
parentTaskId !== undefined || criteria.length > 0 || evidence.length > 0 ||
|
|
690
|
-
((debugAction === "hypothesis" || debugAction === "strategy") !== (payloadFile !== undefined)) ||
|
|
721
|
+
((debugAction === "hypothesis" || debugAction === "strategy" || debugAction === "origin") !== (payloadFile !== undefined)) ||
|
|
691
722
|
((debugAction === "investigate") !== (commandId !== undefined)) || ((debugAction === "extend") !== (eventId !== undefined))))
|
|
692
723
|
throw new CliArgumentError("CLI_MISSING_VALUE", "Task debug requires --task, --debug-action and the operation's exact payload/command/event options.");
|
|
693
724
|
if ((criterionFiles.length > 0 && !(command === "task" && (action === "create" || action === "pin-finding"))) ||
|
|
@@ -699,8 +730,16 @@ export function parseArgs(argv) {
|
|
|
699
730
|
(reason !== undefined && !revisionAction) ||
|
|
700
731
|
(baseline !== undefined && !(command === "task" && (action === "create" || action === "revise"))))
|
|
701
732
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Acceptance options are not supported by this command.");
|
|
733
|
+
if ((escapeOf !== undefined || escapeLabel !== undefined || escapeReceipt !== undefined || escapeFix !== undefined || escapePromote !== undefined) &&
|
|
734
|
+
!(command === "task" && (action === "create" || action === "revise")))
|
|
735
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Escape options apply only to task create and revise.");
|
|
736
|
+
if ((escapeOf === undefined) !== (escapeLabel === undefined) ||
|
|
737
|
+
((escapeReceipt !== undefined || escapeFix !== undefined || escapePromote !== undefined) && escapeOf === undefined))
|
|
738
|
+
throw new CliArgumentError("CLI_MISSING_VALUE", "--escape-of needs --escape-label; --escape-label, --escape-receipt, --escape-fix and --escape-promote need --escape-of.");
|
|
739
|
+
if (command === "escapes" && argv.some(token => token !== "escapes" && token !== "--json"))
|
|
740
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "escapes accepts only --json; it is a read-only report.");
|
|
702
741
|
if (revisionAction && (taskId === undefined || evidence.length > 0 || title !== undefined || intent !== undefined || parentTaskId !== undefined || sessionId !== undefined || criteria.length > 0 || base !== undefined ||
|
|
703
|
-
(action === "revise" && (expectedContract === undefined || (criteriaFile === undefined && surfaceFile === undefined) || reason === undefined)) ||
|
|
742
|
+
(action === "revise" && (expectedContract === undefined || (criteriaFile === undefined && surfaceFile === undefined && escapeOf === undefined) || reason === undefined)) ||
|
|
704
743
|
(action === "replan" && (expectedTreeContract === undefined || planFile === undefined || reason === undefined)) ||
|
|
705
744
|
(action === "pin-finding" && (expectedContract === undefined || findingReference === undefined || criterionFiles.length !== 1))))
|
|
706
745
|
throw new CliArgumentError("CLI_MISSING_VALUE", "Acceptance revision requires task, expected contract, payload and reason.");
|
|
@@ -875,6 +914,11 @@ export function parseArgs(argv) {
|
|
|
875
914
|
...(criterionFiles.length === 0 ? {} : { criterionFiles }),
|
|
876
915
|
...(criteriaFile === undefined ? {} : { criteriaFile }),
|
|
877
916
|
...(surfaceFile === undefined ? {} : { surfaceFile }),
|
|
917
|
+
...(escapeOf === undefined ? {} : { escapeOf }),
|
|
918
|
+
...(escapeLabel === undefined ? {} : { escapeLabel }),
|
|
919
|
+
...(escapeReceipt === undefined ? {} : { escapeReceipt }),
|
|
920
|
+
...(escapeFix === undefined ? {} : { escapeFix }),
|
|
921
|
+
...(escapePromote === undefined ? {} : { escapePromote }),
|
|
878
922
|
...(planFile === undefined ? {} : { planFile }),
|
|
879
923
|
...(expectedContract === undefined ? {} : { expectedContract }),
|
|
880
924
|
...(expectedTreeContract === undefined ? {} : { expectedTreeContract }),
|
|
@@ -52,14 +52,15 @@ import { runTrustCommand } from "./commands/trust.js";
|
|
|
52
52
|
import { runVerifyCommand } from "./commands/verify.js";
|
|
53
53
|
import { runAdvanceCommand } from "./commands/advance.js";
|
|
54
54
|
import { runReviewShowCommand } from "./commands/review-show.js";
|
|
55
|
+
import { runEscapesCommand } from "./commands/escapes.js";
|
|
55
56
|
import { runAllowStopCommand } from "./commands/allow-stop.js";
|
|
56
57
|
import { CompletionGateService } from "../../../runtime/src/hooks/completion-gate.js";
|
|
57
|
-
import { formatUpdatePlan, runUpdateCommand } from "./commands/update.js";
|
|
58
|
+
import { formatUpdatePlan, runUpdateCommand, worktreeSyncLine } from "./commands/update.js";
|
|
58
59
|
import { errorEnvelope } from "./output.js";
|
|
59
60
|
import { runAgyHeadless } from "./agy-headless.js";
|
|
60
61
|
import { runWorktreeCommand } from "./commands/parallel.js";
|
|
61
62
|
import { ensureSessionWorktree, readWorktreeRecord, resolveCheckouts } from "../../../runtime/src/parallel/service.js";
|
|
62
|
-
import { listWorktrees, branchLockDoctorResult, worktreeDoctorResult } from "../../../runtime/src/parallel/manage.js";
|
|
63
|
+
import { listWorktrees, branchLockDoctorResult, sessionWorktreeNames, syncSessionWorktrees, worktreeDoctorResult } from "../../../runtime/src/parallel/manage.js";
|
|
63
64
|
import { detectGhostFiles, ghostFilesDoctorResult } from "../../../runtime/src/install/doctor.js";
|
|
64
65
|
/** The repository's common Git directory, where display-only office records live; undefined when git cannot say. */
|
|
65
66
|
async function officeCommonDir(dir) {
|
|
@@ -264,6 +265,8 @@ else {
|
|
|
264
265
|
const context = await productionRunContext(root);
|
|
265
266
|
return await runRunCommand({ args, ...context, root: context.mainRoot, office: async () => await ensureBackgroundOffice(root, undefined, openOfficeBrowser) });
|
|
266
267
|
}
|
|
268
|
+
if (args.command === "escapes")
|
|
269
|
+
return await runEscapesCommand({ root });
|
|
267
270
|
if (args.command === "office") {
|
|
268
271
|
return await runOfficeCommand({ ensure: async () => await ensureBackgroundOffice(root, undefined, openOfficeBrowser),
|
|
269
272
|
foreground: async (onUrl) => await serveOffice(root, onUrl), writeStdout: (text) => { process.stdout.write(text); } });
|
|
@@ -422,7 +425,11 @@ else {
|
|
|
422
425
|
: { hookTargets: updateArgs.hookTargets }),
|
|
423
426
|
trustStore: store,
|
|
424
427
|
calculateTrustBinding: async (config) => await plannedTrustBinding(root, config),
|
|
425
|
-
confirm: async (plan, trust) => await confirmPlan(formatUpdatePlan(plan, trust)),
|
|
428
|
+
confirm: async (plan, trust, worktrees) => await confirmPlan(formatUpdatePlan(plan, trust) + worktreeSyncLine(worktrees)),
|
|
429
|
+
sessionWorktrees: {
|
|
430
|
+
list: async () => await sessionWorktreeNames(worktreeDependencies(), root),
|
|
431
|
+
sync: async () => await syncSessionWorktrees(worktreeDependencies(), root)
|
|
432
|
+
},
|
|
426
433
|
promptOffice: async (message, enabled) => await selectYesNo(message, { input: process.stdin, output: process.stdout }, enabled),
|
|
427
434
|
promptAutoRun: async (message, enabled) => await selectYesNo(message, { input: process.stdin, output: process.stdout }, enabled),
|
|
428
435
|
promptWorktree: async (message) => await selectYesNo(message, { input: process.stdin, output: process.stdout }, true),
|
|
@@ -31,6 +31,7 @@ Commands:
|
|
|
31
31
|
review Run an independent review or show recorded reports
|
|
32
32
|
run Start or control a supervised Claude/Codex native goal
|
|
33
33
|
office Open the local pixel office (Preview; opt-in)
|
|
34
|
+
escapes Report escaped bugs, gate catches and learned rules (read-only)
|
|
34
35
|
batch Verify and review a parent task's subtasks together
|
|
35
36
|
allow-stop Grant one fingerprint-bound completion-gate Stop permit (requires --session)
|
|
36
37
|
agy-run Run headless agy with a process-exit completion recheck
|
|
@@ -75,6 +76,14 @@ Options:
|
|
|
75
76
|
* rejected.
|
|
76
77
|
*/
|
|
77
78
|
export const COMMAND_HELP_TEXT = {
|
|
79
|
+
escapes: `Usage: agent-ops escapes [--json]
|
|
80
|
+
|
|
81
|
+
Read-only Escape Loop report: misses per gate from the tracked
|
|
82
|
+
.agent-ops-escapes ledger, gate catches from this machine's local log, origin
|
|
83
|
+
and link-state mix, and per-rule fire/included/excluded counts with retirement
|
|
84
|
+
candidates. Counts are raw with n; fewer than 10 observations are marked
|
|
85
|
+
insufficient-data. It never writes files and fails open.
|
|
86
|
+
`,
|
|
78
87
|
office: `Usage: agent-ops office [--json]
|
|
79
88
|
|
|
80
89
|
Office (Preview) is opt-in: select it during init/update or run
|
|
@@ -179,8 +188,8 @@ Options:
|
|
|
179
188
|
--title <text> create
|
|
180
189
|
--goal <text> create: immutable original issue/goal
|
|
181
190
|
--debug create: task-level investigation tag
|
|
182
|
-
--debug-action <status|hypothesis|strategy|investigate|checkpoint|resume|extend>
|
|
183
|
-
--payload-file <path> debug hypothesis/strategy: bounded JSON
|
|
191
|
+
--debug-action <status|hypothesis|strategy|investigate|checkpoint|resume|extend|origin>
|
|
192
|
+
--payload-file <path> debug hypothesis/strategy/origin: bounded JSON
|
|
184
193
|
--command-id <id> debug investigate: trusted configured command
|
|
185
194
|
--event-id <native-event-id|latest> debug extend: authentic human continue event
|
|
186
195
|
--intent <text> create: intended behavior and constraints before editing
|
|
@@ -189,6 +198,11 @@ Options:
|
|
|
189
198
|
--criteria-file <path> revise: replacement criterion array
|
|
190
199
|
--surface-file <path> create (required unless --debug), revise: EVIDENCE-SURFACE-001
|
|
191
200
|
audit JSON, or {"covers":[surface-id]} under a surfaced parent
|
|
201
|
+
--escape-of <commit> create, revise: the commit this bug escaped from (Escape Loop)
|
|
202
|
+
--escape-label <label> with --escape-of: requirement|surface|scenario|criteria|verify|review|flagged|out-of-model
|
|
203
|
+
--escape-receipt <id> with --escape-of: link this local finish receipt instead of resolving one
|
|
204
|
+
--escape-fix <commit> with --escape-of: backfill; missed paths come from this past fix commit
|
|
205
|
+
--escape-promote <runner:check> with --escape-of: the red-to-green check proving a criteria/verify escape
|
|
192
206
|
--plan-file <path> replan: split-task array with requirement mappings
|
|
193
207
|
--expected-contract <hash> revise, pin-finding: current task contract from status
|
|
194
208
|
--expected-tree-contract <hash> replan: current tree contract from status
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { LEDGER_DIRECTORY } from "../../../../runtime/src/escape/link.js";
|
|
4
|
+
import { loadRules } from "../../../../runtime/src/escape/rules.js";
|
|
5
|
+
import { readLocalLog } from "../../../../runtime/src/logging/local-log.js";
|
|
6
|
+
import { okEnvelope } from "../output.js";
|
|
7
|
+
/** Below this many observations a count is shown but not trusted. */
|
|
8
|
+
export const MIN_SAMPLE = 10;
|
|
9
|
+
const status = (n) => n < MIN_SAMPLE ? "insufficient-data" : "ok";
|
|
10
|
+
function section(rows) {
|
|
11
|
+
const counts = {};
|
|
12
|
+
for (const row of rows)
|
|
13
|
+
counts[row.key] = (counts[row.key] ?? 0) + 1;
|
|
14
|
+
const times = rows.flatMap(row => row.at === undefined ? [] : [row.at]).sort();
|
|
15
|
+
return { n: rows.length, status: status(rows.length), window: { first: times[0] ?? null, last: times.at(-1) ?? null }, counts };
|
|
16
|
+
}
|
|
17
|
+
/** Tracked ledger records; a symlinked or unreadable entry is skipped with a warning, never followed. */
|
|
18
|
+
async function ledger(root, warnings) {
|
|
19
|
+
const directory = join(root, LEDGER_DIRECTORY);
|
|
20
|
+
try {
|
|
21
|
+
if (!(await lstat(directory)).isDirectory()) {
|
|
22
|
+
warnings.push(`${LEDGER_DIRECTORY} is not a directory.`);
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const records = [];
|
|
30
|
+
for (const name of (await readdir(directory)).filter(n => n.endsWith(".json")).sort()) {
|
|
31
|
+
try {
|
|
32
|
+
if (!(await lstat(join(directory, name))).isFile())
|
|
33
|
+
throw new Error("not a regular file");
|
|
34
|
+
const value = JSON.parse(await readFile(join(directory, name), "utf8"));
|
|
35
|
+
if (typeof value.label === "string" && typeof value.link?.state === "string")
|
|
36
|
+
records.push(value);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
warnings.push(`Skipped unreadable ledger record ${name}.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return records;
|
|
43
|
+
}
|
|
44
|
+
/** Advisory, read-only and fail-open: every source that cannot be read becomes a warning. */
|
|
45
|
+
export async function buildEscapesReport(root, home) {
|
|
46
|
+
const warnings = [];
|
|
47
|
+
const records = await ledger(root, warnings).catch(() => { warnings.push("Ledger unreadable."); return []; });
|
|
48
|
+
const events = await readLocalLog(home);
|
|
49
|
+
let origins = [];
|
|
50
|
+
try {
|
|
51
|
+
const state = JSON.parse(await readFile(join(root, ".agent-ops", "tasks", "state.json"), "utf8"));
|
|
52
|
+
origins = (state.tasks ?? []).filter(r => r.task?.origin !== undefined || r.task?.debug !== undefined)
|
|
53
|
+
.map(r => ({ ...(r.createdAt === undefined ? {} : { at: r.createdAt }), key: r.task?.origin?.kind ?? "undeclared" }));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
warnings.push("Task state unreadable; origin mix is empty.");
|
|
57
|
+
}
|
|
58
|
+
let rules = [];
|
|
59
|
+
try {
|
|
60
|
+
rules = await loadRules(root);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
warnings.push(`Rules unreadable: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
64
|
+
}
|
|
65
|
+
const ruleEvents = events.flatMap(e => e.event.type === "escape-rule" ? [e.event] : []);
|
|
66
|
+
const ruleRows = rules.map(({ rule }) => {
|
|
67
|
+
const fired = ruleEvents.filter(e => e.ruleId === rule.id);
|
|
68
|
+
return { id: rule.id, label: rule.label, noExclude: rule.noExclude, fires: fired.length,
|
|
69
|
+
included: fired.reduce((n, e) => n + e.included, 0), excluded: fired.reduce((n, e) => n + e.excluded, 0), status: status(fired.length) };
|
|
70
|
+
});
|
|
71
|
+
const links = section(records.map(r => ({ at: r.declaredAt, key: r.link.state })));
|
|
72
|
+
const reasons = {};
|
|
73
|
+
for (const r of records)
|
|
74
|
+
if (r.link.state === "unresolved")
|
|
75
|
+
reasons[r.link.reason] = (reasons[r.link.reason] ?? 0) + 1;
|
|
76
|
+
return {
|
|
77
|
+
misses: section(records.map(r => ({ at: r.declaredAt, key: r.label }))),
|
|
78
|
+
catches: section(events.flatMap(e => e.event.type === "gate-catch" ? [{ at: e.timestamp, key: e.event.code }] : [])),
|
|
79
|
+
origins: section(origins),
|
|
80
|
+
links: { ...links, reasons },
|
|
81
|
+
rules: ruleRows,
|
|
82
|
+
// Retirement is never automatic: only rules with enough fires that never surfaced an included place are listed.
|
|
83
|
+
retirementCandidates: ruleRows.filter(r => r.fires >= MIN_SAMPLE && r.included === 0)
|
|
84
|
+
.map(r => ({ id: r.id, reason: `fired ${r.fires} times; every hit was excluded (${r.excluded}) or none was included.` })),
|
|
85
|
+
warnings
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function render(report) {
|
|
89
|
+
const line = (name, s) => `${name}: n=${s.n}${s.status === "ok" ? "" : " (insufficient-data)"}` +
|
|
90
|
+
`${s.window.first === null ? "" : ` ${s.window.first} .. ${s.window.last}`}` +
|
|
91
|
+
`${Object.keys(s.counts).length === 0 ? "" : ` - ${Object.entries(s.counts).sort().map(([k, v]) => `${k} ${v}`).join(", ")}`}`;
|
|
92
|
+
return [
|
|
93
|
+
"Escape Loop report (advisory; raw counts; catches and rule fires come from this machine's local log)",
|
|
94
|
+
line("Misses by gate (ledger)", report.misses),
|
|
95
|
+
line("Catches by code (local log)", report.catches),
|
|
96
|
+
line("Origin mix (tasks)", report.origins),
|
|
97
|
+
line("Link states (ledger)", report.links) +
|
|
98
|
+
(Object.keys(report.links.reasons).length === 0 ? "" : `; unresolved reasons: ${Object.entries(report.links.reasons).sort().map(([k, v]) => `${k} ${v}`).join(", ")}`),
|
|
99
|
+
`Rules: ${report.rules.length}`,
|
|
100
|
+
...report.rules.map(r => `- ${r.id} (${r.label}${r.noExclude ? ", noExclude" : ""}): fires ${r.fires}, included ${r.included}, excluded ${r.excluded}` +
|
|
101
|
+
`${r.status === "ok" ? "" : " (insufficient-data)"}`),
|
|
102
|
+
`Retirement candidates: ${report.retirementCandidates.length === 0 ? "none" : report.retirementCandidates.map(c => `${c.id} (${c.reason})`).join("; ")}`,
|
|
103
|
+
...report.warnings.map(w => `Warning: ${w}`)
|
|
104
|
+
].join("\n") + "\n";
|
|
105
|
+
}
|
|
106
|
+
export async function runEscapesCommand(options) {
|
|
107
|
+
const report = await buildEscapesReport(options.root, options.home);
|
|
108
|
+
return okEnvelope("ESCAPES_REPORT", { ...report, text: render(report) });
|
|
109
|
+
}
|
|
@@ -58,7 +58,7 @@ export async function readFinishedReview(deps, mainRoot, commonDir, path) {
|
|
|
58
58
|
catch {
|
|
59
59
|
return invalid("Final review receipt is malformed.");
|
|
60
60
|
}
|
|
61
|
-
if (receipt.schemaVersion !== 1 || !/^[a-f0-9]{40,64}$/u.test(receipt.candidateHead) ||
|
|
61
|
+
if ((receipt.schemaVersion !== 1 && receipt.schemaVersion !== 2) || !/^[a-f0-9]{40,64}$/u.test(receipt.candidateHead) ||
|
|
62
62
|
!Array.isArray(receipt.tasks) || receipt.tasks.length === 0 ||
|
|
63
63
|
!["tree", "per-task-fallback"].includes(receipt.reviewMode) ||
|
|
64
64
|
typeof receipt.reviews !== "object" || receipt.reviews === null) {
|
|
@@ -17,7 +17,9 @@ import { calculateConfigHash, isPassingVerificationEvidence } from "../../../../
|
|
|
17
17
|
import { validateEvidence } from "../../../../runtime/src/schema/validate.js";
|
|
18
18
|
import { loadDebugProof, debugProofDigest } from "../../../../runtime/src/task/debug-state.js";
|
|
19
19
|
import { logSurfaceResidualRisks, runSurfaceGate } from "../../../../runtime/src/surface/discovery.js";
|
|
20
|
-
import {
|
|
20
|
+
import { escapeReviewData } from "../../../../runtime/src/escape/link.js";
|
|
21
|
+
import { logGateCatch } from "../../../../runtime/src/logging/local-log.js";
|
|
22
|
+
import { isCovers, surfaceDelta } from "../../../../runtime/src/surface/schema.js";
|
|
21
23
|
import { okEnvelope } from "../output.js";
|
|
22
24
|
/**
|
|
23
25
|
* Review runs against one target. Argument parsing already rejects a
|
|
@@ -318,8 +320,45 @@ const SURFACE_REASONS = {
|
|
|
318
320
|
SURFACE_EXCLUSION_TOO_BROAD: "surface-exclusion-too-broad",
|
|
319
321
|
SCENARIO_MISSING: "scenario-missing",
|
|
320
322
|
SURFACE_BUDGET_EXCEEDED: "surface-budget-exceeded",
|
|
321
|
-
SURFACE_UNASSIGNED: "surface-unassigned"
|
|
323
|
+
SURFACE_UNASSIGNED: "surface-unassigned",
|
|
324
|
+
SURFACE_DISMISSAL_TOO_BROAD: "surface-dismissal-too-broad",
|
|
325
|
+
SURFACE_DISMISSAL_INVALID: "surface-dismissal-invalid",
|
|
326
|
+
ESCAPE_RULE_INVALID: "escape-rule-invalid",
|
|
327
|
+
ESCAPE_RULE_UNSAFE: "escape-rule-unsafe",
|
|
328
|
+
ESCAPE_RULE_NO_EXCLUDE: "escape-rule-no-exclude"
|
|
322
329
|
};
|
|
330
|
+
/** Escape Loop: a declared escape-of origin gives a wrong label a criterion a blocking finding can name. */
|
|
331
|
+
export const ESCAPE_LABEL = {
|
|
332
|
+
id: "escape-label",
|
|
333
|
+
description: "Each declared escape's primary label names the earliest gate that could have caught it (requirement, surface, " +
|
|
334
|
+
"scenario, criteria, verify, review; flagged if a residual risk or finding already said it; out-of-model if no gate could), " +
|
|
335
|
+
"is consistent with the mechanical surface facts, and its tracked .agent-ops-escapes record is current."
|
|
336
|
+
};
|
|
337
|
+
/** Escape Loop packet rows for every task in scope that declares an origin; linking stays advisory. */
|
|
338
|
+
async function reviewEscapes(options, context, scope) {
|
|
339
|
+
if (options.root === undefined || options.gitRunner === undefined)
|
|
340
|
+
return [];
|
|
341
|
+
const rows = [];
|
|
342
|
+
for (const record of context.records) {
|
|
343
|
+
const origin = record.task.origin;
|
|
344
|
+
if (origin === undefined)
|
|
345
|
+
continue;
|
|
346
|
+
const base = record.debug?.baselineCommit ?? record.createdSourceCommit ?? (scope?.mode === "base" ? scope.resolvedBase : undefined);
|
|
347
|
+
try {
|
|
348
|
+
if (base === undefined)
|
|
349
|
+
throw new Error("no base");
|
|
350
|
+
rows.push(await escapeReviewData({ root: options.root, git: options.gitRunner, taskId: record.task.id, origin, base }));
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
rows.push({ taskId: record.task.id, origin, link: { state: "unresolved", reason: "link-unavailable" } });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return rows;
|
|
357
|
+
}
|
|
358
|
+
async function logCatch(options, code, taskId) {
|
|
359
|
+
if (taskId !== undefined && options.config?.profiles.includes("advisory") === true)
|
|
360
|
+
await logGateCatch(code, taskId);
|
|
361
|
+
}
|
|
323
362
|
/** Review-only: gives an omission a criterion that a blocking finding can name. */
|
|
324
363
|
export const SURFACE_COVERAGE = {
|
|
325
364
|
id: "surface-coverage",
|
|
@@ -359,8 +398,11 @@ async function reviewSurface(options, context, scope) {
|
|
|
359
398
|
return { gate };
|
|
360
399
|
return { gate, packet: {
|
|
361
400
|
audit: holder.task.surface,
|
|
362
|
-
|
|
363
|
-
|
|
401
|
+
// Deltas, not whole previous audits: a large audit revised a few times must still fit the review packet.
|
|
402
|
+
history: (holder.revisions ?? []).map((revision, i, all) => ({ at: revision.at, reason: revision.reason,
|
|
403
|
+
change: surfaceDelta(revision.previousTask.surface, (all[i + 1]?.previousTask ?? holder.task).surface) })).slice(-10),
|
|
404
|
+
discovery: { cached: gate.discovery.cached, sources: gate.discovery.sources, skipped: gate.discovery.skipped,
|
|
405
|
+
dismissed: gate.discovery.dismissed },
|
|
364
406
|
postBlockExclusions: gate.discovery.postBlockExclusions
|
|
365
407
|
} };
|
|
366
408
|
}
|
|
@@ -658,16 +700,19 @@ export async function runReviewCommand(options) {
|
|
|
658
700
|
});
|
|
659
701
|
}
|
|
660
702
|
if (context.policyConfigHash !== options.policyConfigHash) {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
703
|
+
const result = { ...resultBase, status: "NOT_RUN", reason: "reviewer-policy-changed", scope };
|
|
704
|
+
const own = context.records.find(r => r.task.id === context.taskId).task;
|
|
705
|
+
const message = "Independent review was not run.";
|
|
706
|
+
return { code: "REVIEW_NOT_RUN", status: "error", errors: [{ code: "REVIEW_NOT_RUN", message }], data: { message, result,
|
|
707
|
+
text: renderReviewResult(result) +
|
|
708
|
+
`The configuration changed after task ${context.taskId} was created. Rebind it, then verify again:\n` +
|
|
709
|
+
` agent-ops task revise --task ${context.taskId} --expected-contract ${taskContractHash(own)} --reason "Rebind to the current configuration"\n` } };
|
|
666
710
|
}
|
|
667
711
|
}
|
|
668
712
|
if (context !== undefined && options.tasks !== undefined) {
|
|
669
713
|
const surface = await reviewSurface(options, context, scope);
|
|
670
714
|
if (surface.gate.status === "NOT_RUN") {
|
|
715
|
+
await logCatch(options, surface.gate.code, context.taskId);
|
|
671
716
|
return surfaceNotRunEnvelope({
|
|
672
717
|
...resultBase,
|
|
673
718
|
status: "NOT_RUN",
|
|
@@ -733,9 +778,11 @@ export async function runReviewCommand(options) {
|
|
|
733
778
|
candidateCommit: await resolveGitCommit(options.gitRunner, "HEAD"), coverageDigest: coverageDigest(rows)
|
|
734
779
|
};
|
|
735
780
|
}
|
|
781
|
+
const escapes = context === undefined ? [] : await reviewEscapes(options, context, scope);
|
|
736
782
|
const criteria = [
|
|
737
783
|
...(context?.criteria ?? GENERIC_CRITERIA),
|
|
738
|
-
...(surfacePacket === undefined || context?.criteria.some(c => c.id === SURFACE_COVERAGE.id) === true ? [] : [SURFACE_COVERAGE])
|
|
784
|
+
...(surfacePacket === undefined || context?.criteria.some(c => c.id === SURFACE_COVERAGE.id) === true ? [] : [SURFACE_COVERAGE]),
|
|
785
|
+
...(!context?.records.some(r => r.task.origin?.kind === "escape-of") || context.criteria.some(c => c.id === ESCAPE_LABEL.id) ? [] : [ESCAPE_LABEL])
|
|
739
786
|
];
|
|
740
787
|
// Tree attestations bind the exact criterion list the reviewer answered.
|
|
741
788
|
if (context !== undefined)
|
|
@@ -784,6 +831,7 @@ export async function runReviewCommand(options) {
|
|
|
784
831
|
}
|
|
785
832
|
packet = buildReviewPacket({
|
|
786
833
|
...(surfacePacket === undefined ? {} : { surface: surfacePacket }),
|
|
834
|
+
...(escapes.length === 0 ? {} : { escape: escapes }),
|
|
787
835
|
...(contractManifest === undefined ? {} : { contractManifest, contractArtifacts }),
|
|
788
836
|
request: context?.title ?? GENERIC_REQUEST,
|
|
789
837
|
criteria,
|
|
@@ -922,6 +970,8 @@ export async function runReviewCommand(options) {
|
|
|
922
970
|
if (finalResult.status === "PASS") {
|
|
923
971
|
return okEnvelope("REVIEW_RESULT", data);
|
|
924
972
|
}
|
|
973
|
+
if (finalResult.status === "FAIL")
|
|
974
|
+
await logCatch(options, "REVIEW_FAIL", context?.taskId);
|
|
925
975
|
const code = finalResult.status === "FAIL" ? "REVIEW_FAILED" : "REVIEW_NOT_RUN";
|
|
926
976
|
return {
|
|
927
977
|
code,
|
|
@@ -30,6 +30,35 @@ async function jsonFile(path) {
|
|
|
30
30
|
throw new AgentOpsError("TASK_PAYLOAD_INVALID", "Invalid JSON payload.");
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
async function escapeOrigin(args, service) {
|
|
34
|
+
return args.escapeOf === undefined ? undefined : await service.resolveOrigin({ kind: "escape-of", commit: args.escapeOf,
|
|
35
|
+
label: args.escapeLabel, ...(args.escapeReceipt === undefined ? {} : { receipt: args.escapeReceipt }),
|
|
36
|
+
...(args.escapeFix === undefined ? {} : { fix: args.escapeFix }), ...(args.escapePromote === undefined ? {} : { promote: args.escapePromote }) });
|
|
37
|
+
}
|
|
38
|
+
/** Link state, label and blame hints are printed; the ledger record is committed with the fix. */
|
|
39
|
+
async function withEscape(envelope, escape, service) {
|
|
40
|
+
if (escape === undefined || envelope.data === null)
|
|
41
|
+
return envelope;
|
|
42
|
+
const { link, label, agentLabel, facts, proof } = escape.record;
|
|
43
|
+
const proposal = proof?.status === "check" && proof.check !== null ? await service.escapeConfigProposal(proof.check) : undefined;
|
|
44
|
+
const lines = [
|
|
45
|
+
`Escape of ${escape.record.escapeOf}: ${link.state}${link.state === "unresolved" ? ` (${link.reason})` : ""}` +
|
|
46
|
+
`${link.state === "gated" ? ` via receipt ${link.receipt} (${link.matchedBy})` : ""}; label ${label}${label === agentLabel ? "" : ` (agent proposed ${agentLabel})`}.`,
|
|
47
|
+
...facts.map(fact => `- ${fact.path}: ${fact.surface}`),
|
|
48
|
+
...(proof === undefined ? [] : [proof.status === "proven"
|
|
49
|
+
? `Proof: rule ${proof.rule} replays at the escaped commit; backtest ${proof.backtest.fires}/${proof.backtest.commits} commits (${proof.backtest.rate}).`
|
|
50
|
+
: proof.status === "unproven" ? `Proof: ${proof.code} - ${proof.reason} Finish refuses this until a tracked rule proves it.`
|
|
51
|
+
: proof.status === "lesson" ? `Lesson (unproven, does not block): ${proof.reason}`
|
|
52
|
+
: proof.check === null ? "Proof: ESCAPE_RULE_UNPROVEN - name the red-to-green check with --escape-promote <runnerId:checkId>."
|
|
53
|
+
: `Proof: promoted check ${proof.check} must be a proven redCheckId of this task. Config proposal (not written):\n${JSON.stringify(proposal, null, 2)}`]),
|
|
54
|
+
`Ledger record: ${escape.path} (commit it with the fix).`,
|
|
55
|
+
escape.blame.length === 0 ? "Blame hints: none (the fix only adds lines; an omission has no author)."
|
|
56
|
+
: `Blame hints (a hint only): ${escape.blame.map(hint => `${hint.commit.slice(0, 12)} (${hint.lines} line${hint.lines === 1 ? "" : "s"})`).join(", ")}`
|
|
57
|
+
];
|
|
58
|
+
return { ...envelope, data: { ...envelope.data, escape: { ...escape.record, blame: escape.blame, path: escape.path,
|
|
59
|
+
...(proposal === undefined ? {} : { configProposal: proposal }) },
|
|
60
|
+
text: `${envelope.data.text ?? ""}${envelope.data.text?.endsWith("\n") === false ? "\n" : ""}${lines.join("\n")}\n` } };
|
|
61
|
+
}
|
|
33
62
|
/** An unreadable surface file is a missing surface, not a generic payload error. */
|
|
34
63
|
async function surfaceFile(path) {
|
|
35
64
|
try {
|
|
@@ -105,6 +134,7 @@ export async function runTaskCommand(options) {
|
|
|
105
134
|
const worktree = sessionId === undefined || options.sessionWorktree === undefined
|
|
106
135
|
? undefined
|
|
107
136
|
: await options.sessionWorktree(sessionId);
|
|
137
|
+
const origin = await escapeOrigin(options.args, worktree?.service ?? options.service);
|
|
108
138
|
const record = await (worktree?.service ?? options.service).create({
|
|
109
139
|
title: options.args.title,
|
|
110
140
|
...(options.args.goal === undefined ? {} : { goal: options.args.goal }),
|
|
@@ -122,19 +152,21 @@ export async function runTaskCommand(options) {
|
|
|
122
152
|
: { parentTaskId: options.args.parentTaskId }),
|
|
123
153
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
124
154
|
...(options.args.surfaceFile === undefined ? {} : { surface: await surfaceFile(options.args.surfaceFile) }),
|
|
155
|
+
...(origin === undefined ? {} : { origin }),
|
|
125
156
|
requireSurface: true
|
|
126
157
|
});
|
|
158
|
+
const escape = origin === undefined ? undefined : await (worktree?.service ?? options.service).declareEscape(record.task.id);
|
|
127
159
|
// An explicit CLI DEBUG intake may be the only start signal on hosts
|
|
128
160
|
// without a prompt hook (notably agy). The native session owns the clock;
|
|
129
161
|
// foregroundStart is idempotent for a repeated hook or command.
|
|
130
162
|
if (options.args.debug && sessionId !== undefined) {
|
|
131
163
|
await (worktree?.service ?? options.service).debugService().foregroundStart(record.task.id, sessionId);
|
|
132
164
|
}
|
|
133
|
-
const created = taskEnvelope(action, "TASK_CREATED", sessionId === undefined
|
|
165
|
+
const created = await withEscape(taskEnvelope(action, "TASK_CREATED", sessionId === undefined
|
|
134
166
|
? `Created task ${record.task.id}.`
|
|
135
167
|
: record.task.parentTaskId === undefined
|
|
136
168
|
? `Created and attached task ${record.task.id}.`
|
|
137
|
-
: `Created subtask ${record.task.id}; the session stays on the top of its task tree.`, record);
|
|
169
|
+
: `Created subtask ${record.task.id}; the session stays on the top of its task tree.`, record), escape, worktree?.service ?? options.service);
|
|
138
170
|
if (worktree === undefined || created.data === null)
|
|
139
171
|
return created;
|
|
140
172
|
return {
|
|
@@ -157,6 +189,10 @@ export async function runTaskCommand(options) {
|
|
|
157
189
|
}
|
|
158
190
|
if (action === "debug") {
|
|
159
191
|
const id = requireTaskId(options.args), service = options.service.debugService();
|
|
192
|
+
if (options.args.debugAction === "origin") {
|
|
193
|
+
const declared = await service.origin(id, await jsonFile(options.args.payloadFile));
|
|
194
|
+
return await withEscape(taskEnvelope(action, "TASK_DEBUG_ORIGIN_DECLARED", "Origin bound into the contract; re-verify, re-record the confirmed hypothesis, then review.", declared.record), declared.escape, options.service);
|
|
195
|
+
}
|
|
160
196
|
switch (options.args.debugAction) {
|
|
161
197
|
case "hypothesis":
|
|
162
198
|
await service.hypothesis(id, await jsonFile(options.args.payloadFile));
|
|
@@ -191,13 +227,15 @@ export async function runTaskCommand(options) {
|
|
|
191
227
|
const payload = options.args.criteriaFile === undefined ? undefined : await jsonFile(options.args.criteriaFile);
|
|
192
228
|
if (payload !== undefined && !Array.isArray(payload))
|
|
193
229
|
throw new AgentOpsError("TASK_PAYLOAD_INVALID", "Criteria file must contain an array.");
|
|
230
|
+
const origin = await escapeOrigin(options.args, options.service);
|
|
194
231
|
const record = await options.service.revise(requireTaskId(options.args), {
|
|
195
232
|
expectedContractHash: options.args.expectedContract,
|
|
196
233
|
...(payload === undefined ? {} : { criteria: payload }),
|
|
197
234
|
...(options.args.surfaceFile === undefined ? {} : { surface: await surfaceFile(options.args.surfaceFile) }),
|
|
235
|
+
...(origin === undefined ? {} : { origin }),
|
|
198
236
|
reason: options.args.reason, ...(options.args.baseline === undefined ? {} : { baseline: options.args.baseline })
|
|
199
237
|
});
|
|
200
|
-
return taskEnvelope(action, "TASK_REVISED", "Contract revised; obtain new verification and fresh review.", record);
|
|
238
|
+
return await withEscape(taskEnvelope(action, "TASK_REVISED", "Contract revised; obtain new verification and fresh review.", record), origin === undefined ? undefined : await options.service.declareEscape(record.task.id), options.service);
|
|
201
239
|
}
|
|
202
240
|
if (action === "replan") {
|
|
203
241
|
const payload = await jsonFile(options.args.planFile);
|
|
@@ -8,6 +8,11 @@ import { okEnvelope } from "../output.js";
|
|
|
8
8
|
import { formatOperationPlan } from "../plan-output.js";
|
|
9
9
|
import { toPublicUpdatePlan } from "../public-plan.js";
|
|
10
10
|
import { formatTrustChange, planTrustGrant } from "./trust.js";
|
|
11
|
+
/** One plan line, so the user approves the worktrees together with the commands. */
|
|
12
|
+
export function worktreeSyncLine(worktrees) {
|
|
13
|
+
return worktrees.length === 0 ? "" :
|
|
14
|
+
`Session worktrees that receive this config and its trust: ${worktrees.join(", ")}\n`;
|
|
15
|
+
}
|
|
11
16
|
function repairedLines(plan) {
|
|
12
17
|
return plan.installation.repaired.map(({ path, reason }) => `repaired: ${path} (${reason.replace("-", " ")})`);
|
|
13
18
|
}
|
|
@@ -146,19 +151,21 @@ export async function runUpdateCommand(options) {
|
|
|
146
151
|
...(worktree === undefined ? {} : { worktree })
|
|
147
152
|
});
|
|
148
153
|
const trust = await trustChange(options, plan);
|
|
154
|
+
const worktrees = await options.sessionWorktrees?.list().catch(() => []) ?? [];
|
|
149
155
|
if (options.args.dryRun) {
|
|
150
156
|
return okEnvelope("UPDATE_PLAN_READY", {
|
|
151
157
|
applied: false,
|
|
152
158
|
plan: toPublicUpdatePlan(plan, trust),
|
|
153
159
|
message: "Update plan calculated; no files were changed.",
|
|
154
|
-
text: formatUpdatePlan(plan, trust)
|
|
160
|
+
text: formatUpdatePlan(plan, trust) + worktreeSyncLine(worktrees),
|
|
161
|
+
...(worktrees.length === 0 ? {} : { worktrees })
|
|
155
162
|
});
|
|
156
163
|
}
|
|
157
164
|
if (!options.args.yes && !options.isTTY) {
|
|
158
165
|
return updateError("UPDATE_CONFIRMATION_REQUIRED", "Non-interactive update requires --yes.", plan, trust);
|
|
159
166
|
}
|
|
160
167
|
if (!options.args.yes &&
|
|
161
|
-
!(await options.confirm(plan, trust))) {
|
|
168
|
+
!(await options.confirm(plan, trust, worktrees))) {
|
|
162
169
|
return updateError("UPDATE_CANCELLED", "Update was cancelled; no files were changed.", plan, trust);
|
|
163
170
|
}
|
|
164
171
|
await applyUpdatePlan(options.root, plan);
|
|
@@ -173,16 +180,21 @@ export async function runUpdateCommand(options) {
|
|
|
173
180
|
return updateError("UPDATE_TRUST_FAILED", "Update was applied, but repository trust was not granted. Run `agent-ops trust grant --yes`.", plan, trust, true);
|
|
174
181
|
}
|
|
175
182
|
}
|
|
183
|
+
// Fail-open: a worktree that cannot be synced keeps its old trust and says so via trust status.
|
|
184
|
+
const synced = worktrees.length === 0 ? [] : await options.sessionWorktrees?.sync().catch(() => []) ?? [];
|
|
185
|
+
const syncedLines = synced.length === 0 ? [] : [`Synced config and trust into session worktrees: ${synced.join(", ")}.`];
|
|
176
186
|
return okEnvelope("UPDATE_APPLIED", {
|
|
177
187
|
applied: true,
|
|
178
188
|
plan: toPublicUpdatePlan(plan, trust),
|
|
179
189
|
message: `Managed installation updated to ${plan.targetVersion}.`,
|
|
180
|
-
...(
|
|
190
|
+
...(synced.length === 0 ? {} : { worktrees: synced }),
|
|
191
|
+
...(plan.installation.repaired.length === 0 && synced.length === 0
|
|
181
192
|
? {}
|
|
182
193
|
: {
|
|
183
194
|
text: [
|
|
184
195
|
`Managed installation updated to ${plan.targetVersion}.`,
|
|
185
|
-
...repairedLines(plan)
|
|
196
|
+
...repairedLines(plan),
|
|
197
|
+
...syncedLines
|
|
186
198
|
].join("\n")
|
|
187
199
|
})
|
|
188
200
|
});
|
|
@@ -72,7 +72,10 @@ function formatReport(report) {
|
|
|
72
72
|
const lines = [
|
|
73
73
|
`Verification task ${safe(report.taskId)}: ${report.status}`,
|
|
74
74
|
`Scope: ${report.selection.reason} (${report.selection.fallback ? "fallback" : "bounded"})`,
|
|
75
|
-
...report.results.flatMap(formatResult)
|
|
75
|
+
...report.results.flatMap(formatResult),
|
|
76
|
+
// A timed-out runner step says which step and which limit; plain UNKNOWN left people guessing.
|
|
77
|
+
...(report.acceptance ?? []).flatMap(row => row.detail === undefined ? [] :
|
|
78
|
+
[`- ${row.status} ${safe(row.runnerId)} (${row.criterionId}, ${row.phase}): ${safe(row.detail)}`])
|
|
76
79
|
];
|
|
77
80
|
if (report.signal !== null) {
|
|
78
81
|
lines.push(`Signal: ${report.signal}`);
|
|
@@ -171,7 +171,7 @@ export async function routeDebugPrompt(root, input, host = "claude") {
|
|
|
171
171
|
}
|
|
172
172
|
else
|
|
173
173
|
clockNote = " Native foreground timing is unsupported on this host; controlled operations remain evidence-timed and do not claim a complete active-time budget.";
|
|
174
|
-
return `DEBUG issue ${created.task.id} created in ${path}. Preserve the original goal. Investigate, record falsifiable hypotheses, revise provisional criteria to relevant behavioral regression, add the surface audit (task revise --surface-file) before the first repair, repair, verify, obtain two fresh independent reviews, then finish. A clean HEAD baseline is fixed automatically; do not ask permission for it or offer diagnosis-only as a fallback. Failure to reproduce alone is not a stop condition: continue discriminating checks within the budget. Defaults: 60 active minutes / 5 material repair candidates. No user-authored debug plan is required.${clockNote}`;
|
|
174
|
+
return `DEBUG issue ${created.task.id} created in ${path}. Preserve the original goal. Investigate, record falsifiable hypotheses, revise provisional criteria to relevant behavioral regression, add the surface audit (task revise --surface-file) before the first repair, repair, declare the origin (task debug --debug-action origin) once a hypothesis is confirmed, verify, obtain two fresh independent reviews, then finish. A clean HEAD baseline is fixed automatically; do not ask permission for it or offer diagnosis-only as a fallback. Failure to reproduce alone is not a stop condition: continue discriminating checks within the budget. Defaults: 60 active minutes / 5 material repair candidates. No user-authored debug plan is required.${clockNote}`;
|
|
175
175
|
}
|
|
176
176
|
export async function interruptDebugSession(root, sessionId, nativeInput) {
|
|
177
177
|
const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
|