@kylecheng3146/agent-ops 0.3.7 → 0.4.1
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 +35 -0
- package/dist/packages/cli/src/args.js +93 -9
- package/dist/packages/cli/src/bin.js +151 -104
- package/dist/packages/cli/src/cli.js +54 -5
- package/dist/packages/cli/src/commands/batch.js +106 -0
- package/dist/packages/cli/src/commands/hook.js +4 -1
- package/dist/packages/cli/src/commands/init.js +1 -0
- package/dist/packages/cli/src/commands/parallel.js +16 -3
- package/dist/packages/cli/src/commands/review.js +22 -0
- package/dist/packages/cli/src/commands/uninstall.js +3 -2
- package/dist/packages/cli/src/commands/update.js +1 -0
- package/dist/packages/cli/src/context.js +28 -3
- package/dist/packages/cli/src/hook-process.js +14 -3
- package/dist/packages/cli/src/public-plan.js +32 -2
- package/dist/runtime/src/adapters/claude/config.js +1 -1
- package/dist/runtime/src/adapters/claude/input.js +17 -0
- package/dist/runtime/src/adapters/claude/output.js +9 -0
- package/dist/runtime/src/hooks/codex-loop.js +67 -7
- package/dist/runtime/src/hooks/completion-gate.js +27 -2
- package/dist/runtime/src/hooks/dispatch.js +5 -0
- package/dist/runtime/src/install/apply.js +2 -0
- package/dist/runtime/src/install/doctor.js +8 -1
- package/dist/runtime/src/install/harness.js +17 -6
- package/dist/runtime/src/install/plan.js +9 -0
- package/dist/runtime/src/install/preauth.js +276 -0
- package/dist/runtime/src/install/probes.js +32 -0
- package/dist/runtime/src/install/uninstall.js +29 -11
- package/dist/runtime/src/install/update.js +1 -0
- package/dist/runtime/src/parallel/enter.js +43 -0
- package/dist/runtime/src/parallel/finish.js +2 -1
- package/dist/runtime/src/parallel/guard.js +20 -1
- package/dist/runtime/src/parallel/manage.js +38 -0
- package/dist/runtime/src/parallel/service.js +3 -1
- package/dist/runtime/src/review/batch-guard.js +11 -0
- package/dist/runtime/src/review/batch-select.js +37 -0
- package/dist/runtime/src/review/batch.js +202 -0
- package/dist/runtime/src/review/execute.js +10 -7
- package/dist/runtime/src/review/host-sandbox.js +9 -0
- package/dist/runtime/src/review/invocation.js +15 -2
- package/dist/runtime/src/review/probe.js +51 -15
- package/dist/runtime/src/review/render.js +4 -0
- package/docs/en/guides/configuration.md +51 -1
- package/docs/en/spec/review.md +70 -0
- package/docs/zh-TW/guides/configuration.md +41 -0
- package/docs/zh-TW/spec/review.md +71 -1
- package/package.json +1 -1
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { runVerificationCommand } from "../verify/spawn.js";
|
|
2
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { extractFinalMessage } from "./extract.js";
|
|
6
|
-
import { isolatedReviewEnvironment } from "./execute.js";
|
|
7
|
-
import { isNetworkFailure, probeHostReachable, TARGET_API_HOST } from "./host-sandbox.js";
|
|
6
|
+
import { firstComplaint, isolatedReviewEnvironment } from "./execute.js";
|
|
7
|
+
import { isLoginFailure, isNetworkFailure, probeHostReachable, TARGET_API_HOST } from "./host-sandbox.js";
|
|
8
8
|
import { buildProbeInvocation } from "./invocation.js";
|
|
9
|
+
/** A log's last non-empty line: where a CLI that logs to a file puts its error. */
|
|
10
|
+
async function lastLogLine(path) {
|
|
11
|
+
const text = await readFile(path, "utf8").catch(() => "");
|
|
12
|
+
return text.split(/\r?\n/u).filter((line) => line.trim().length > 0).at(-1) ?? "";
|
|
13
|
+
}
|
|
9
14
|
const PROBE_PROMPT = "Reply with the single word OK and nothing else.";
|
|
10
15
|
/**
|
|
11
16
|
* Matches the review timeout rather than being "quick": codex at high
|
|
@@ -30,6 +35,22 @@ export function probeTimeoutMs(requested) {
|
|
|
30
35
|
* is not evidence at all.
|
|
31
36
|
*/
|
|
32
37
|
export async function probeReviewTarget(target, options) {
|
|
38
|
+
return (await probeReviewTargetDetailed(target, options)).result;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A deep probe is one model call, and a model call can fail for reasons that
|
|
42
|
+
* say nothing about the install. `probe-failed` is the answer for "it did not
|
|
43
|
+
* work and did not say why", so it gets one more try; every other answer is
|
|
44
|
+
* specific (not logged in, no network, timed out, not installed) and repeating
|
|
45
|
+
* it would only spend more of the budget on the same result.
|
|
46
|
+
*/
|
|
47
|
+
export async function probeReviewTargetDetailed(target, options) {
|
|
48
|
+
const first = await probeOnce(target, options);
|
|
49
|
+
return options.deep === true && first.result === "probe-failed"
|
|
50
|
+
? await probeOnce(target, options)
|
|
51
|
+
: first;
|
|
52
|
+
}
|
|
53
|
+
async function probeOnce(target, options) {
|
|
33
54
|
const deep = options.deep === true;
|
|
34
55
|
const directory = deep
|
|
35
56
|
? await mkdtemp(join(tmpdir(), "agent-ops-review-probe-"))
|
|
@@ -67,7 +88,7 @@ export async function probeReviewTarget(target, options) {
|
|
|
67
88
|
: {})
|
|
68
89
|
});
|
|
69
90
|
if (invocation === undefined) {
|
|
70
|
-
return "ineligible";
|
|
91
|
+
return { result: "ineligible" };
|
|
71
92
|
}
|
|
72
93
|
const spawned = await runVerificationCommand({
|
|
73
94
|
id: `review-probe-${target}`,
|
|
@@ -89,29 +110,44 @@ export async function probeReviewTarget(target, options) {
|
|
|
89
110
|
: {})
|
|
90
111
|
});
|
|
91
112
|
if (spawned.failureClass === "missing-executable") {
|
|
92
|
-
return "missing-executable";
|
|
113
|
+
return { result: "missing-executable" };
|
|
93
114
|
}
|
|
94
115
|
if (spawned.timedOut) {
|
|
95
|
-
return "timeout";
|
|
116
|
+
return { result: "timeout" };
|
|
96
117
|
}
|
|
97
|
-
|
|
98
|
-
|
|
118
|
+
const output = `${spawned.stderr}\n${spawned.stdout}`;
|
|
119
|
+
// The target's own words, so a failure is never reduced to a guess. agy
|
|
120
|
+
// writes its errors to the log file the probe hands it, not to stderr.
|
|
121
|
+
const complaint = async () => firstComplaint(spawned.stderr, spawned.stdout) ??
|
|
122
|
+
(deep && target === "agy"
|
|
123
|
+
? firstComplaint(await lastLogLine(join(directory, "agy.log")))
|
|
124
|
+
: undefined);
|
|
125
|
+
if (/(?:operation not permitted|permission denied|bind(?:ing)?[^\n]*(?:failed|denied))/iu.test(output)) {
|
|
126
|
+
return { result: "capability-unavailable", diagnostic: await complaint() };
|
|
99
127
|
}
|
|
100
128
|
if (!deep) {
|
|
101
|
-
return spawned.status === "PASS"
|
|
129
|
+
return spawned.status === "PASS"
|
|
130
|
+
? { result: "ok" }
|
|
131
|
+
: { result: "probe-failed", diagnostic: await complaint() };
|
|
102
132
|
}
|
|
103
133
|
if (spawned.status === "PASS" &&
|
|
104
134
|
extractFinalMessage(target, spawned.stdout) !== undefined) {
|
|
105
|
-
return "ok";
|
|
135
|
+
return { result: "ok" };
|
|
106
136
|
}
|
|
107
137
|
// An unreachable API host outranks whatever the CLI printed: a sandbox that
|
|
108
138
|
// withholds the network makes an authenticated CLI say "not logged in".
|
|
109
|
-
if (isNetworkFailure(
|
|
110
|
-
return "network-unreachable";
|
|
139
|
+
if (isNetworkFailure(output)) {
|
|
140
|
+
return { result: "network-unreachable", diagnostic: await complaint() };
|
|
141
|
+
}
|
|
142
|
+
if (!await (options.reachable ?? probeHostReachable)(TARGET_API_HOST[target])) {
|
|
143
|
+
return { result: "network-unreachable", diagnostic: await complaint() };
|
|
111
144
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
const diagnostic = await complaint();
|
|
146
|
+
// Login is what the output says it is; anything else is a probe that could
|
|
147
|
+
// not run, and the diagnostic is all the evidence there is.
|
|
148
|
+
return isLoginFailure(`${output}\n${diagnostic ?? ""}`)
|
|
149
|
+
? { result: "unauthenticated", diagnostic }
|
|
150
|
+
: { result: "probe-failed", diagnostic };
|
|
115
151
|
}
|
|
116
152
|
finally {
|
|
117
153
|
if (deep) {
|
|
@@ -88,6 +88,10 @@ export function renderReviewResult(result) {
|
|
|
88
88
|
lines.push("The reviewer could not reach the network. This is not a login " +
|
|
89
89
|
"problem: run agent-ops outside the sandbox, or grant it escalated execution.");
|
|
90
90
|
}
|
|
91
|
+
if (result.reason === "probe-failed") {
|
|
92
|
+
lines.push("The reviewer CLI failed its probe for the reason quoted above. This " +
|
|
93
|
+
"is not a login problem: fix that cause instead of logging in again.");
|
|
94
|
+
}
|
|
91
95
|
// Evidence is pinned to the source it was produced from, so any edit to a
|
|
92
96
|
// changed file after the verifier ran — a doc rewritten by a later step
|
|
93
97
|
// counts — voids it. Without the next command the caller reads
|
|
@@ -140,7 +140,11 @@ review preflight finds a target unauthenticated, a 2-second TCP connection to
|
|
|
140
140
|
that target's API host on port 443 (`cloudcode-pa.googleapis.com` for agy,
|
|
141
141
|
`api.openai.com` for codex, `api.anthropic.com` for claude) decides between the
|
|
142
142
|
two; it only refines the reported reason and never blocks a review. The review suggests
|
|
143
|
-
`doctor --check-auth` only when an authentication failure was detected.
|
|
143
|
+
`doctor --check-auth` only when an authentication failure was detected. A probe
|
|
144
|
+
that fails without network or login wording is `probe-failed`, never
|
|
145
|
+
`login-required`: the preflight record and `doctor` quote the target's own first
|
|
146
|
+
line (for agy, the last line of its log file when stderr is empty) so the real
|
|
147
|
+
cause is visible instead of guessed. Confirm target authentication with:
|
|
144
148
|
|
|
145
149
|
```bash
|
|
146
150
|
agent-ops doctor # presence only: no tokens, no network
|
|
@@ -203,6 +207,52 @@ evidence. Give each editing conversation its own worktree instead:
|
|
|
203
207
|
- The session's completion gate follows it: Claude Code enters the worktree
|
|
204
208
|
with EnterWorktree, and a host that cannot move a session (agy) is judged by
|
|
205
209
|
the worktree's gate through a redirect the main checkout records.
|
|
210
|
+
- A command knows its session from `--session`, then `AGENT_OPS_SESSION_ID`,
|
|
211
|
+
then `CODEX_THREAD_ID`, then the id a SessionStart recorded for the
|
|
212
|
+
checkout. Claude Code's SessionStart appends `AGENT_OPS_SESSION_ID` to its
|
|
213
|
+
`CLAUDE_ENV_FILE`, so every later Bash command carries it. When a different
|
|
214
|
+
session started in the same checkout within fifteen minutes, the recorded id
|
|
215
|
+
is contested: `task create`, `attach` and `status` fail with
|
|
216
|
+
`SESSION_ID_AMBIGUOUS` instead of acting as the wrong session.
|
|
217
|
+
- A worktree belongs to the session that created it: a file write into
|
|
218
|
+
another session's worktree is denied (`WORKTREE_OWNED_BY_OTHER_SESSION`),
|
|
219
|
+
naming the session's own worktree. A session with no baseline (its
|
|
220
|
+
SessionStart never arrived) gets one at its first tool call, never at Stop.
|
|
221
|
+
- Inside a worktree the project config is the main checkout's, so a branch
|
|
222
|
+
cannot change the gate, verifiers or trust that judge it before merging; a
|
|
223
|
+
worktree of a repository whose main checkout has no config uses its own.
|
|
224
|
+
- `agent-ops worktree commit --message <text>` stages and commits every change
|
|
225
|
+
in the current worktree. Git runs inside agent-ops, not the shell, so a host
|
|
226
|
+
that cannot prove a shell command stays inside the worktree still lets the
|
|
227
|
+
session commit; it refuses the main checkout, a worktree another session
|
|
228
|
+
owns, a caller it cannot identify and an empty change. It stages everything
|
|
229
|
+
Git does not ignore, so keep stray files out of the worktree.
|
|
230
|
+
- In auto mode the Claude PreToolUse hook also handles `EnterWorktree`: a
|
|
231
|
+
switch into exactly a worktree recorded for the calling session is allowed
|
|
232
|
+
without Claude Code's own confirmation, and any other path is denied
|
|
233
|
+
(`WORKTREE_ENTER_DENIED`, naming the session's worktrees). Run `agent-ops
|
|
234
|
+
update` once so the hook's matcher includes `EnterWorktree`; `doctor`
|
|
235
|
+
reports an older matcher as registration drift.
|
|
236
|
+
- With `worktree.mode` auto, `init` and `update` also pre-authorize what a
|
|
237
|
+
session runs itself, and the plan lists it before you confirm: in
|
|
238
|
+
`.claude/settings.local.json`, `permissions.allow` for `agent-ops task`,
|
|
239
|
+
`verify`, `review`, `worktree` and `doctor` and `sandbox.excludedCommands`
|
|
240
|
+
for `agent-ops review` (your other entries are kept, and the plan describes
|
|
241
|
+
the file instead of printing it); and, when the CLI can see Codex's home
|
|
242
|
+
(`CODEX_HOME`, else `~/.codex`), `rules/agent-ops.rules` with allow rules
|
|
243
|
+
for `agent-ops review` and `doctor --check-auth`, with and without
|
|
244
|
+
`AGENT_OPS_HOST=`. `trust`, `allow-stop`, `init`, `update` and `uninstall`
|
|
245
|
+
are never pre-authorized. Turning auto mode off, or `uninstall`, takes
|
|
246
|
+
exactly those entries back out; a rules file of that name agent-ops did not
|
|
247
|
+
write is left alone.
|
|
248
|
+
- `review` and `doctor` take `--output <file>`: the JSON envelope is written
|
|
249
|
+
there (mode 0600) and one summary line is printed. Use it instead of a shell
|
|
250
|
+
redirect, which Codex wraps in `zsh -lc` and can only allow by matching the
|
|
251
|
+
whole command string.
|
|
252
|
+
- Trust is bound to the config hash, so a hand edit of `.agent-ops/config.json`
|
|
253
|
+
(adding `worktree.setup`, say) leaves the repository untrusted and
|
|
254
|
+
`WORKTREE_SETUP_UNTRUSTED` stops the next worktree. The one grant is the
|
|
255
|
+
user's: `agent-ops update` lists the commands it will trust and asks once.
|
|
206
256
|
- In a worktree, verify and review each task but do not run `task complete`
|
|
207
257
|
(Claude Code refuses it inside an isolated worktree session). `agent-ops
|
|
208
258
|
worktree finish <name>` completes every non-archived task in the worktree,
|
package/docs/en/spec/review.md
CHANGED
|
@@ -125,3 +125,73 @@ FAIL.
|
|
|
125
125
|
- Evidence: The result reason distinguishes a protocol violation from a verdict.
|
|
126
126
|
- Positive: `NOT_RUN: unparseable-output; one criterion was missing.`
|
|
127
127
|
- Negative: `Record a failed review because the model's JSON was malformed.`
|
|
128
|
+
|
|
129
|
+
## REVIEW-BATCH-001
|
|
130
|
+
|
|
131
|
+
A batch review MUST cover exactly the active subtasks of the named parent and the parent itself.
|
|
132
|
+
|
|
133
|
+
- Trigger: Running `batch --parent <task-id>`.
|
|
134
|
+
- Action: Select the parent's active subtasks in creation order, then the parent. Leave out archived and completed tasks and tasks under another parent. Reject an unknown parent with `BATCH_PARENT_NOT_FOUND` and an inactive one with `BATCH_PARENT_NOT_ACTIVE`.
|
|
135
|
+
- Evidence: `data.tasks` lists exactly those task ids, in that order.
|
|
136
|
+
- Positive: `Two active subtasks and the parent are covered; the archived subtask is not.`
|
|
137
|
+
- Negative: `Cover every task in the worktree whatever its parent.`
|
|
138
|
+
|
|
139
|
+
## REVIEW-BATCH-002
|
|
140
|
+
|
|
141
|
+
Each task's base MUST come from an explicit flag, then its recorded review base, then the worktree base.
|
|
142
|
+
|
|
143
|
+
- Trigger: Resolving the range a batch task is verified and reviewed against.
|
|
144
|
+
- Action: Use `--base` for subtasks and `--parent-base` for the parent; otherwise the task's recorded `reviewBase`; otherwise the worktree base. Report `BATCH_BASE_UNKNOWN` when none exists.
|
|
145
|
+
- Evidence: `data.tasks[].base` names the base each task used.
|
|
146
|
+
- Positive: `A rerun reuses each PASSed task's recorded base, so its fingerprint and attestation still match.`
|
|
147
|
+
- Negative: `Apply --base to the parent, or fall back to HEAD when no base is known.`
|
|
148
|
+
|
|
149
|
+
## REVIEW-BATCH-003
|
|
150
|
+
|
|
151
|
+
Verification MUST run one task at a time, each task's review MUST start as soon as its own verification passes, and no more than `--width` reviews (default 2) MUST run at once.
|
|
152
|
+
|
|
153
|
+
- Trigger: Tasks in a batch need verification and review.
|
|
154
|
+
- Action: Skip verification for a task that already holds fresh PASS evidence, using the same check review preflight makes. Verify the rest serially. Start a task's review right after its verification passes. Give a task whose verification fails no review, and continue with the others.
|
|
155
|
+
- Evidence: `data.tasks[].verify` is `skipped`, `PASS` or `FAIL`, and a task that failed verification has no review.
|
|
156
|
+
- Positive: `Task B is verified while task A is being reviewed.`
|
|
157
|
+
- Negative: `Run every task's test suite at once.`
|
|
158
|
+
|
|
159
|
+
## REVIEW-BATCH-004
|
|
160
|
+
|
|
161
|
+
A transient NOT_RUN MUST lower the review width to 1 for the rest of the batch and earn exactly one retry, and every other result MUST be final.
|
|
162
|
+
|
|
163
|
+
- Trigger: A review in a batch returns NOT_RUN.
|
|
164
|
+
- Action: When the reason is `probe-failed`, `stalled`, `timeout`, `quota-exhausted` or `network-unreachable`, set the width to 1 for every review not yet started and retry that task once, alone, after the rest have finished. Never retry another reason, never retry twice, and never raise the width again.
|
|
165
|
+
- Evidence: `review.retried` is true for a retried task, and a second transient NOT_RUN stays NOT_RUN.
|
|
166
|
+
- Positive: `probe-failed on task A drops the width to 1, and A runs once more after the others.`
|
|
167
|
+
- Negative: `Retry host-sandboxed until it passes.`
|
|
168
|
+
|
|
169
|
+
## REVIEW-BATCH-005
|
|
170
|
+
|
|
171
|
+
A batch MUST stop when the source it started on changes or the process is interrupted.
|
|
172
|
+
|
|
173
|
+
- Trigger: HEAD or the working tree differs from the start of the batch, or the process receives SIGINT or SIGTERM.
|
|
174
|
+
- Action: Require a clean working tree before starting and check HEAD and the tree before every step. On a change, abort the reviews in flight, start nothing new, and report each unfinished task as `aborted`. A signal exits 130 or 143.
|
|
175
|
+
- Evidence: `data.tasks[].aborted` is true for every task that did not finish.
|
|
176
|
+
- Positive: `A commit during the batch aborts it and the unfinished tasks say aborted.`
|
|
177
|
+
- Negative: `Let in-flight reviews finish against a HEAD that no longer matches.`
|
|
178
|
+
|
|
179
|
+
## REVIEW-BATCH-006
|
|
180
|
+
|
|
181
|
+
A batch MUST probe each review target at most once while the probe succeeds and MUST NOT cache a failed probe.
|
|
182
|
+
|
|
183
|
+
- Trigger: Reviews in a batch need target preflight.
|
|
184
|
+
- Action: Share the probe in flight between concurrent reviews and keep a successful answer for the rest of the batch. Forget a failed answer, so the retry of a task probes again.
|
|
185
|
+
- Evidence: A batch of several reviews makes one probe per target, and a retry after a failure makes a new one.
|
|
186
|
+
- Positive: `Five reviews share one successful agy probe.`
|
|
187
|
+
- Negative: `Cache probe-failed and refuse every remaining task.`
|
|
188
|
+
|
|
189
|
+
## REVIEW-BATCH-007
|
|
190
|
+
|
|
191
|
+
A batch result MUST report every task's outcome and MUST exit 0 only when every task passed.
|
|
192
|
+
|
|
193
|
+
- Trigger: A batch ends.
|
|
194
|
+
- Action: Return `BATCH_RESULT`, `BATCH_FAILED` or `BATCH_NOT_RUN` with `data.tasks[]` holding each task's id, base, verify status, review status and reason, `reused`, `retried` and `aborted`, without the full reports, which stay under `.agent-ops/reviews/`. Exit 0 when every task passed, 1 when any review or verification failed, and 2 otherwise. `batch` is started like `review`: it needs `--yes` and trust, and REVIEW-HOST-001 applies to it.
|
|
195
|
+
- Evidence: The envelope code and the exit code agree with the per-task outcomes.
|
|
196
|
+
- Positive: `One FAIL and one NOT_RUN exit 1, so the defect is handled first.`
|
|
197
|
+
- Negative: `Exit 0 while a task is still NOT_RUN.`
|
|
@@ -121,6 +121,9 @@ preflight 判定目標未認證時,會再對該目標的 API 主機(agy 為
|
|
|
121
121
|
`api.anthropic.com`)的 443 埠做 2 秒 TCP 連線測試,據此在兩種原因間擇一;
|
|
122
122
|
它只修正回報的原因,不會阻擋 review。
|
|
123
123
|
只有偵測到認證失敗時,review 才會建議執行 `doctor --check-auth`。
|
|
124
|
+
探測失敗但輸出沒有網路或登入字樣時為 `probe-failed`,不會被當成
|
|
125
|
+
`login-required`:preflight 紀錄與 `doctor` 會引述目標自己的第一行輸出
|
|
126
|
+
(agy 在 stderr 為空時取其 log 檔最後一行),讓真正原因可見而非猜測。
|
|
124
127
|
可用以下指令確認目標的認證狀態:
|
|
125
128
|
|
|
126
129
|
```bash
|
|
@@ -177,6 +180,44 @@ fingerprint,因此兩個對話修改同一個 checkout 會互相作廢對方
|
|
|
177
180
|
- Session 的 completion gate 會跟著它:Claude Code 以 EnterWorktree 進入
|
|
178
181
|
worktree;無法移動 session 的 host(agy)則透過主 checkout 記錄的 redirect,
|
|
179
182
|
由 worktree 的 gate 判定。
|
|
183
|
+
- 指令依序以 `--session`、`AGENT_OPS_SESSION_ID`、`CODEX_THREAD_ID`、SessionStart
|
|
184
|
+
為該 checkout 記錄的 id 得知自己的 session。Claude Code 的 SessionStart 會把
|
|
185
|
+
`AGENT_OPS_SESSION_ID` 寫入 `CLAUDE_ENV_FILE`,之後每個 Bash 指令都帶有它。
|
|
186
|
+
十五分鐘內若有另一個 session 在同一 checkout 啟動,記錄的 id 視為有爭議:
|
|
187
|
+
`task create`、`attach`、`status` 會以 `SESSION_ID_AMBIGUOUS` 失敗,而不是以
|
|
188
|
+
錯誤的 session 行動。
|
|
189
|
+
- Worktree 屬於建立它的 session:寫入別的 session 的 worktree 會被拒絕
|
|
190
|
+
(`WORKTREE_OWNED_BY_OTHER_SESSION`),並指出該 session 自己的 worktree。沒有
|
|
191
|
+
baseline 的 session(SessionStart 沒送達)會在第一次呼叫工具時補建,絕不會在
|
|
192
|
+
Stop 時補建。
|
|
193
|
+
- 在 worktree 內,專案 config 一律取自主 checkout,因此分支無法在合併前改變判定
|
|
194
|
+
它自己的 gate、verifier 或 trust;主 checkout 沒有 config 時才使用 worktree 自己的。
|
|
195
|
+
- `agent-ops worktree commit --message <text>` 會暫存並 commit 目前 worktree 的
|
|
196
|
+
所有變更。Git 由 agent-ops 自己執行而非經過 shell,因此即使 host 無法證明某條
|
|
197
|
+
shell 指令停留在 worktree 內,session 仍能 commit;它會拒絕主 checkout、別的
|
|
198
|
+
session 擁有的 worktree、無法辨識的呼叫者,以及沒有變更的情況。它會暫存所有
|
|
199
|
+
Git 未忽略的檔案,所以請不要把雜檔留在 worktree 裡。
|
|
200
|
+
- 在 auto 模式下,Claude 的 PreToolUse hook 也處理 `EnterWorktree`:切入恰好是
|
|
201
|
+
為呼叫 session 記錄的 worktree 時,不必經過 Claude Code 自己的確認即放行;其他
|
|
202
|
+
任何路徑一律拒絕(`WORKTREE_ENTER_DENIED`,並列出該 session 的 worktree)。請
|
|
203
|
+
執行一次 `agent-ops update`,讓 hook 的 matcher 包含 `EnterWorktree`;`doctor`
|
|
204
|
+
會把較舊的 matcher 回報為註冊漂移。
|
|
205
|
+
- 當 `worktree.mode` 為 auto,`init` 與 `update` 也會預先授權 session 自己會執行的
|
|
206
|
+
指令,並在你確認前先列在計畫裡:在 `.claude/settings.local.json` 寫入
|
|
207
|
+
`agent-ops task`、`verify`、`review`、`worktree`、`doctor` 的 `permissions.allow`,
|
|
208
|
+
以及 `agent-ops review` 的 `sandbox.excludedCommands`(你原有的項目會保留,計畫只
|
|
209
|
+
描述該檔案而不印出內容);若 CLI 找得到 Codex 的 home(`CODEX_HOME`,否則
|
|
210
|
+
`~/.codex`),另寫入 `rules/agent-ops.rules`,內含 `agent-ops review` 與
|
|
211
|
+
`doctor --check-auth` 的 allow 規則(含與不含 `AGENT_OPS_HOST=`)。`trust`、
|
|
212
|
+
`allow-stop`、`init`、`update`、`uninstall` 永遠不會被預先授權。關閉 auto 模式或
|
|
213
|
+
`uninstall` 會精確移除這些項目;同名但不是 agent-ops 寫的規則檔不會被動到。
|
|
214
|
+
- `review` 與 `doctor` 支援 `--output <file>`:JSON envelope 寫入該檔(權限 0600),
|
|
215
|
+
並只印出一行摘要。請用它取代 shell 重導向;Codex 會把重導向包成 `zsh -lc`,只能
|
|
216
|
+
以整串指令比對來放行。
|
|
217
|
+
- Trust 綁定 config hash,因此手動編輯 `.agent-ops/config.json`(例如加入
|
|
218
|
+
`worktree.setup`)會讓 repository 變成未受信任,`WORKTREE_SETUP_UNTRUSTED` 會擋下
|
|
219
|
+
下一個 worktree。唯一的授權點屬於使用者:`agent-ops update` 會列出它將信任的指令並
|
|
220
|
+
詢問一次。
|
|
180
221
|
- 在 worktree 裡對每個 task 執行 verify 與 review,但不要執行 `task complete`
|
|
181
222
|
(Claude Code 在隔離的 worktree session 中會拒絕它)。`agent-ops worktree finish
|
|
182
223
|
<name>` 會在任何 rebase 之前 complete worktree 內每個未封存的 task,先 subtask
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 審查
|
|
2
2
|
|
|
3
|
-
English source version: 2026-
|
|
3
|
+
English source version: 2026-09-30. Revalidate: when the English specification changes.
|
|
4
4
|
|
|
5
5
|
## REVIEW-INDEPENDENT-001
|
|
6
6
|
|
|
@@ -116,3 +116,73 @@ permission bypass。外部 host runner 不可用時,結果 MUST 保持 NOT_RUN
|
|
|
116
116
|
- Evidence: 結果的 reason 能區分協議違規與判定結果。
|
|
117
117
|
- Positive: `NOT_RUN:unparseable-output;缺少一條 criterion。`
|
|
118
118
|
- Negative: `因為模型的 JSON 格式錯誤就記錄一次失敗的審查。`
|
|
119
|
+
|
|
120
|
+
## REVIEW-BATCH-001
|
|
121
|
+
|
|
122
|
+
批次審查 MUST 只涵蓋指定 parent 的 active 子 task 以及 parent 本身。
|
|
123
|
+
|
|
124
|
+
- Trigger: 執行 `batch --parent <task-id>`。
|
|
125
|
+
- Action: 依建立順序選出 parent 的 active 子 task,再加上 parent。排除 archived、已完成的 task 與屬於其他 parent 的 task。未知 parent 以 `BATCH_PARENT_NOT_FOUND` 拒絕,非 active 的以 `BATCH_PARENT_NOT_ACTIVE` 拒絕。
|
|
126
|
+
- Evidence: `data.tasks` 依序列出的正是這些 task id。
|
|
127
|
+
- Positive: `兩個 active 子 task 與 parent 被涵蓋;archived 的子 task 不在內。`
|
|
128
|
+
- Negative: `不論 parent 為何,涵蓋 worktree 內所有 task。`
|
|
129
|
+
|
|
130
|
+
## REVIEW-BATCH-002
|
|
131
|
+
|
|
132
|
+
每個 task 的 base MUST 依序取自明確旗標、該 task 已記錄的 review base、worktree base。
|
|
133
|
+
|
|
134
|
+
- Trigger: 決定批次中某個 task 驗證與審查所用的範圍。
|
|
135
|
+
- Action: 子 task 用 `--base`,parent 用 `--parent-base`;否則用該 task 已記錄的 `reviewBase`;再否則用 worktree base。都沒有時回報 `BATCH_BASE_UNKNOWN`。
|
|
136
|
+
- Evidence: `data.tasks[].base` 指出每個 task 使用的 base。
|
|
137
|
+
- Positive: `重跑時沿用每個已 PASS task 記錄的 base,fingerprint 與 attestation 仍然吻合。`
|
|
138
|
+
- Negative: `把 --base 套到 parent,或在不知道 base 時退回 HEAD。`
|
|
139
|
+
|
|
140
|
+
## REVIEW-BATCH-003
|
|
141
|
+
|
|
142
|
+
驗證 MUST 一次只跑一個 task,每個 task 的審查 MUST 在它自己的驗證通過後立刻開始,同時進行的審查 MUST NOT 超過 `--width`(預設 2)。
|
|
143
|
+
|
|
144
|
+
- Trigger: 批次中的 task 需要驗證與審查。
|
|
145
|
+
- Action: 已有 fresh PASS evidence 的 task 略過驗證,判斷方式與 review 預檢相同。其餘串行驗證。task 驗證通過後立刻開始它的審查。驗證失敗的 task 不開審查,其他 task 繼續。
|
|
146
|
+
- Evidence: `data.tasks[].verify` 為 `skipped`、`PASS` 或 `FAIL`,驗證失敗的 task 沒有 review。
|
|
147
|
+
- Positive: `task A 審查的同時驗證 task B。`
|
|
148
|
+
- Negative: `同時跑每個 task 的完整測試。`
|
|
149
|
+
|
|
150
|
+
## REVIEW-BATCH-004
|
|
151
|
+
|
|
152
|
+
暫時性的 NOT_RUN MUST 讓其餘審查的 width 降為 1 並只重試一次,其他結果 MUST 為最終結果。
|
|
153
|
+
|
|
154
|
+
- Trigger: 批次中的某個審查回傳 NOT_RUN。
|
|
155
|
+
- Action: 原因為 `probe-failed`、`stalled`、`timeout`、`quota-exhausted` 或 `network-unreachable` 時,讓所有尚未開始的審查 width 降為 1,並在其餘審查結束後,單獨重試該 task 一次。其他原因不重試,不重試第二次,width 也不再升回。
|
|
156
|
+
- Evidence: 被重試的 task `review.retried` 為 true,第二次暫時性 NOT_RUN 仍是 NOT_RUN。
|
|
157
|
+
- Positive: `task A 的 probe-failed 使 width 降為 1,A 在其他 task 之後再跑一次。`
|
|
158
|
+
- Negative: `對 host-sandboxed 不斷重試直到通過。`
|
|
159
|
+
|
|
160
|
+
## REVIEW-BATCH-005
|
|
161
|
+
|
|
162
|
+
來源變動或程序被中斷時,批次 MUST 停止。
|
|
163
|
+
|
|
164
|
+
- Trigger: HEAD 或工作樹與批次開始時不同,或程序收到 SIGINT 或 SIGTERM。
|
|
165
|
+
- Action: 開始前要求工作樹乾淨,每個步驟前檢查 HEAD 與工作樹。發現變動時,中止進行中的審查,不再開始新步驟,並把每個未完成的 task 回報為 `aborted`。訊號以 130 或 143 結束。
|
|
166
|
+
- Evidence: 每個未完成的 task 其 `data.tasks[].aborted` 為 true。
|
|
167
|
+
- Positive: `批次進行中發生 commit 就中止,未完成的 task 標示 aborted。`
|
|
168
|
+
- Negative: `讓進行中的審查對已不吻合的 HEAD 跑完。`
|
|
169
|
+
|
|
170
|
+
## REVIEW-BATCH-006
|
|
171
|
+
|
|
172
|
+
批次 MUST 在探測成功時對每個審查 target 最多探測一次,且 MUST NOT 快取失敗的探測。
|
|
173
|
+
|
|
174
|
+
- Trigger: 批次中的審查需要 target 預檢。
|
|
175
|
+
- Action: 同時進行的審查共用進行中的探測,成功的答案在批次剩餘期間保留。失敗的答案丟棄,讓 task 的重試再探測一次。
|
|
176
|
+
- Evidence: 多個審查的批次每個 target 只探測一次,失敗後的重試會產生新的探測。
|
|
177
|
+
- Positive: `五個審查共用一次成功的 agy 探測。`
|
|
178
|
+
- Negative: `快取 probe-failed 並拒絕其餘所有 task。`
|
|
179
|
+
|
|
180
|
+
## REVIEW-BATCH-007
|
|
181
|
+
|
|
182
|
+
批次結果 MUST 回報每個 task 的結果,且 MUST 僅在每個 task 都通過時以 0 結束。
|
|
183
|
+
|
|
184
|
+
- Trigger: 批次結束。
|
|
185
|
+
- Action: 回傳 `BATCH_RESULT`、`BATCH_FAILED` 或 `BATCH_NOT_RUN`,`data.tasks[]` 含每個 task 的 id、base、驗證狀態、審查狀態與原因、`reused`、`retried` 與 `aborted`,不含完整報告(報告留在 `.agent-ops/reviews/`)。全數通過以 0 結束,任一審查或驗證失敗以 1 結束,其餘以 2 結束。`batch` 的啟動方式與 `review` 相同:需要 `--yes` 與 trust,且適用 REVIEW-HOST-001。
|
|
186
|
+
- Evidence: envelope code 與 exit code 與各 task 的結果一致。
|
|
187
|
+
- Positive: `一個 FAIL 與一個 NOT_RUN 以 1 結束,先處理缺陷。`
|
|
188
|
+
- Negative: `仍有 task 是 NOT_RUN 卻以 0 結束。`
|