@kylecheng3146/agent-ops 0.11.1 → 0.11.3
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 +9 -1
- package/dist/packages/cli/src/args.js +68 -2
- package/dist/packages/cli/src/bin.js +11 -1
- package/dist/packages/cli/src/cli.js +29 -0
- package/dist/packages/cli/src/commands/advance.js +3 -2
- package/dist/packages/cli/src/commands/followup.js +74 -0
- package/dist/packages/cli/src/commands/parallel.js +4 -0
- package/dist/packages/cli/src/commands/review.js +15 -1
- package/dist/packages/cli/src/commands/run.js +1 -1
- package/dist/packages/cli/src/commands/task.js +8 -2
- package/dist/packages/cli/src/context.js +13 -9
- package/dist/packages/cli/src/criteria-review.js +8 -3
- package/dist/packages/cli/src/followup-deps.js +56 -0
- package/dist/packages/cli/src/run-deps.js +16 -5
- package/dist/packages/cli/src/run-entry.js +159 -64
- package/dist/runtime/src/followup/intake.js +75 -0
- package/dist/runtime/src/followup/process.js +273 -0
- package/dist/runtime/src/followup/receipt.js +116 -0
- package/dist/runtime/src/followup/store.js +133 -0
- package/dist/runtime/src/install/harness.js +1 -1
- package/dist/runtime/src/install/preauth.js +11 -4
- package/dist/runtime/src/parallel/finish.js +10 -2
- package/dist/runtime/src/review/attestation.js +45 -13
- package/dist/runtime/src/review/criteria.js +22 -5
- package/dist/runtime/src/review/followup.js +233 -0
- package/dist/runtime/src/review/report.js +8 -4
- package/dist/runtime/src/review/runner.js +30 -2
- package/dist/runtime/src/run/service.js +59 -6
- package/dist/runtime/src/run/usage.js +5 -2
- package/dist/runtime/src/security/permissions.js +16 -1
- package/dist/runtime/src/verify/acceptance-coverage.js +2 -2
- package/dist/runtime/src/verify/service.js +62 -2
- package/dist/runtime/src/verify/spawn.js +64 -2
- package/docs/en/guides/configuration.md +5 -0
- package/docs/en/guides/task-debug.md +36 -0
- package/docs/en/spec/acceptance-and-evidence.md +54 -0
- package/docs/en/spec/loop-engineering.md +36 -0
- package/docs/en/spec/review.md +37 -3
- package/docs/zh-TW/guides/configuration.md +3 -0
- package/docs/zh-TW/guides/task-debug.md +31 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +39 -0
- package/docs/zh-TW/spec/loop-engineering.md +21 -0
- package/docs/zh-TW/spec/review.md +24 -4
- package/package.json +1 -1
- package/schemas/review-report.schema.json +2 -1
|
@@ -322,10 +322,32 @@ const RECOVERABLE = new Set(["RUN_RECOVERY_DIRTY", "RUN_NATIVE_VERSION_CHANGED",
|
|
|
322
322
|
function resumeBlocker(current, acknowledged = new Set()) {
|
|
323
323
|
return [...current.events].reverse().find(e => !acknowledged.has(e.id) && (RECOVERABLE.has(e.code) || ["RUN_NO_PROGRESS_REPEAT", "RUN_COMMAND_DENIED"].includes(e.code)));
|
|
324
324
|
}
|
|
325
|
-
function cannotResume(current, acknowledged = new Set()) {
|
|
325
|
+
function cannotResume(current, acknowledged = new Set(), retryingDelivery = false) {
|
|
326
326
|
return current.status === "complete" || current.events.some(e => e.code === "RUN_COMMAND_DENIED" && !acknowledged.has(e.id)) ||
|
|
327
327
|
(current.integration?.proofDigest === undefined && (current.workers.some(w => w.workerId === current.coordinatorId && w.status === "blocked" && w.lastFailure !== null && w.lastFailure.failureClass !== "native-start") ||
|
|
328
|
-
(current.status === "blocked" && !RECOVERABLE.has(resumeBlocker(current, acknowledged)?.code ?? "")
|
|
328
|
+
(current.status === "blocked" && !RECOVERABLE.has(resumeBlocker(current, acknowledged)?.code ?? "") &&
|
|
329
|
+
!(retryingDelivery && resumeBlocker(current, acknowledged) === undefined))));
|
|
330
|
+
}
|
|
331
|
+
/** A manual retry grants a fresh attempt, never proof or command authorization. */
|
|
332
|
+
export function validateDeliveryRetry(state, taskId, now = Date.now()) {
|
|
333
|
+
const task = state.tasks.find(t => t.taskId === taskId);
|
|
334
|
+
const worker = state.workers.find(w => w.workerId === task?.workerId && w.taskId === taskId);
|
|
335
|
+
const intent = worker?.stopIntent;
|
|
336
|
+
const dead = (w) => !["assigned", "starting", "running", "idle", "handing-off"].includes(w.status) &&
|
|
337
|
+
(w.nativeSessionId === null && w.processId === null || w.stopIntent?.runId === state.runId &&
|
|
338
|
+
w.stopIntent.workerId === w.workerId && w.stopIntent.generation === w.generation &&
|
|
339
|
+
w.stopIntent.nativeSessionId === w.nativeSessionId && w.stopIntent.confirmedDeadAt !== null &&
|
|
340
|
+
Number.isFinite(Date.parse(w.stopIntent.confirmedDeadAt)) && Date.parse(w.stopIntent.confirmedDeadAt) <= now);
|
|
341
|
+
if (["complete", "failed", "stopping"].includes(state.status) || state.integration !== null || state.proofProcess != null ||
|
|
342
|
+
task === undefined || task.taskId === state.rootTaskId || task.status !== "blocked" || task.blockedReason !== "RUN_FINAL_PROOF_INTERRUPTED" ||
|
|
343
|
+
task.deliveryDigest !== null || task.sourceCommit !== null || worker === undefined || worker.workerId === state.coordinatorId ||
|
|
344
|
+
worker.status !== "blocked" || worker.lastFailure?.failureClass !== "RUN_FINAL_PROOF_INTERRUPTED" ||
|
|
345
|
+
worker.lastFailure.checkId !== "RUN_FINAL_PROOF_INTERRUPTED" || worker.lastFailure.pinId !== null ||
|
|
346
|
+
intent?.reason !== "handoff" || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(intent.sourceCommit ?? "") ||
|
|
347
|
+
!/^[a-f0-9]{64}$/u.test(intent.contractDigest ?? "") || !/^[a-f0-9]{64}$/u.test(intent.deliveryDigest ?? "") ||
|
|
348
|
+
state.workers.some(w => w.proofProcess != null || !dead(w)))
|
|
349
|
+
throw new AgentOpsError("RUN_DELIVERY_RETRY_INVALID", "Retry requires an interrupted child handoff, unchanged identity and confirmed stopped writers/proof; inspect run status first.");
|
|
350
|
+
return worker;
|
|
329
351
|
}
|
|
330
352
|
/** Observations are not proof validation; resume still reconciles every authority and sealed artifact. */
|
|
331
353
|
export function describeRunRecovery(state, now = Date.now()) {
|
|
@@ -418,12 +440,21 @@ export class RunService {
|
|
|
418
440
|
}));
|
|
419
441
|
return { state, message: `Run ${runId} is paused.` };
|
|
420
442
|
}
|
|
421
|
-
async resume(runId, retryApprovalIds = [], upgradeBudget = true) {
|
|
443
|
+
async resume(runId, retryApprovalIds = [], upgradeBudget = true, retryDeliveryTaskId, expectedFailureId) {
|
|
422
444
|
const state = await this.#repository.mutate(runId, async (current) => {
|
|
423
|
-
|
|
445
|
+
if (retryDeliveryTaskId !== undefined && retryApprovalIds.length > 0)
|
|
446
|
+
throw new AgentOpsError("RUN_DELIVERY_RETRY_INVALID", "Delivery and approval retries require separate explicit controls.");
|
|
447
|
+
const retry = retryDeliveryTaskId === undefined ? null : validateDeliveryRetry(current, retryDeliveryTaskId, Date.parse(this.#now()));
|
|
448
|
+
if (retry !== null && expectedFailureId !== undefined && retry.lastFailure.id !== expectedFailureId)
|
|
449
|
+
throw new AgentOpsError("RUN_DELIVERY_RETRY_INVALID", "The interrupted attempt changed during recovery.");
|
|
450
|
+
const acknowledged = new Set(await approvalRetryEvents(current, retryApprovalIds));
|
|
451
|
+
if (retry !== null)
|
|
452
|
+
for (const e of current.events)
|
|
453
|
+
if (e.code === "RUN_NO_PROGRESS_REPEAT" && e.workerId === retry.workerId && e.taskId === retry.taskId)
|
|
454
|
+
acknowledged.add(e.id);
|
|
424
455
|
const recoverable = RECOVERABLE;
|
|
425
456
|
const blocker = resumeBlocker(current, acknowledged);
|
|
426
|
-
if (cannotResume(current, acknowledged)) {
|
|
457
|
+
if (cannotResume(current, acknowledged, retry !== null)) {
|
|
427
458
|
throw new AgentOpsError("RUN_NOT_RESUMABLE", `Run ${runId} is ${current.status}.`);
|
|
428
459
|
}
|
|
429
460
|
if (current.integration?.proofDigest === undefined && !upgradeBudget && budgetExpired(current, Date.parse(this.#now())))
|
|
@@ -439,11 +470,17 @@ export class RunService {
|
|
|
439
470
|
...(recover ? { workers: current.workers.map(w => w.status === "blocked" && (w.lastFailure === null || w.lastFailure.failureClass === "native-start") &&
|
|
440
471
|
!current.tasks.some(t => t.taskId === w.taskId && t.status === "blocked" && !recoverable.has(t.blockedReason ?? "")) ? { ...w, status: "stopped" } : w),
|
|
441
472
|
tasks: current.tasks.map(t => t.status === "blocked" && recoverable.has(t.blockedReason ?? "") ? { ...t, status: "ready", blockedReason: null } : t) } : {}),
|
|
473
|
+
...(retry === null ? {} : {
|
|
474
|
+
workers: current.workers.map(w => w.workerId === retry.workerId ? { ...w, status: "stopped", lastFailure: null } : w),
|
|
475
|
+
tasks: current.tasks.map(t => t.taskId === retry.taskId ? { ...t, status: "ready", blockedReason: null } : t)
|
|
476
|
+
}),
|
|
442
477
|
status: "active",
|
|
443
478
|
awaitingResume: false,
|
|
444
479
|
disableRestart: false,
|
|
445
480
|
controls: [...current.controls, {
|
|
446
|
-
action: "resume", actor: "user", reason:
|
|
481
|
+
action: "resume", actor: "user", reason: retry !== null ? "retry-delivery:" + JSON.stringify({ taskId: retry.taskId,
|
|
482
|
+
workerId: retry.workerId, generation: retry.generation, failure: retry.lastFailure }) :
|
|
483
|
+
retryApprovalIds.length === 0 ? null : "retry-approval:" + JSON.stringify(retryApprovalIds), requestedAt: this.#now(), expiresAt: null, appliedAt: null
|
|
447
484
|
}]
|
|
448
485
|
};
|
|
449
486
|
});
|
|
@@ -461,9 +498,25 @@ export class RunService {
|
|
|
461
498
|
if (question.answeredAt !== null)
|
|
462
499
|
throw new AgentOpsError("RUN_QUESTION_ANSWERED", `Question already answered: ${cleanQuestion}`);
|
|
463
500
|
const answeredAt = this.#now();
|
|
501
|
+
const coordinator = current.workers.find(worker => worker.workerId === current.coordinatorId);
|
|
502
|
+
const intent = coordinator?.stopIntent;
|
|
503
|
+
const wake = coordinator?.status === "stopped" && coordinator.taskId === current.rootTaskId &&
|
|
504
|
+
coordinator.nativeSessionId === null && coordinator.nativeJobId === null && coordinator.nativeGoalState === "inactive" &&
|
|
505
|
+
coordinator.proofProcess == null && intent?.reason === "handoff" && intent.runId === runId &&
|
|
506
|
+
intent.workerId === coordinator.workerId && intent.generation === coordinator.generation &&
|
|
507
|
+
intent.confirmedDeadAt !== null && Number.isFinite(Date.parse(intent.confirmedDeadAt)) &&
|
|
508
|
+
Date.parse(intent.confirmedDeadAt) <= Date.parse(answeredAt);
|
|
464
509
|
return {
|
|
465
510
|
...current,
|
|
466
511
|
status: "active",
|
|
512
|
+
// Consume this wait once; ordinary resume still requires death reconciliation and a fresh lease.
|
|
513
|
+
...(wake ? {
|
|
514
|
+
workers: current.workers.map(worker => worker.workerId === current.coordinatorId
|
|
515
|
+
? { ...worker, stopIntent: { ...intent, reason: "stop" } } : worker),
|
|
516
|
+
events: [...current.events, event({ type: "diagnostic", code: "RUN_COORDINATOR_RESPONSE_WAKE",
|
|
517
|
+
workerId: coordinator.workerId, taskId: coordinator.taskId,
|
|
518
|
+
detail: `Answer ${cleanQuestion} released coordinator wait at generation ${coordinator.generation}; normal recovery is required.` }, answeredAt)].slice(-MAX_EVENTS)
|
|
519
|
+
} : {}),
|
|
467
520
|
questions: current.questions.map((item) => item.questionId === cleanQuestion
|
|
468
521
|
? { ...item, answeredAt, answerDigest: sha256(cleanAnswer) }
|
|
469
522
|
: item),
|
|
@@ -66,7 +66,9 @@ export async function recordNativeRunUsage(repository, event) {
|
|
|
66
66
|
}
|
|
67
67
|
else if (event.host === "codex" && plain(payload.tokenUsage) && plain(payload.tokenUsage.total)) {
|
|
68
68
|
const usage = payload.tokenUsage.total;
|
|
69
|
-
|
|
69
|
+
const threadKey = worker.nativeJobId !== null && worker.nativeJobId !== worker.nativeSessionId
|
|
70
|
+
? `${worker.nativeSessionId}:${worker.nativeJobId}` : worker.nativeSessionId;
|
|
71
|
+
incoming = { source: "codex", epoch: `${worker.workerId}:${threadKey}:thread`, inputTokens: count(usage.inputTokens),
|
|
70
72
|
outputTokens: count(usage.outputTokens), totalTokens: count(usage.totalTokens), costUsd: null,
|
|
71
73
|
completeness: "partial", observedAt: event.at };
|
|
72
74
|
}
|
|
@@ -74,7 +76,8 @@ export async function recordNativeRunUsage(repository, event) {
|
|
|
74
76
|
return;
|
|
75
77
|
await repository.mutate(event.runId, current => {
|
|
76
78
|
const saved = current.workers.find(w => w.workerId === event.workerId);
|
|
77
|
-
if (saved?.generation !== event.generation || saved.nativeSessionId !== worker.nativeSessionId
|
|
79
|
+
if (saved?.generation !== event.generation || saved.nativeSessionId !== worker.nativeSessionId ||
|
|
80
|
+
saved.nativeJobId !== worker.nativeJobId)
|
|
78
81
|
return current;
|
|
79
82
|
return appendUsage(current, incoming);
|
|
80
83
|
});
|
|
@@ -607,7 +607,22 @@ export async function writePrivateFile(path, content, anchorDirectory) {
|
|
|
607
607
|
await handle.close();
|
|
608
608
|
handle = undefined;
|
|
609
609
|
await chmod(temporaryPath, 0o600);
|
|
610
|
-
|
|
610
|
+
for (let attempt = 0;; attempt += 1) {
|
|
611
|
+
try {
|
|
612
|
+
await rename(temporaryPath, path);
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
// Windows readers can temporarily deny replacement of an existing file.
|
|
617
|
+
if (process.platform !== "win32" || attempt >= 4 ||
|
|
618
|
+
typeof error !== "object" || error === null || !("code" in error) ||
|
|
619
|
+
!["EPERM", "EACCES", "EBUSY"].includes(String(error.code))) {
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
await delay(50 * (attempt + 1));
|
|
623
|
+
await inspectPath(path, anchorDirectory, "file");
|
|
624
|
+
}
|
|
625
|
+
}
|
|
611
626
|
await chmod(path, 0o600);
|
|
612
627
|
await syncDirectory(parent);
|
|
613
628
|
}
|
|
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { sha256 } from "../fs/hash.js";
|
|
5
5
|
import { criterionContractHash, evidenceMatchesContract } from "../task/contract.js";
|
|
6
6
|
/** Only current, paired check evidence discharges a mechanical contract. */
|
|
7
|
-
export async function acceptanceCoverage(record, config, sourceFingerprint, store) {
|
|
7
|
+
export async function acceptanceCoverage(record, config, sourceFingerprint, store, readExecution = path => readPrivateFile(join(store.root, ...path.split("/")), store.root)) {
|
|
8
8
|
const configHash = calculateConfigHash(config);
|
|
9
9
|
const rows = [];
|
|
10
10
|
for (const criterion of record.task.criteria) {
|
|
@@ -26,7 +26,7 @@ export async function acceptanceCoverage(record, config, sourceFingerprint, stor
|
|
|
26
26
|
for (const item of matching) {
|
|
27
27
|
const proof = item.evidence.acceptance;
|
|
28
28
|
try {
|
|
29
|
-
const source = await
|
|
29
|
+
const source = await readExecution(proof.executionArtifact);
|
|
30
30
|
if (source === null || proof.executionArtifact !== ".agent-ops/tasks/acceptance/" + sha256(source) + ".json")
|
|
31
31
|
continue;
|
|
32
32
|
const artifact = JSON.parse(source);
|
|
@@ -5,7 +5,7 @@ import { validateTaskAgainstConfig } from "../schema/validate.js";
|
|
|
5
5
|
import { collectChangeSurface } from "./change-surface.js";
|
|
6
6
|
import { resolveReviewScope, reviewScopeSignature } from "../review/scope.js";
|
|
7
7
|
import { calculateSourceFingerprint } from "./source-fingerprint.js";
|
|
8
|
-
import { buildVerificationEvidence } from "./evidence.js";
|
|
8
|
+
import { buildVerificationEvidence, calculateConfigHash, isPassingVerificationEvidence } from "./evidence.js";
|
|
9
9
|
import { createFailureFingerprint } from "./fingerprint.js";
|
|
10
10
|
import { aggregateVerificationStatus, executeConfiguredCommand } from "./command-executor.js";
|
|
11
11
|
import { selectVerificationScope } from "./scope.js";
|
|
@@ -70,6 +70,64 @@ export class VerificationService {
|
|
|
70
70
|
}
|
|
71
71
|
return references;
|
|
72
72
|
}
|
|
73
|
+
async #sharedTreeCommandReport(task, command, sourceFingerprint) {
|
|
74
|
+
if (!this.#options.trusted)
|
|
75
|
+
return null;
|
|
76
|
+
const active = (await this.#options.taskService.list()).filter((record) => record.status === "active" && record.supersededBy === undefined);
|
|
77
|
+
const byId = new Map(active.map((record) => [record.task.id, record]));
|
|
78
|
+
const rootOf = (id) => {
|
|
79
|
+
let current = id;
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
while (!seen.has(current)) {
|
|
82
|
+
seen.add(current);
|
|
83
|
+
const parentId = byId.get(current)?.task.parentTaskId;
|
|
84
|
+
if (parentId === undefined || !byId.has(parentId))
|
|
85
|
+
return current;
|
|
86
|
+
current = parentId;
|
|
87
|
+
}
|
|
88
|
+
return current;
|
|
89
|
+
};
|
|
90
|
+
const rootId = rootOf(task.id);
|
|
91
|
+
const peers = active.filter((record) => record.task.id !== task.id &&
|
|
92
|
+
rootOf(record.task.id) === rootId);
|
|
93
|
+
if (peers.length === 0)
|
|
94
|
+
return null;
|
|
95
|
+
const configHash = calculateConfigHash(this.#options.config);
|
|
96
|
+
for (const peer of peers) {
|
|
97
|
+
for (const reference of new Set(Object.values(peer.evidence).flat())) {
|
|
98
|
+
let loaded;
|
|
99
|
+
try {
|
|
100
|
+
loaded = await this.#options.evidenceStore.load(reference);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (loaded === null || typeof loaded !== "object")
|
|
106
|
+
continue;
|
|
107
|
+
const evidence = loaded;
|
|
108
|
+
if (evidence.commandId === command.id &&
|
|
109
|
+
evidence.sourceFingerprint === sourceFingerprint &&
|
|
110
|
+
evidence.configHash === configHash &&
|
|
111
|
+
evidence.scope === this.#options.scope &&
|
|
112
|
+
isPassingVerificationEvidence(command, evidence)) {
|
|
113
|
+
return {
|
|
114
|
+
commandId: command.id,
|
|
115
|
+
required: command.required,
|
|
116
|
+
status: "PASS",
|
|
117
|
+
failureClass: "none",
|
|
118
|
+
exitCode: evidence.exitCode,
|
|
119
|
+
timedOut: false,
|
|
120
|
+
testCount: evidence.testCount,
|
|
121
|
+
diagnostic: "",
|
|
122
|
+
evidenceReferences: [],
|
|
123
|
+
startedAt: evidence.startedAt,
|
|
124
|
+
finishedAt: evidence.finishedAt
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
73
131
|
async #runCommand(task, command) {
|
|
74
132
|
const startedAt = (this.#options.now ?? (() => new Date().toISOString()))();
|
|
75
133
|
const result = await executeConfiguredCommand(command, {
|
|
@@ -144,7 +202,9 @@ export class VerificationService {
|
|
|
144
202
|
};
|
|
145
203
|
const results = [];
|
|
146
204
|
for (const commandId of selection.verifierIds) {
|
|
147
|
-
|
|
205
|
+
const command = commandById(this.#options.config, commandId);
|
|
206
|
+
const shared = await this.#sharedTreeCommandReport(validation.value, command, sourceFingerprint);
|
|
207
|
+
results.push(shared ?? (await this.#runCommand(validation.value, command)));
|
|
148
208
|
}
|
|
149
209
|
const acceptance = validation.value.criteria.some(c => c.acceptance !== undefined && c.acceptance.mode !== "review-only")
|
|
150
210
|
? await replayAcceptance({ ...this.#options, task: validation.value, sourceFingerprint }) : [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile as execFileCallback, spawn } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
const DEFAULT_TIMEOUT_MS =
|
|
3
|
+
export const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
4
4
|
const DEFAULT_OUTPUT_LIMIT_BYTES = 64 * 1024;
|
|
5
5
|
const MAX_OUTPUT_LIMIT_BYTES = 1024 * 1024;
|
|
6
6
|
const DEFAULT_TERMINATION_GRACE_MS = 500;
|
|
@@ -137,6 +137,64 @@ async function terminatePosixTree(processId, completion, graceMs) {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
+
async function collectPosixSubtree(seen, excluded) {
|
|
141
|
+
try {
|
|
142
|
+
const { stdout } = await execFile("ps", ["-axo", "pid=,ppid="], {
|
|
143
|
+
encoding: "utf8"
|
|
144
|
+
});
|
|
145
|
+
const rows = stdout
|
|
146
|
+
.trim()
|
|
147
|
+
.split("\n")
|
|
148
|
+
.map((line) => {
|
|
149
|
+
const [pid, ppid] = line.trim().split(/\s+/u).map(Number);
|
|
150
|
+
return { pid, ppid };
|
|
151
|
+
})
|
|
152
|
+
.filter((row) => Number.isSafeInteger(row.pid) &&
|
|
153
|
+
row.pid > 0 &&
|
|
154
|
+
Number.isSafeInteger(row.ppid) &&
|
|
155
|
+
row.ppid >= 0);
|
|
156
|
+
for (let added = true; added;) {
|
|
157
|
+
added = false;
|
|
158
|
+
for (const { pid, ppid } of rows) {
|
|
159
|
+
if (!excluded.has(pid) && !seen.has(pid) && seen.has(ppid)) {
|
|
160
|
+
seen.add(pid);
|
|
161
|
+
added = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const alive = new Set(rows.map((row) => row.pid));
|
|
166
|
+
const root = [...seen][0];
|
|
167
|
+
return [...seen].filter((pid) => !excluded.has(pid) && (pid === root || alive.has(pid)));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return [...seen].filter((pid) => !excluded.has(pid));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function terminatePosixSubtree(processId, processGroupId, completion, graceMs) {
|
|
174
|
+
const excluded = new Set([process.pid, processGroupId]);
|
|
175
|
+
const seen = new Set([processId]);
|
|
176
|
+
for (const pid of (await collectPosixSubtree(seen, excluded)).reverse()) {
|
|
177
|
+
try {
|
|
178
|
+
process.kill(pid, "SIGTERM");
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (errorCode(error) !== "ESRCH") {
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
await waitForCompletion(completion, graceMs);
|
|
187
|
+
for (const pid of (await collectPosixSubtree(seen, excluded)).reverse()) {
|
|
188
|
+
try {
|
|
189
|
+
process.kill(pid, "SIGKILL");
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (errorCode(error) !== "ESRCH") {
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
140
198
|
async function defaultTerminateWindowsTree(processId, graceMs) {
|
|
141
199
|
await execFile("taskkill", ["/pid", String(processId), "/T", "/F"], {
|
|
142
200
|
encoding: "utf8",
|
|
@@ -208,7 +266,11 @@ export class NodeVerificationProcessRunner {
|
|
|
208
266
|
await this.#terminateWindowsTree(processId, graceMs);
|
|
209
267
|
return;
|
|
210
268
|
}
|
|
211
|
-
|
|
269
|
+
if (this.#processGroupId !== undefined) {
|
|
270
|
+
await terminatePosixSubtree(processId, this.#processGroupId, completion, graceMs);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
await terminatePosixTree(processId, completion, graceMs);
|
|
212
274
|
}
|
|
213
275
|
};
|
|
214
276
|
}
|
|
@@ -263,6 +263,11 @@ evidence. Give each editing conversation its own worktree instead:
|
|
|
263
263
|
are never pre-authorized. Turning auto mode off, or `uninstall`, takes
|
|
264
264
|
exactly those entries back out; a rules file of that name agent-ops did not
|
|
265
265
|
write is left alone.
|
|
266
|
+
- The opt-in `run` profile additionally authorizes `agent-ops run` and the
|
|
267
|
+
selected `agent-ops followup process` action on Claude and Codex. Normal
|
|
268
|
+
init/update plans disclose the exact entries; dropping the profile removes
|
|
269
|
+
them. Followup metadata actions grant no native execution authority. Processing
|
|
270
|
+
still enforces current repository trust and all ordinary native start controls.
|
|
266
271
|
- `review` and `doctor` take `--output <file>`: the JSON envelope is written
|
|
267
272
|
there (mode 0600) and one summary line is printed. Use it instead of a shell
|
|
268
273
|
redirect, which Codex wraps in `zsh -lc` and can only allow by matching the
|
|
@@ -39,3 +39,39 @@ gone. `task debug` now fails with `TASK_DEBUG_REMOVED`.
|
|
|
39
39
|
Older state still loads: tasks keep their `debug` tag, stored budget and
|
|
40
40
|
hypothesis state is ignored, `originRequired` no longer blocks finish, and a
|
|
41
41
|
config's `features.debug` is accepted and dropped by the next `update`.
|
|
42
|
+
|
|
43
|
+
## Retained review followups
|
|
44
|
+
|
|
45
|
+
After finish, inspect `agent-ops followup list` and `followup show <followup-id>`
|
|
46
|
+
(add `--json` for agents). `followup import` / `followup sync` retries metadata-only local
|
|
47
|
+
history collection without models or repairs. Ledger state is private in the
|
|
48
|
+
Git common directory and shared by linked worktrees. Collection warnings leave
|
|
49
|
+
original completion intact; retry after fixing the reported source/state problem.
|
|
50
|
+
|
|
51
|
+
A free-text observation needs explicit clarification first:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
agent-ops followup clarify <followup-id> --finding-file finding.json --reason 'Concrete claim and supporting source'
|
|
55
|
+
agent-ops followup defer <followup-id> --reason 'Revisit after the dependency upgrade'
|
|
56
|
+
agent-ops followup accept <followup-id> --reason 'Accepted tradeoff for this release'
|
|
57
|
+
agent-ops followup reopen <followup-id>
|
|
58
|
+
env -u CODEX_SANDBOX_NETWORK_DISABLED agent-ops followup process <followup-id> --host codex
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`finding.json` is an ordinary nonblocking ReviewFinding: severity, blocking=false,
|
|
62
|
+
title, details, locations (`path`, `line`), evidence, recommendation and criterionIds.
|
|
63
|
+
Accept/defer require reasons and never mean resolved. Reopen and process again
|
|
64
|
+
preserve source/proof history. Process is an explicit authorization for two fresh
|
|
65
|
+
isolated read-only current-source reviews and, on conclusive PRESENT only, a new
|
|
66
|
+
bounded production repair run. UNKNOWN, drift or reviewer errors stay unresolved.
|
|
67
|
+
Native macOS/clean main/checked-out target/trust/run-policy controls remain; a
|
|
68
|
+
refusal preserves its exact code and never triggers a nested-run workaround.
|
|
69
|
+
|
|
70
|
+
Use the trusted outer host for process; `update --auto-run on` can propose scoped
|
|
71
|
+
run-profile permissions. Do not modify trust or installed settings to bypass a
|
|
72
|
+
refusal. A non-executable claim may use `--review-only-reason 'specific reason'`;
|
|
73
|
+
ABSENT then needs two fresh reasoned reviews. Reproducible bugs need corresponding
|
|
74
|
+
behavioral red-to-green checks. Repair closure uses the exact original finding,
|
|
75
|
+
`followup-resolution` contract, delivered receipt, exact commit and evidence
|
|
76
|
+
digests; generic suite PASS, changed paths or run completion alone cannot close it.
|
|
77
|
+
This workflow adds no DEBUG investigation ceremony or SessionStart summary.
|
|
@@ -270,3 +270,57 @@ Before committing, use the default worktree scope for verify, review and complet
|
|
|
270
270
|
This enforces the toolkit's completion command, not a host's natural-language completion claim. It does not prevent a process with the same filesystem authority from editing task/evidence files directly, and it does not lock Git or external source writers.
|
|
271
271
|
|
|
272
272
|
Project init/update installs `.agent-ops/.gitignore` for `/tasks/` and `/reviews/` on every harness and profile. An existing user-owned `.agent-ops/.gitignore` blocks init/update with `UNMANAGED_INSTALL_PATH`: first back it up outside that path and move its custom rules to the root `.gitignore` (prefix them with `/.agent-ops/`), then retry. Git collection fails with `CHANGE_SURFACE_TRACKED_RUNTIME` if these directories contain indexed files (even clean tracked files), or staged runtime removals. Move any real source files out of the runtime directories, install ignore rules, then use `git rm -r --cached --ignore-unmatch -- .agent-ops/tasks/ .agent-ops/reviews/` to keep local files while removing them from the index. Commit any staged removals before producing fresh verification/review evidence. The active completion Stop gate also blocks this condition with `COMPLETION_GATE_TRACKED_RUNTIME` and the recovery message. Run verify/review from the Git repository root; nested project roots are not supported by the current path/fingerprint collector. Uninstall removes the managed ignore file but preserves runtime output.
|
|
273
|
+
|
|
274
|
+
## EVIDENCE-FOLLOWUP-001
|
|
275
|
+
|
|
276
|
+
Retained nonblocking findings and free-text residual risks MUST keep their original
|
|
277
|
+
provenance and MUST NOT be treated as resolved by generic passing checks.
|
|
278
|
+
|
|
279
|
+
- Trigger: Successful receipt-bound delivery, local history import or explicit selected processing.
|
|
280
|
+
- Action: Collect metadata, preserve provenance/dispositions and assess the current source before any bounded repair.
|
|
281
|
+
- Evidence: Resolution binds the original finding, exact commit and corresponding acceptance artifacts or a reasoned review-only contract with two fresh reviews.
|
|
282
|
+
- Positive: A selected PRESENT finding starts a new native repair; its corresponding red-to-green proof and sealed delivery resolve the exact item.
|
|
283
|
+
- Negative: A generic passing suite, changed path or completed run resolves a retained finding without its specific proof.
|
|
284
|
+
|
|
285
|
+
- Successful ordinary finish, native final delivery and recovery collect from
|
|
286
|
+
authenticated local receipts, Git-note digests and full sealed reports. Ordinary
|
|
287
|
+
repository CLI use retries collection and linked-result reconciliation. Historical
|
|
288
|
+
`agent-ops followup import` (alias `sync`) is metadata-only: no model calls,
|
|
289
|
+
assessment, repair or provider quota. Invalid sources warn conservatively;
|
|
290
|
+
collection is advisory and fail-open after valid completion.
|
|
291
|
+
- Versioned private state lives under the repository Git common directory at
|
|
292
|
+
`agent-ops/followup/`, shared by linked worktrees, never committed. Exact
|
|
293
|
+
provenance identities make repeated import idempotent; dispositions, assessment
|
|
294
|
+
links, repair runs and proof history survive retries. Schema 1 readable receipts
|
|
295
|
+
retain an explicit legacy contract provenance limit. Source tasks, runs and
|
|
296
|
+
receipts remain immutable.
|
|
297
|
+
- `followup list` and `show <followup-id>` expose text or `--json` state. Concrete
|
|
298
|
+
findings are candidates; text risks are observations. `clarify <followup-id>
|
|
299
|
+
--finding-file <json> --reason <text>` retains the original observation and
|
|
300
|
+
supplies a concrete nonblocking ReviewFinding with locations and evidence.
|
|
301
|
+
`accept` and `defer` require nonempty reasons, retain history and are distinct
|
|
302
|
+
from resolution. `reopen` retains proof history; `process` may be selected again.
|
|
303
|
+
- `followup process <followup-id> --host <claude|codex>` explicitly authorizes
|
|
304
|
+
current-source revalidation and, only on conclusive PRESENT, a fresh bounded
|
|
305
|
+
production native repair run without another confirmation. Two independent
|
|
306
|
+
read-only sessions inspect isolated current source and the immutable finding.
|
|
307
|
+
UNKNOWN (including reviewer failure, invalid output, timeout, source/config
|
|
308
|
+
drift or insufficient evidence) cannot launch repair or resolve. Fingerprints,
|
|
309
|
+
changed paths and path existence alone prove no semantic conclusion.
|
|
310
|
+
- Native repair preserves macOS support, clean main checkout, checked-out target,
|
|
311
|
+
current trust, run policy, native controls and worktree ownership. Refusals
|
|
312
|
+
retain the item and exact code; there is no nested-run workaround or restart
|
|
313
|
+
of the completed source run. An exclusive private claim and reserved run
|
|
314
|
+
identity prevent duplicate starts; ambiguous interrupted startup stays pending.
|
|
315
|
+
- ABSENT can resolve only under `--review-only-reason <text>` with a specific
|
|
316
|
+
justified contract and two fresh reviews. Reproducible bugs need corresponding
|
|
317
|
+
behavioral red-to-green acceptance. Completed repair closure validates the
|
|
318
|
+
exact original finding's `followup-resolution` criterion, baseline, target,
|
|
319
|
+
source/config, paired checks or reasoned review-only contract, two fresh sealed
|
|
320
|
+
reviews, delivered receipt and exact commit/evidence digests. Generic test PASS,
|
|
321
|
+
run status, acceptance or deferral cannot close it; stale/unlinked proof stays
|
|
322
|
+
unresolved. Cleanup recovery must retain links.
|
|
323
|
+
- CLI permissions for `followup process` follow the opt-in run profile and appear
|
|
324
|
+
in normal init/update plans. They grant no trust/policy changes. No Office
|
|
325
|
+
write UI, SessionStart summary, cloud service or additional mandatory gate is
|
|
326
|
+
introduced.
|
|
@@ -62,6 +62,17 @@
|
|
|
62
62
|
Repeat the flag for multiple failures. Successful acknowledgements are audited
|
|
63
63
|
and revalidated on later resumes. This grants no command permission: native
|
|
64
64
|
approval runs again. Existing writer-death, checkout, trust and budget checks apply.
|
|
65
|
+
`run resume <id> --retry-delivery <task-id>` MAY explicitly requeue one child
|
|
66
|
+
blocked by `RUN_FINAL_PROOF_INTERRUPTED` after the harness cause is repaired.
|
|
67
|
+
It MUST require the matching interrupted handoff, confirmed stopped writers
|
|
68
|
+
and proof, clean owned checkouts and current trust/policy. It MUST preserve
|
|
69
|
+
run/task/goal/baselines/commits, accounting and failure history, record the
|
|
70
|
+
acknowledged attempt, and require a fresh native generation and normal proof.
|
|
71
|
+
It MUST NOT mark delivery, reset another blocker or acknowledge command denial;
|
|
72
|
+
approval retry is a separate control. Ordinary resume retains no-progress blocks.
|
|
73
|
+
Interrupted controlled proof MUST record bounded redacted exit/signal, result
|
|
74
|
+
failure stage and subprocess diagnostics without treating invalid output or a
|
|
75
|
+
nonzero successful envelope as proof.
|
|
65
76
|
Initial recovery after a current manual resume MAY ignore an owned Codex limit
|
|
66
77
|
snapshot received before activation whose native update second strictly predates
|
|
67
78
|
that resume. Automatic restarts, unknown/equal/future timestamps, clock rollback,
|
|
@@ -83,6 +94,31 @@ status and delivered commit. With `jobs=1`, child execution MUST wait for that
|
|
|
83
94
|
confirmed death. Native registration, recovery and start MUST enforce at most
|
|
84
95
|
`jobs` active agents; changing task slot accounting alone is insufficient.
|
|
85
96
|
|
|
97
|
+
An explicit answer to a pending run question MAY release a confirmed stopped
|
|
98
|
+
coordinator wait before all children finish. The response consumes only this
|
|
99
|
+
run's current-generation wait, records `RUN_COORDINATOR_RESPONSE_WAKE`, and starts
|
|
100
|
+
no process. Normal resume still reconciles death, trust and policy, then issues a
|
|
101
|
+
fresh generation within the job limit. Ordinary resume and replayed answers do
|
|
102
|
+
not consume a new wait. Child delivery, failure history and policy bindings remain
|
|
103
|
+
unchanged; an answer itself grants no additional command capability.
|
|
104
|
+
|
|
105
|
+
A run-level proof MUST match its persisted proof PID, process group and process
|
|
106
|
+
start identity, plus the current coordinator ID and generation. That actor may
|
|
107
|
+
read the run policy for the main root and registered worker checkouts, including
|
|
108
|
+
delivered children during integration, without changing its worker environment.
|
|
109
|
+
Worker-local proof remains bound to its own checkout and worker identity; its
|
|
110
|
+
record MUST NOT shadow an independently authenticated run-level proof. Ordinary
|
|
111
|
+
checkout record checks and integration ownership checks remain authoritative.
|
|
112
|
+
Invalid actor, group, generation or unregistered checkout claims return
|
|
113
|
+
`RUN_WORKER_STALE`; an inactive, disabled or awaiting-resume run still returns
|
|
114
|
+
`RUN_POLICY_DISABLED` after identity validation. Policy reads grant no additional
|
|
115
|
+
capability and do not alter config, trust, registration or delivery state.
|
|
116
|
+
|
|
117
|
+
Known limitation: an inner verification command timeout can terminate the shared
|
|
118
|
+
proof process group before the evidence CLI emits JSON. Response recovery does
|
|
119
|
+
not fix this process isolation defect; a bounded run-only command timeout may be
|
|
120
|
+
assessed through the existing coordinator policy flow, with the failure retained.
|
|
121
|
+
|
|
86
122
|
This module defines the bounded loop used to plan, implement, verify, and hand off work.
|
|
87
123
|
|
|
88
124
|
## LOOP-START-001
|
package/docs/en/spec/review.md
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
- Trigger: Production task criteria become ready before implementation.
|
|
6
6
|
- Action: Await one scoped advisory and let the writer record adoption/rejection reasons.
|
|
7
7
|
- Evidence: Private snapshot report plus safe local status/usage log; no completion attestation.
|
|
8
|
-
- Positive: `
|
|
9
|
-
- Negative: `
|
|
8
|
+
- Positive: `Adopting advice rewrites a criterion; that revise reruns the advisory once and shows the new advice.`
|
|
9
|
+
- Negative: `Re-review every revise, including surface-only ones, or treat advice as final PASS proof.`
|
|
10
10
|
|
|
11
11
|
Every production task creation (including DEBUG, replan children and native run root/child tasks) MUST wait for one independent criteria advisory before implementation. A fresh session uses one configured independent-review target, preferring a different target from the host. It receives only the original goal, assigned intent/covers, criteria and surface audit in an empty disposable workspace. It MUST NOT inspect repository files or run tests. Missing goals are insufficient information; child completeness is scoped to its assignment. Verification materials/import validity remain deferred until implementation; reasoned review-only acceptance is valid.
|
|
12
12
|
|
|
13
13
|
The advisory has no agent-ops execution deadline, stall cutoff or token budget. Provider limitations, genuine launch/auth/output errors and explicit cancellation remain effective. Errors produce NOT_RUN and permit work; cancellation preserves the task and MUST stop implementation. Native review processes remain registered and stoppable. Normal verification and final two-round review retain their existing deadlines and gates.
|
|
14
14
|
|
|
15
|
-
Task creation shows advice and a private report reference. The writer decides which recommendations to adopt and records acceptance/rejection reasons through task revise (a same-criteria revision may record rejected advice). Requirements that are ambiguous or expand scope require user clarification. Reports bind the original task/goal/criteria/surface snapshot
|
|
15
|
+
Task creation shows advice and a private report reference. The writer decides which recommendations to adopt and records acceptance/rejection reasons through task revise (a same-criteria revision may record rejected advice). Requirements that are ambiguous or expand scope require user clarification. Reports bind the original task/goal/criteria/surface snapshot and record their trigger (`create`, `rerun` or `revise`; absent on older reports). Task status and revise show a report as stale when the goal, intent, criteria or assigned scope change; a surface-only change leaves it current. The first saved revision that makes the current report stale, when that report was not itself revise-triggered, MUST wait for one automatic fresh round (trigger `revise`) and show its advice; the revision is kept whether that round completes, reports NOT_RUN or is cancelled. Further revisions MUST NOT automatically re-review until an explicit rerun. Repeated initial requests reuse the first attempt, including NOT_RUN; outside that one automatic round, only explicit `agent-ops review --criteria --task <id> --rerun --yes` starts another fresh single round. Without --rerun the same command reads/waits for the current attempt.
|
|
16
16
|
|
|
17
17
|
Detailed redacted reports remain private in `.agent-ops/reviews/criteria-<task-id>*.json`; the local log contains only status, duration, counts, reported usage and report reference. CLI invocation counts are distinct from provider model calls, which may be unreported. Report/log failures MUST NOT roll back a task or fabricate evidence. These advisory artifacts MUST NOT satisfy verify, final review or finish.
|
|
18
18
|
|
|
@@ -112,6 +112,40 @@ successful refutation MUST make the run FAIL.
|
|
|
112
112
|
replays the recorded queries and reads every listed `valueChanges` consumer;
|
|
113
113
|
this is the only exception to "do not survey the repository".
|
|
114
114
|
|
|
115
|
+
## REVIEW-ORIGIN-001
|
|
116
|
+
|
|
117
|
+
A defect the change did not cause MUST NOT block the change; it MUST be
|
|
118
|
+
reported as `origin: "pre-existing"` with `blocking: false` and flow to the
|
|
119
|
+
followup ledger.
|
|
120
|
+
|
|
121
|
+
- Trigger: A reviewer meets a defect while reading for the review.
|
|
122
|
+
- Action: Mark it pre-existing only when it is present identically at the base
|
|
123
|
+
and the change did not introduce, expose, worsen or depend on it. Report only
|
|
124
|
+
defects met on the normal read path; do not search for old ones.
|
|
125
|
+
- Evidence: The report validator rejects a pre-existing finding with
|
|
126
|
+
`blocking: true` at any severity, including critical. A finding without
|
|
127
|
+
`origin` means introduced and keeps the old severity rules, so stored reports
|
|
128
|
+
stay valid. Nonblocking findings of a delivered PASS reach followup as before.
|
|
129
|
+
- Positive: `A critical injection in an untouched helper is reported as pre-existing, nonblocking, and collected into followup.`
|
|
130
|
+
- Negative: `A caller in an unchanged file breaks because of this change and is marked pre-existing because its lines were not edited.`
|
|
131
|
+
|
|
132
|
+
## REVIEW-ROUNDS-001
|
|
133
|
+
|
|
134
|
+
Review MUST stop after three failed reviews of one task contract and ask for a
|
|
135
|
+
new approach instead of another fix round.
|
|
136
|
+
|
|
137
|
+
- Trigger: `review` (directly or through `batch`, `task advance` or a run) for a
|
|
138
|
+
task with three or more FAIL reports bound to its current task contract hash
|
|
139
|
+
and newer than its latest PASS.
|
|
140
|
+
- Action: Return NOT_RUN `round-cap` before resolving scope or starting any
|
|
141
|
+
reviewer, printing the exact `task revise` command. Reconsider the approach and
|
|
142
|
+
revise the criteria or intent with a reason, or ask the user.
|
|
143
|
+
- Evidence: NOT_RUN reports never count; each source fingerprint keeps one
|
|
144
|
+
report, so re-reviewing unchanged code adds nothing; a contract revision or a
|
|
145
|
+
newer PASS starts the count again. The cap is a constant, not configuration.
|
|
146
|
+
- Positive: `Third FAIL on the same contract; the agent revises the intent to a different design and reviews again.`
|
|
147
|
+
- Negative: `Keep committing one fix per finding through a fifth review of the same contract.`
|
|
148
|
+
|
|
115
149
|
## REVIEW-HOST-001
|
|
116
150
|
|
|
117
151
|
A host that cannot provide network and loopback capability MUST fail closed
|
|
@@ -227,6 +227,9 @@ fingerprint,因此兩個對話修改同一個 checkout 會互相作廢對方
|
|
|
227
227
|
`npx lint-staged` 的 allow 規則,讓 pre-commit hook 能寫入主 `.git`。`trust`、
|
|
228
228
|
`allow-stop`、`init`、`update`、`uninstall` 永遠不會被預先授權。關閉 auto 模式或
|
|
229
229
|
`uninstall` 會精確移除這些項目;同名但不是 agent-ops 寫的規則檔不會被動到。
|
|
230
|
+
- opt-in `run` profile 另外在 Claude 與 Codex 授權 `agent-ops run` 與明確選取的
|
|
231
|
+
`agent-ops followup process`。一般 init/update 計畫會列出精確項目;移除 profile
|
|
232
|
+
會移除這些權限。中繼資料操作不授予原生執行權限,process 仍遵守目前儲存庫信任與所有原生啟動限制。
|
|
230
233
|
- `review` 與 `doctor` 支援 `--output <file>`:JSON envelope 寫入該檔(權限 0600),
|
|
231
234
|
並只印出一行摘要。請用它取代 shell 重導向;Codex 會把重導向包成 `zsh -lc`,只能
|
|
232
235
|
以整串指令比對來放行。
|
|
@@ -35,3 +35,34 @@ prompt hook 自動建立 DEBUG、DEBUG 專用 Stop 阻擋,以及 Codex `Interr
|
|
|
35
35
|
`originRequired` 不再阻擋 finish,config 的 `features.debug` 仍被接受並在下次 `update` 時移除。
|
|
36
36
|
|
|
37
37
|
完整說明見英文 [Task DEBUG](../../en/guides/task-debug.md)。
|
|
38
|
+
|
|
39
|
+
## 保留的審查 followup
|
|
40
|
+
|
|
41
|
+
finish 後以 `agent-ops followup list`、`followup show <followup-id>` 檢視,agent 可加
|
|
42
|
+
`--json`。`followup import` / `followup sync` 只回填本機歷史中繼資料,不呼叫模型或修復。
|
|
43
|
+
私有狀態在 Git common directory,所有連結 worktree 共用。收集警告不影響原始完成;
|
|
44
|
+
修正來源或狀態問題後可重試。
|
|
45
|
+
|
|
46
|
+
自由文字觀察需先明確釐清:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
agent-ops followup clarify <followup-id> --finding-file finding.json --reason '具體主張與來源證據'
|
|
50
|
+
agent-ops followup defer <followup-id> --reason '相依套件升級後再處理'
|
|
51
|
+
agent-ops followup accept <followup-id> --reason '接受此版本的取捨'
|
|
52
|
+
agent-ops followup reopen <followup-id>
|
|
53
|
+
env -u CODEX_SANDBOX_NETWORK_DISABLED agent-ops followup process <followup-id> --host codex
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`finding.json` 是一般非阻擋 ReviewFinding:severity、blocking=false、title、details、
|
|
57
|
+
locations(path、line)、evidence、recommendation、criterionIds。接受與延後必須有理由,
|
|
58
|
+
不等於結案;reopen 與再次 process 均保留來源及證據歷史。process 明確授權兩次隔離、
|
|
59
|
+
唯讀、針對目前原始碼的新鮮審查,且只在確定 PRESENT 時啟動全新有界的正式原生修復 run。
|
|
60
|
+
UNKNOWN、漂移與 reviewer 錯誤維持未解決。仍遵守 macOS、乾淨主 checkout、目標分支、
|
|
61
|
+
信任與 run policy;拒絕保留原始碼,不繞過 nested-run 限制。
|
|
62
|
+
|
|
63
|
+
process 必須透過可信任外層 host;`update --auto-run on` 可提出限定 run profile 權限。
|
|
64
|
+
不可自行修改信任或已安裝設定繞過拒絕。無法用可執行檢查判定的主張可加
|
|
65
|
+
`--review-only-reason '具體理由'`;ABSENT 仍需兩次新鮮且有理由的審查。
|
|
66
|
+
可重現錯誤仍需對應 behavioral 紅轉綠。修復結案驗證原始 finding、`followup-resolution`
|
|
67
|
+
契約、交付 receipt、精確 commit 與證據摘要;一般測試 PASS、路徑變動或 run 完成不能結案。
|
|
68
|
+
本流程不新增 DEBUG 調查手續或 SessionStart 摘要。
|