@kylecheng3146/agent-ops 0.1.5 → 0.1.7

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.
Files changed (49) hide show
  1. package/README.md +104 -6
  2. package/dist/packages/cli/src/args.js +33 -1
  3. package/dist/packages/cli/src/bin.js +40 -3
  4. package/dist/packages/cli/src/cli.js +13 -2
  5. package/dist/packages/cli/src/codex-loop-process.js +70 -0
  6. package/dist/packages/cli/src/commands/hook.js +16 -1
  7. package/dist/packages/cli/src/commands/init.js +4 -1
  8. package/dist/packages/cli/src/commands/review.js +97 -10
  9. package/dist/packages/cli/src/commands/update.js +3 -0
  10. package/dist/packages/cli/src/context.js +60 -0
  11. package/dist/packages/cli/src/hook-process.js +128 -15
  12. package/dist/packages/cli/src/loop-entry.js +8 -0
  13. package/dist/packages/cli/src/version.js +1 -1
  14. package/dist/packages/cli/src/wizard.js +71 -7
  15. package/dist/runtime/src/adapters/claude/config.js +57 -11
  16. package/dist/runtime/src/adapters/claude/events.js +7 -0
  17. package/dist/runtime/src/adapters/claude/output.js +2 -1
  18. package/dist/runtime/src/adapters/codex/config.js +39 -4
  19. package/dist/runtime/src/adapters/codex/events.js +7 -0
  20. package/dist/runtime/src/config/merge.js +17 -2
  21. package/dist/runtime/src/fs/managed-block.js +35 -18
  22. package/dist/runtime/src/hooks/codex-loop.js +439 -0
  23. package/dist/runtime/src/install/codex-loop.js +139 -0
  24. package/dist/runtime/src/install/doctor.js +108 -9
  25. package/dist/runtime/src/install/harness.js +8 -10
  26. package/dist/runtime/src/install/ownership.js +37 -2
  27. package/dist/runtime/src/install/plan.js +81 -9
  28. package/dist/runtime/src/install/profiles.js +5 -3
  29. package/dist/runtime/src/install/uninstall.js +1 -1
  30. package/dist/runtime/src/install/update.js +5 -1
  31. package/dist/runtime/src/logging/local-log.js +25 -0
  32. package/dist/runtime/src/review/execute.js +120 -0
  33. package/dist/runtime/src/review/extract.js +71 -0
  34. package/dist/runtime/src/review/invocation.js +52 -0
  35. package/dist/runtime/src/review/probe.js +48 -0
  36. package/dist/runtime/src/review/result.js +2 -2
  37. package/dist/runtime/src/review/roles.js +35 -0
  38. package/dist/runtime/src/review/runner.js +38 -4
  39. package/dist/runtime/src/schema/validate.js +70 -1
  40. package/dist/runtime/src/task/service.js +40 -0
  41. package/docs/en/guides/configuration.md +138 -2
  42. package/docs/en/spec/harness-adapters.md +50 -12
  43. package/docs/en/spec/review.md +37 -4
  44. package/docs/zh-TW/guides/configuration.md +126 -5
  45. package/docs/zh-TW/spec/harness-adapters.md +44 -12
  46. package/docs/zh-TW/spec/review.md +33 -3
  47. package/package.json +1 -1
  48. package/schemas/config.schema.json +30 -1
  49. package/schemas/manifest.schema.json +12 -1
package/README.md CHANGED
@@ -35,7 +35,7 @@ installation plan.
35
35
 
36
36
  The interactive multi-select screens start with no harness or profile selected.
37
37
  Choose at least one of `codex`, `claude`, and `opencode`, and at least one of
38
- the `core`, `advisory`, and `guardrails` profiles before confirming. For
38
+ the `core`, `advisory`, `guardrails`, and `loop` profiles before confirming. For
39
39
  scripted use, `--harness all` selects all three harnesses; comma-separated
40
40
  selections such as `codex,opencode` are also supported. The legacy `both` value
41
41
  remains an alias for `codex,claude`.
@@ -64,6 +64,54 @@ agent-ops update --yes --json
64
64
  agent-ops uninstall --dry-run --json
65
65
  ```
66
66
 
67
+ ### Project-local loop
68
+
69
+ `loop` is an explicit, project-only profile for Codex, Claude Code, or both.
70
+ It requires a POSIX-compatible `bash`; the generated native launchers are
71
+ `.sh` files, so Windows is not currently a supported loop host. Preview it
72
+ first, then install only after reviewing the plan. `loop` also implies the
73
+ `core` baseline, so the project retains the managed rules and routing files:
74
+
75
+ ```bash
76
+ agent-ops init --dry-run --scope project --harness codex,claude --profile loop --json
77
+ agent-ops init --scope project --harness codex,claude --profile loop --yes
78
+ ```
79
+
80
+ It creates one small managed launcher per selected native host:
81
+
82
+ - Codex: `.codex/hooks/agent-ops-loop.sh`, `.codex/config.toml` when absent,
83
+ `.codex/loop-goal.md`, `.codex/loop-state.md`, and
84
+ `.codex/loop-telemetry.jsonl`.
85
+ - Claude Code: `.claude/hooks/agent-ops-loop.sh`, `.claude/loop-goal.md`,
86
+ `.claude/loop-state.md`, and `.claude/loop-telemetry.jsonl`.
87
+
88
+ The launchers delegate to one installed Node runtime; agent-ops does not copy
89
+ project-specific policy code into either hook directory. It adds an exact,
90
+ hash-commented `.gitignore` block for the goal, state, and telemetry files.
91
+ Those files and `.codex/config.toml` remain user-owned: update never overwrites
92
+ them, and uninstall keeps them while removing only launchers, hook handlers,
93
+ and the managed ignore block.
94
+
95
+ The loop registers `SessionStart`, `UserPromptSubmit`, `PreToolUse`,
96
+ `PermissionRequest`, `PostToolUse`, `PreCompact`, `PostCompact`,
97
+ `SubagentStart`, and `SubagentStop`; it intentionally does not register
98
+ `Stop`. On high-confidence matches, it blocks literal credential-shaped user
99
+ prompts or Bash commands, plus dangerous Bash commands such as broad recursive
100
+ deletion or `git reset --hard`. Codex uses its documented exit-code denial path; Claude Code
101
+ uses its documented native JSON denial shapes. Permission and escalation
102
+ requests are never auto-approved or denied by the loop, so the harness's normal
103
+ approval prompt remains authoritative.
104
+
105
+ Session context is bounded and derives from the redacted goal plus a telemetry
106
+ count. Telemetry records only timestamp, event, outcome, and rule code; it
107
+ never stores raw prompts, Bash commands, or credentials and is byte-rotated.
108
+ Before compaction, the loop writes a bounded, redacted Git-status snapshot into
109
+ its own block inside `loop-state.md`, preserving surrounding user text. This is
110
+ a guardrail, not a complete sandbox or a replacement for each harness's own
111
+ permissions. Review and trust the generated hook configuration in Codex and
112
+ Claude Code before use. An existing Codex `.codex/config.toml` with an explicit
113
+ `[features]` / `hooks = false` setting stops installation before any write.
114
+
67
115
  Use `--scope user` with user-home installations. Keep `--dry-run` for any
68
116
  operation you want to inspect before applying; non-interactive automation should
69
117
  pass `--yes` only after reviewing the plan. Add `--json` when another tool will
@@ -121,6 +169,12 @@ The commands after `init --yes` are post-apply operations. `doctor` reports
121
169
  `trust grant` runs, and `smoke-availability` until the configuration declares a
122
170
  verification command.
123
171
 
172
+ `artifact-staleness` reports `DEGRADED` with `UPDATE_REQUIRED` when a toolkit
173
+ upgrade or effective profile or capability change makes intact managed rules
174
+ differ from the current baseline. Run `agent-ops update` to regenerate them.
175
+ Missing, altered, or hash-mismatched managed artifacts remain `FAIL` under
176
+ `artifacts`.
177
+
124
178
  Installing the `advisory` or `guardrails` profile registers the lifecycle and
125
179
  command-policy hooks implied by those profiles for the selected harnesses.
126
180
  Claude Code and Codex use their native JSON settings files; opencode uses the agent-ops-owned
@@ -132,11 +186,24 @@ scope, the opencode plugin is placed under
132
186
  `$XDG_CONFIG_HOME/opencode/` or `$OPENCODE_CONFIG_DIR/` when that location is
133
187
  inside the managed user root. The shims call `agent-ops hook <harness> <event>`
134
188
  through the installed absolute runtime path. Advisory failures remain
135
- fail-open; command-policy failures are fail-closed only where the native
136
- harness can block the tool. Claude and Codex lifecycle summaries are
137
- `supported`; OpenCode's app-scoped initialization is `degraded` rather than
138
- per-session coverage. Codex command denial remains `unknown` until native
139
- blocking is confirmed.
189
+ fail-open. Runtime-failure enforcement is deliberately narrow: Claude Code can
190
+ emit its documented `PreToolUse` denial shape for a classified invalid installed
191
+ configuration, and the managed OpenCode `tool.execute.before` plugin throws
192
+ its documented command-policy denial or unavailable-runtime error for its
193
+ supported Bash surface. Codex command policy is `unknown` and explicitly
194
+ non-enforcing for the ordinary `guardrails` profile. The project-local `loop`
195
+ profile uses its separate native hook policy described above. These are output
196
+ and plugin contracts, not proof that a host
197
+ honors a denial. Claude and Codex lifecycle summaries are `supported`; OpenCode's
198
+ app-scoped initialization is `degraded` rather than per-session coverage.
199
+
200
+ Claude's invalid-config fallback has four safeguards: only an invalid (not
201
+ absent) `.agent-ops/config.json` can reach it; a safely read manifest must list
202
+ the current harness; `AGENT_OPS_DISABLE=1` must not be set; and Claude's denial
203
+ reason names the config file with a repair or temporary shell-disable remedy.
204
+ `AGENT_OPS_DISABLE=1` is a human-shell recovery variable only. Agent-ops never
205
+ reads it from configuration, a manifest, or another file it writes. Every
206
+ `SessionStart` and `Stop` failure path remains fail-open.
140
207
 
141
208
  `guardrails` enables command policy only; it does not imply Stop verification.
142
209
  Stop verification is an explicit, disabled-by-default config feature. Enable it
@@ -173,6 +240,15 @@ files authoritative. Existing installations with the previous canonical
173
240
  wording are migrated by `agent-ops update`; changed managed blocks still fail
174
241
  closed.
175
242
 
243
+ ### Rejected proposals and deliberate boundaries
244
+
245
+ The following proposals are deliberately rejected: emitting a model-visible
246
+ `SessionStart` advisory summary, inspecting user-authored Markdown link
247
+ integrity in `doctor`, and creating backups for agent-authored rule edits.
248
+ Transactional backups remain limited to agent-ops apply operations. Agent-ops
249
+ also does not add a git-workflow instruction to the generated baseline, which
250
+ stays project-neutral.
251
+
176
252
  Dry-run plans keep harness settings writes opaque: human and JSON output expose
177
253
  only the expected hash, content hash, and a safe summary. Use
178
254
  `--hook-target <harness>=<surface-id>` when selecting a non-default discovered
@@ -185,6 +261,28 @@ For a full command reference, run `agent-ops --help`. The `task`, `verify`, and
185
261
  `review` commands support acceptance tracking and independent verification when
186
262
  the project configuration defines those workflows.
187
263
 
264
+ ### External review
265
+
266
+ `agent-ops review` can hand the review to another agent CLI, so the work is not
267
+ judged by the agent that produced it. Enable it during `agent-ops init` (the
268
+ default is off) and pick an ordered fallback chain of targets: `codex`, `agy`
269
+ (Antigravity), and `claude`. Each is launched with its own read-only flag, and a
270
+ target without one is skipped rather than run unsandboxed — which is why
271
+ `opencode` is not a review target despite being a supported harness.
272
+
273
+ The first target that actually runs produces the verdict. A `FAIL` is final:
274
+ the chain never retries elsewhere after a real verdict. `--yes` is still
275
+ required for every run, since each run spends another provider's quota.
276
+
277
+ Authentication is diagnosed, never guessed:
278
+
279
+ ```bash
280
+ agent-ops doctor # presence only: no tokens, no network
281
+ agent-ops doctor --check-auth # one real print call per configured target
282
+ ```
283
+
284
+ See [Configuration](docs/en/guides/configuration.md) for the full contract.
285
+
188
286
  ## Project principles
189
287
 
190
288
  - Define verifiable success before making changes.
@@ -12,7 +12,9 @@ export const COMMAND_NAMES = [
12
12
  ];
13
13
  const COMMAND_SET = new Set(COMMAND_NAMES);
14
14
  const SCOPES = new Set(["project", "user"]);
15
- const PROFILES = new Set(["advisory", "core", "guardrails"]);
15
+ const PROFILES = new Set(["advisory", "core", "guardrails", "loop"]);
16
+ // opencode is absent: it has no read-only flag, so it cannot review.
17
+ const REVIEW_TARGETS = new Set(["agy", "claude", "codex"]);
16
18
  export class CliArgumentError extends Error {
17
19
  code;
18
20
  option;
@@ -65,8 +67,10 @@ export function parseArgs(argv) {
65
67
  let title;
66
68
  let sessionId;
67
69
  const profiles = [];
70
+ const reviewTargets = [];
68
71
  const criteria = [];
69
72
  const evidence = [];
73
+ let checkAuth = false;
70
74
  let dryRun = false;
71
75
  let json = false;
72
76
  let yes = false;
@@ -121,6 +125,18 @@ export function parseArgs(argv) {
121
125
  index += 1;
122
126
  break;
123
127
  }
128
+ case "--review-target": {
129
+ const value = readOptionValue(argv, index, token);
130
+ if (!REVIEW_TARGETS.has(value)) {
131
+ invalidValue(token, value);
132
+ }
133
+ if (reviewTargets.includes(value)) {
134
+ duplicate(`${token} ${value}`);
135
+ }
136
+ reviewTargets.push(value);
137
+ index += 1;
138
+ break;
139
+ }
124
140
  case "--task": {
125
141
  if (taskId !== undefined) {
126
142
  duplicate(token);
@@ -163,6 +179,12 @@ export function parseArgs(argv) {
163
179
  index += 1;
164
180
  break;
165
181
  }
182
+ case "--check-auth":
183
+ if (checkAuth) {
184
+ duplicate(token);
185
+ }
186
+ checkAuth = true;
187
+ break;
166
188
  case "--dry-run":
167
189
  if (dryRun) {
168
190
  duplicate(token);
@@ -249,7 +271,9 @@ export function parseArgs(argv) {
249
271
  title !== undefined ||
250
272
  criteria.length > 0 ||
251
273
  evidence.length > 0 ||
274
+ reviewTargets.length > 0 ||
252
275
  sessionId !== undefined ||
276
+ checkAuth ||
253
277
  dryRun ||
254
278
  yes) {
255
279
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Only --json may be combined with global help or version.");
@@ -284,6 +308,12 @@ export function parseArgs(argv) {
284
308
  command !== "update") {
285
309
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--hook-target may be used only with init or update.");
286
310
  }
311
+ if (checkAuth && command !== "doctor") {
312
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--check-auth may be used only with doctor.");
313
+ }
314
+ if (reviewTargets.length > 0 && command !== "init") {
315
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--review-target may be used only with init.");
316
+ }
287
317
  if (command === "task") {
288
318
  if (harness !== undefined ||
289
319
  profiles.length > 0 ||
@@ -345,9 +375,11 @@ export function parseArgs(argv) {
345
375
  ...(taskId === undefined ? {} : { taskId }),
346
376
  ...(targetVersion === undefined ? {} : { targetVersion }),
347
377
  ...(title === undefined ? {} : { title }),
378
+ ...(reviewTargets.length === 0 ? {} : { reviewTargets }),
348
379
  ...(criteria.length === 0 ? {} : { criteria }),
349
380
  ...(evidence.length === 0 ? {} : { evidence }),
350
381
  ...(sessionId === undefined ? {} : { sessionId }),
382
+ ...(checkAuth ? { checkAuth } : {}),
351
383
  dryRun,
352
384
  json,
353
385
  yes
@@ -29,6 +29,9 @@ import { formatInstallPlan, runInitCommand } from "./commands/init.js";
29
29
  import { formatUninstallPlan, runUninstallCommand } from "./commands/uninstall.js";
30
30
  import { runTaskCommand } from "./commands/task.js";
31
31
  import { runReviewCommand } from "./commands/review.js";
32
+ import { createReviewExecutor } from "../../../runtime/src/review/execute.js";
33
+ import { probeReviewTarget } from "../../../runtime/src/review/probe.js";
34
+ import { resolveReviewRole } from "../../../runtime/src/review/roles.js";
32
35
  import { runTrustCommand } from "./commands/trust.js";
33
36
  import { runVerifyCommand } from "./commands/verify.js";
34
37
  import { formatUpdatePlan, runUpdateCommand } from "./commands/update.js";
@@ -131,6 +134,7 @@ else {
131
134
  const config = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
132
135
  return await runDoctorCommand({
133
136
  root,
137
+ toolkitVersion: CLI_VERSION,
134
138
  probes: {
135
139
  hookRegistration: async () => hookRegistrationSatisfied({
136
140
  harness: await installedHarness(root),
@@ -138,8 +142,12 @@ else {
138
142
  sources: await hookSources(root, args.scope === "user" ? "user" : "project")
139
143
  }),
140
144
  repositoryTrust: async () => repositoryTrustStatus(await repositoryTrust(root, config, CLI_VERSION)),
141
- smokeAvailability: () => smokeAvailabilityStatus(config)
142
- }
145
+ smokeAvailability: () => smokeAvailabilityStatus(config),
146
+ reviewTarget: async (target, deep) => await probeReviewTarget(target, { cwd: root, deep })
147
+ },
148
+ ...(args.checkAuth === true
149
+ ? { checkReviewTargetAuth: true }
150
+ : {})
143
151
  });
144
152
  }
145
153
  if (args.command === "uninstall") {
@@ -157,6 +165,7 @@ else {
157
165
  adapters: commonHarnessAdapters(),
158
166
  registry: new NpmRegistryClient(),
159
167
  isTTY,
168
+ toolkitVersion: CLI_VERSION,
160
169
  hookRuntimePath: HOOK_RUNTIME_PATH,
161
170
  ...(args.hookTargets === undefined
162
171
  ? {}
@@ -177,9 +186,37 @@ else {
177
186
  });
178
187
  }
179
188
  if (args.command === "review") {
189
+ const reviewSessionId = process.env.AGENT_OPS_SESSION_ID;
190
+ const reviewConfig = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
191
+ const reviewRole = resolveReviewRole("independent-review", reviewConfig.reviewRoles ?? []);
180
192
  return await runReviewCommand({
181
193
  args,
182
- authorized: args.yes
194
+ authorized: args.yes,
195
+ tasks: taskService,
196
+ ...(reviewSessionId === undefined
197
+ ? {}
198
+ : { sessionId: reviewSessionId }),
199
+ ...(reviewConfig.reviewRoles === undefined
200
+ ? {}
201
+ : { roles: reviewConfig.reviewRoles }),
202
+ execute: createReviewExecutor({
203
+ targets: reviewRole?.targets ?? [],
204
+ cwd: root,
205
+ ...(reviewRole?.model === undefined
206
+ ? {}
207
+ : { model: reviewRole.model }),
208
+ ...(reviewRole?.effort === undefined
209
+ ? {}
210
+ : { effort: reviewRole.effort }),
211
+ ...(reviewRole?.timeoutMs === undefined
212
+ ? {}
213
+ : { timeoutMs: reviewRole.timeoutMs }),
214
+ onProgress: (line) => {
215
+ if (!args.json) {
216
+ process.stderr.write(`${line}\n`);
217
+ }
218
+ }
219
+ })
183
220
  });
184
221
  }
185
222
  if (args.command === "config") {
@@ -2,6 +2,7 @@ import { CliArgumentError, parseArgs } from "./args.js";
2
2
  import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
3
3
  import { errorEnvelope, okEnvelope, writeEnvelope } from "./output.js";
4
4
  import { completeInitChoices } from "./wizard.js";
5
+ import { probeReviewTarget } from "../../../runtime/src/review/probe.js";
5
6
  import { BANNER } from "./ui.js";
6
7
  export function renderWelcome(color) {
7
8
  const cyan = color ? "\u001b[36m" : "";
@@ -31,7 +32,11 @@ Options:
31
32
  --scope <project|user>
32
33
  --harness <all|both|claude|codex|opencode|comma-separated> Init/update
33
34
  --hook-target <harness=surface-id> Repeatable advanced init/update option
34
- --profile <core|advisory|guardrails> Repeatable
35
+ --profile <core|advisory|guardrails|loop> Repeatable
36
+ --review-target <codex|agy|claude> Repeatable init option; external review
37
+ targets in fallback-chain order
38
+ --check-auth Doctor only: probe each review target's
39
+ authentication with one real call
35
40
  --task <id>
36
41
  --target-version <version> Update target version (offline-capable)
37
42
  --title <text>
@@ -76,7 +81,13 @@ export async function runCli(argv, io, services) {
76
81
  }
77
82
  try {
78
83
  if (args.command === "init") {
79
- args = await completeInitChoices(args, args.json ? { ...io, isTTY: false } : io);
84
+ args = await completeInitChoices(args, args.json ? { ...io, isTTY: false } : io, {
85
+ probeReviewTarget: async (target) => (await probeReviewTarget(target, {
86
+ cwd: process.cwd(),
87
+ deep: true
88
+ })) === "ok",
89
+ warn: (message) => io.writeStderr(`${message}\n`)
90
+ });
80
91
  }
81
92
  const execute = args.command === "help" || args.command === "version"
82
93
  ? services.execute
@@ -0,0 +1,70 @@
1
+ import { PROJECT_LOOP_EVENTS, runProjectLoop } from "../../../runtime/src/hooks/codex-loop.js";
2
+ const MAX_LOOP_INPUT_BYTES = 64 * 1024;
3
+ function isLoopHarness(value) {
4
+ return value === "claude" || value === "codex";
5
+ }
6
+ function isLoopEvent(value) {
7
+ return (value !== undefined &&
8
+ PROJECT_LOOP_EVENTS.includes(value));
9
+ }
10
+ async function readStdin(stream) {
11
+ const chunks = [];
12
+ let total = 0;
13
+ for await (const chunk of stream) {
14
+ const buffer = Buffer.isBuffer(chunk)
15
+ ? chunk
16
+ : Buffer.from(String(chunk), "utf8");
17
+ total += buffer.byteLength;
18
+ if (total > MAX_LOOP_INPUT_BYTES) {
19
+ return null;
20
+ }
21
+ chunks.push(buffer);
22
+ }
23
+ return Buffer.concat(chunks, total).toString("utf8");
24
+ }
25
+ function parseInput(source) {
26
+ if (source === null) {
27
+ return null;
28
+ }
29
+ try {
30
+ return JSON.parse(source);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * Process boundary for the generated Bash launchers. Invalid input stays
38
+ * fail-open; the runtime owns every policy decision and output shape.
39
+ */
40
+ export async function runLoopProcess(argv, io, dependencies = {}) {
41
+ const [harness, event] = argv;
42
+ if (!isLoopHarness(harness) || !isLoopEvent(event)) {
43
+ return 0;
44
+ }
45
+ try {
46
+ const result = await runProjectLoop({
47
+ harness,
48
+ event,
49
+ input: parseInput(await readStdin(io.stdin)),
50
+ root: dependencies.root ?? process.cwd(),
51
+ ...(dependencies.now === undefined ? {} : { now: dependencies.now }),
52
+ ...(dependencies.gitStatus === undefined
53
+ ? {}
54
+ : { gitStatus: dependencies.gitStatus }),
55
+ ...(dependencies.telemetryMaxBytes === undefined
56
+ ? {}
57
+ : { telemetryMaxBytes: dependencies.telemetryMaxBytes })
58
+ });
59
+ if (result.stdout.length > 0) {
60
+ io.writeStdout(result.stdout);
61
+ }
62
+ if (result.stderr.length > 0) {
63
+ io.writeStderr(result.stderr);
64
+ }
65
+ return result.exitCode;
66
+ }
67
+ catch {
68
+ return 0;
69
+ }
70
+ }
@@ -7,6 +7,18 @@ export const HOOK_EVENTS = [
7
7
  "PreToolUse",
8
8
  "Stop"
9
9
  ];
10
+ /**
11
+ * Keep adapter normalization at the command boundary so exceptional hook
12
+ * paths use the same native-event interpretation as normal dispatch.
13
+ */
14
+ export function normalizeHookInput(harness, input) {
15
+ try {
16
+ return harnessDescriptor(harness).runtime.normalizeInput(input);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
10
22
  /**
11
23
  * Hooks are advisory infrastructure: every failure path stays fail-open with
12
24
  * exit code 0 so a broken toolkit can never wedge the harness.
@@ -24,7 +36,10 @@ export async function runHookCommand(options) {
24
36
  return { exitCode: 0, stdout: "", stderr: "" };
25
37
  }
26
38
  const descriptor = harnessDescriptor(options.harness);
27
- const normalized = descriptor.runtime.normalizeInput(input);
39
+ const normalized = normalizeHookInput(options.harness, input);
40
+ if (normalized === null) {
41
+ return { exitCode: 0, stdout: "", stderr: "" };
42
+ }
28
43
  const stopRegistration = descriptor.control.registrations.find(({ capability }) => capability === "optional-stop-verify");
29
44
  const stopVerification = options.stopVerification !== undefined &&
30
45
  stopRegistration?.support !== "unsupported"
@@ -63,7 +63,10 @@ export async function runInitCommand(options) {
63
63
  : { hookRuntimePath: options.hookRuntimePath }),
64
64
  ...((options.hookTargets ?? args.hookTargets) === undefined
65
65
  ? {}
66
- : { hookTargets: options.hookTargets ?? args.hookTargets })
66
+ : { hookTargets: options.hookTargets ?? args.hookTargets }),
67
+ ...(args.reviewTargets === undefined
68
+ ? {}
69
+ : { reviewTargets: args.reviewTargets })
67
70
  });
68
71
  if (args.dryRun) {
69
72
  return okEnvelope("INIT_PLAN_READY", {
@@ -3,18 +3,78 @@ import { runIndependentReview } from "../../../../runtime/src/review/runner.js";
3
3
  import { resolveReviewRole } from "../../../../runtime/src/review/roles.js";
4
4
  import { okEnvelope } from "../output.js";
5
5
  /**
6
- * Review runs against one harness. Argument parsing already rejects a
6
+ * Review runs against one target. Argument parsing already rejects a
7
7
  * multi-harness selection here, so the first entry is the whole selection.
8
+ * `opencode` is not a review target — it has no read-only flag — so selecting
9
+ * it leaves the target unresolved and the configured chain decides.
8
10
  */
9
11
  function harness(value) {
10
- return value?.[0] ?? "codex";
12
+ const selected = value?.[0];
13
+ return selected === undefined || selected === "opencode"
14
+ ? undefined
15
+ : selected;
11
16
  }
12
- export async function runReviewCommand(options) {
13
- const ids = options.args.criteria ?? [];
14
- const criteria = ids.map((id) => ({
15
- id,
16
- description: id
17
+ /**
18
+ * Criterion descriptions come from the task store, never from the id. A
19
+ * reviewer handed `criterion: tests` cannot review anything, so a review with
20
+ * no task context is reported as not run rather than run meaninglessly.
21
+ */
22
+ async function taskContext(options) {
23
+ const tasks = options.tasks;
24
+ if (tasks === undefined) {
25
+ return undefined;
26
+ }
27
+ const query = options.taskId !== undefined
28
+ ? { taskId: options.taskId }
29
+ : options.sessionId === undefined
30
+ ? undefined
31
+ : { sessionId: options.sessionId };
32
+ if (query === undefined) {
33
+ return undefined;
34
+ }
35
+ let record;
36
+ try {
37
+ record = await tasks.status(query);
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ const requested = options.args.criteria ?? [];
43
+ const criteria = record.task.criteria
44
+ .filter((criterion) => requested.length === 0 || requested.includes(criterion.id))
45
+ .map((criterion) => ({
46
+ id: criterion.id,
47
+ description: criterion.description,
48
+ verifierIds: [...criterion.verifierIds]
17
49
  }));
50
+ if (criteria.length === 0 ||
51
+ (requested.length > 0 && criteria.length !== requested.length)) {
52
+ return undefined;
53
+ }
54
+ return {
55
+ taskId: record.task.id,
56
+ active: record.status === "active",
57
+ criteria
58
+ };
59
+ }
60
+ function notRunEnvelope(result) {
61
+ const message = "Independent review was not run.";
62
+ return {
63
+ code: "REVIEW_NOT_RUN",
64
+ status: "error",
65
+ data: {
66
+ message,
67
+ result,
68
+ text: [message, `Reason: ${result.reason ?? "unknown"}.`, ""].join("\n")
69
+ },
70
+ errors: [{ code: "REVIEW_NOT_RUN", message }]
71
+ };
72
+ }
73
+ export async function runReviewCommand(options) {
74
+ const role = resolveReviewRole(options.role ?? "independent-review", options.roles ?? []);
75
+ const selectedHarness = harness(options.args.harness);
76
+ const target = role?.targets[0] ?? selectedHarness ?? "codex";
77
+ const context = await taskContext(options);
18
78
  const evidenceRequirements = (options.args.evidence ?? []).map((value) => {
19
79
  const separator = value.indexOf("=");
20
80
  return {
@@ -22,11 +82,22 @@ export async function runReviewCommand(options) {
22
82
  requirement: separator < 0 ? value : value.slice(separator + 1)
23
83
  };
24
84
  });
25
- const selectedHarness = harness(options.args.harness);
26
- const role = resolveReviewRole(options.role ?? "independent-review", options.roles ?? []);
85
+ if (options.tasks !== undefined && context === undefined) {
86
+ return notRunEnvelope({
87
+ status: "NOT_RUN",
88
+ reason: "no-task-context",
89
+ harness: target,
90
+ model: role?.model ?? options.model ?? "configured",
91
+ effort: role?.effort ?? options.effort ?? "configured",
92
+ prompt: ""
93
+ });
94
+ }
95
+ const criteria = context?.criteria !== undefined
96
+ ? [...context.criteria]
97
+ : (options.args.criteria ?? []).map((id) => ({ id, description: id }));
27
98
  const result = await runIndependentReview({
28
99
  invocation: {
29
- harness: role?.harness ?? selectedHarness,
100
+ harness: target,
30
101
  model: role?.model ?? options.model ?? "configured",
31
102
  effort: role?.effort ?? options.effort ?? "configured",
32
103
  packet: buildReviewPacket({
@@ -42,6 +113,17 @@ export async function runReviewCommand(options) {
42
113
  reason: "missing-cli"
43
114
  }))
44
115
  });
116
+ // Evidence is only appended while the task is active: a completed record
117
+ // must stay exactly as it was verified.
118
+ if (options.tasks !== undefined &&
119
+ context !== undefined &&
120
+ context.active &&
121
+ result.results !== undefined) {
122
+ await options.tasks.recordEvidence(context.taskId, Object.fromEntries(result.results.map((item) => [
123
+ item.criterionId,
124
+ item.evidence.map((reference) => `review:${target}:${reference}`)
125
+ ])));
126
+ }
45
127
  const message = result.status === "PASS"
46
128
  ? "Independent review passed."
47
129
  : result.status === "FAIL"
@@ -55,6 +137,11 @@ export async function runReviewCommand(options) {
55
137
  `Status: ${result.status}`,
56
138
  `Harness: ${result.harness}; model: ${result.model}; effort: ${result.effort}.`,
57
139
  ...(result.reason === undefined ? [] : [`Reason: ${result.reason}.`]),
140
+ ...(result.status === "NOT_RUN"
141
+ ? [
142
+ "Run: agent-ops doctor --check-auth to verify target authentication."
143
+ ]
144
+ : []),
58
145
  ...(result.results === undefined
59
146
  ? []
60
147
  : result.results.map((item) => `${item.criterionId}: ${item.status} [${item.evidence.join(", ")}]`)),
@@ -38,6 +38,9 @@ export async function runUpdateCommand(options) {
38
38
  ...(options.targetVersion === undefined
39
39
  ? {}
40
40
  : { targetVersion: options.targetVersion }),
41
+ ...(options.toolkitVersion === undefined
42
+ ? {}
43
+ : { toolkitVersion: options.toolkitVersion }),
41
44
  ...(options.hookRuntimePath === undefined
42
45
  ? {}
43
46
  : { hookRuntimePath: options.hookRuntimePath }),