@kylecheng3146/agent-ops 0.1.6 → 0.1.8

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 (43) hide show
  1. package/README.md +27 -0
  2. package/dist/packages/cli/src/args.js +47 -0
  3. package/dist/packages/cli/src/bin.js +74 -25
  4. package/dist/packages/cli/src/cli.js +13 -1
  5. package/dist/packages/cli/src/commands/init.js +4 -1
  6. package/dist/packages/cli/src/commands/review.js +371 -27
  7. package/dist/packages/cli/src/commands/task.js +4 -1
  8. package/dist/packages/cli/src/commands/verify.js +13 -1
  9. package/dist/packages/cli/src/version.js +1 -1
  10. package/dist/packages/cli/src/wizard.js +62 -3
  11. package/dist/runtime/src/config/merge.js +17 -2
  12. package/dist/runtime/src/contracts.js +1 -1
  13. package/dist/runtime/src/install/doctor.js +42 -1
  14. package/dist/runtime/src/install/plan.js +11 -5
  15. package/dist/runtime/src/review/execute.js +180 -0
  16. package/dist/runtime/src/review/extract.js +69 -0
  17. package/dist/runtime/src/review/invocation.js +116 -0
  18. package/dist/runtime/src/review/packet.js +42 -5
  19. package/dist/runtime/src/review/probe.js +72 -0
  20. package/dist/runtime/src/review/render.js +62 -0
  21. package/dist/runtime/src/review/report.js +183 -0
  22. package/dist/runtime/src/review/result.js +2 -2
  23. package/dist/runtime/src/review/roles.js +35 -0
  24. package/dist/runtime/src/review/runner.js +98 -12
  25. package/dist/runtime/src/review/scope.js +123 -0
  26. package/dist/runtime/src/schema/validate.js +80 -0
  27. package/dist/runtime/src/task/service.js +46 -1
  28. package/dist/runtime/src/task/store.js +16 -4
  29. package/dist/runtime/src/verify/change-surface.js +38 -2
  30. package/dist/runtime/src/verify/command-executor.js +4 -1
  31. package/dist/runtime/src/verify/evidence.js +36 -0
  32. package/dist/runtime/src/verify/scope.js +1 -2
  33. package/dist/runtime/src/verify/service.js +66 -9
  34. package/dist/runtime/src/verify/source-fingerprint.js +49 -0
  35. package/dist/runtime/src/verify/spawn.js +9 -3
  36. package/docs/en/guides/configuration.md +68 -0
  37. package/docs/en/spec/review.md +37 -4
  38. package/docs/zh-TW/guides/configuration.md +60 -0
  39. package/docs/zh-TW/spec/review.md +33 -3
  40. package/package.json +1 -1
  41. package/schemas/config.schema.json +29 -0
  42. package/schemas/evidence.schema.json +16 -1
  43. package/schemas/review-report.schema.json +48 -0
package/README.md CHANGED
@@ -261,6 +261,33 @@ For a full command reference, run `agent-ops --help`. The `task`, `verify`, and
261
261
  `review` commands support acceptance tracking and independent verification when
262
262
  the project configuration defines those workflows.
263
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). Reviews require an attached task, current required
269
+ verification evidence, and a deterministic worktree (or `--base`) scope. The
270
+ native-schema detailed report is displayed but not persisted.
271
+
272
+ Each attempt uses a fresh temporary cwd, a small allowlisted environment, and a
273
+ target-native read-only/context-isolation mode. Currently only Claude safe mode
274
+ meets the full isolation contract; configured Codex and Agy entries return
275
+ `capability-unavailable` rather than run with a weaker boundary. `opencode` is
276
+ not a review target.
277
+
278
+ The first target that actually runs produces the verdict. A `FAIL` is final:
279
+ the chain never retries elsewhere after a real verdict. `--yes` is still
280
+ required for every run, since each run spends another provider's quota.
281
+
282
+ Authentication is diagnosed, never guessed:
283
+
284
+ ```bash
285
+ agent-ops doctor # presence only: no tokens, no network
286
+ agent-ops doctor --check-auth # one real print call per configured target
287
+ ```
288
+
289
+ See [Configuration](docs/en/guides/configuration.md) for the full contract.
290
+
264
291
  ## Project principles
265
292
 
266
293
  - Define verifiable success before making changes.
@@ -13,6 +13,8 @@ export const COMMAND_NAMES = [
13
13
  const COMMAND_SET = new Set(COMMAND_NAMES);
14
14
  const SCOPES = new Set(["project", "user"]);
15
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;
@@ -64,9 +66,12 @@ export function parseArgs(argv) {
64
66
  let targetVersion;
65
67
  let title;
66
68
  let sessionId;
69
+ let base;
67
70
  const profiles = [];
71
+ const reviewTargets = [];
68
72
  const criteria = [];
69
73
  const evidence = [];
74
+ let checkAuth = false;
70
75
  let dryRun = false;
71
76
  let json = false;
72
77
  let yes = false;
@@ -121,6 +126,18 @@ export function parseArgs(argv) {
121
126
  index += 1;
122
127
  break;
123
128
  }
129
+ case "--review-target": {
130
+ const value = readOptionValue(argv, index, token);
131
+ if (!REVIEW_TARGETS.has(value)) {
132
+ invalidValue(token, value);
133
+ }
134
+ if (reviewTargets.includes(value)) {
135
+ duplicate(`${token} ${value}`);
136
+ }
137
+ reviewTargets.push(value);
138
+ index += 1;
139
+ break;
140
+ }
124
141
  case "--task": {
125
142
  if (taskId !== undefined) {
126
143
  duplicate(token);
@@ -163,6 +180,20 @@ export function parseArgs(argv) {
163
180
  index += 1;
164
181
  break;
165
182
  }
183
+ case "--base": {
184
+ if (base !== undefined) {
185
+ duplicate(token);
186
+ }
187
+ base = readOptionValue(argv, index, token);
188
+ index += 1;
189
+ break;
190
+ }
191
+ case "--check-auth":
192
+ if (checkAuth) {
193
+ duplicate(token);
194
+ }
195
+ checkAuth = true;
196
+ break;
166
197
  case "--dry-run":
167
198
  if (dryRun) {
168
199
  duplicate(token);
@@ -249,7 +280,10 @@ export function parseArgs(argv) {
249
280
  title !== undefined ||
250
281
  criteria.length > 0 ||
251
282
  evidence.length > 0 ||
283
+ reviewTargets.length > 0 ||
252
284
  sessionId !== undefined ||
285
+ base !== undefined ||
286
+ checkAuth ||
253
287
  dryRun ||
254
288
  yes) {
255
289
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Only --json may be combined with global help or version.");
@@ -279,11 +313,20 @@ export function parseArgs(argv) {
279
313
  if (command !== "update" && targetVersion !== undefined) {
280
314
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--target-version may be used only with update.");
281
315
  }
316
+ if (base !== undefined && command !== "verify" && command !== "review") {
317
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--base may be used only with verify or review.");
318
+ }
282
319
  if (hookTargets.length > 0 &&
283
320
  command !== "init" &&
284
321
  command !== "update") {
285
322
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--hook-target may be used only with init or update.");
286
323
  }
324
+ if (checkAuth && command !== "doctor") {
325
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--check-auth may be used only with doctor.");
326
+ }
327
+ if (reviewTargets.length > 0 && command !== "init") {
328
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--review-target may be used only with init.");
329
+ }
287
330
  if (command === "task") {
288
331
  if (harness !== undefined ||
289
332
  profiles.length > 0 ||
@@ -325,6 +368,7 @@ export function parseArgs(argv) {
325
368
  evidence.length > 0 ||
326
369
  dryRun ||
327
370
  yes ||
371
+ base !== undefined ||
328
372
  (taskId !== undefined && sessionId !== undefined))) {
329
373
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Verify accepts only scope, task or session, and json options.");
330
374
  }
@@ -345,9 +389,12 @@ export function parseArgs(argv) {
345
389
  ...(taskId === undefined ? {} : { taskId }),
346
390
  ...(targetVersion === undefined ? {} : { targetVersion }),
347
391
  ...(title === undefined ? {} : { title }),
392
+ ...(reviewTargets.length === 0 ? {} : { reviewTargets }),
348
393
  ...(criteria.length === 0 ? {} : { criteria }),
349
394
  ...(evidence.length === 0 ? {} : { evidence }),
350
395
  ...(sessionId === undefined ? {} : { sessionId }),
396
+ ...(base === undefined ? {} : { base }),
397
+ ...(checkAuth ? { checkAuth } : {}),
351
398
  dryRun,
352
399
  json,
353
400
  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";
@@ -67,6 +70,29 @@ async function installedManifest(root) {
67
70
  async function installedHarness(root) {
68
71
  return (await installedManifest(root))?.harness ?? [...HARNESS_IDS];
69
72
  }
73
+ function gitRunner(root) {
74
+ return {
75
+ run: async (gitArgs) => {
76
+ try {
77
+ return {
78
+ exitCode: 0,
79
+ stdout: execFileSync("git", [...gitArgs], {
80
+ cwd: root,
81
+ encoding: "buffer",
82
+ stdio: ["ignore", "pipe", "ignore"]
83
+ })
84
+ };
85
+ }
86
+ catch (error) {
87
+ const failure = error;
88
+ return {
89
+ exitCode: failure.status ?? 1,
90
+ stdout: failure.stdout ?? new Uint8Array()
91
+ };
92
+ }
93
+ }
94
+ };
95
+ }
70
96
  async function confirmInit(plan) {
71
97
  writeBanner({
72
98
  isTTY: process.stdout.isTTY === true,
@@ -139,8 +165,12 @@ else {
139
165
  sources: await hookSources(root, args.scope === "user" ? "user" : "project")
140
166
  }),
141
167
  repositoryTrust: async () => repositoryTrustStatus(await repositoryTrust(root, config, CLI_VERSION)),
142
- smokeAvailability: () => smokeAvailabilityStatus(config)
143
- }
168
+ smokeAvailability: () => smokeAvailabilityStatus(config),
169
+ reviewTarget: async (target, deep) => await probeReviewTarget(target, { cwd: root, deep })
170
+ },
171
+ ...(args.checkAuth === true
172
+ ? { checkReviewTargetAuth: true }
173
+ : {})
144
174
  });
145
175
  }
146
176
  if (args.command === "uninstall") {
@@ -172,16 +202,54 @@ else {
172
202
  const taskService = new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root));
173
203
  if (args.command === "task") {
174
204
  const sessionId = process.env.AGENT_OPS_SESSION_ID;
205
+ const policyConfigHash = args.action === "create"
206
+ ? calculateConfigHash((await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config)
207
+ : undefined;
175
208
  return await runTaskCommand({
176
209
  args,
177
210
  service: taskService,
211
+ ...(policyConfigHash === undefined ? {} : { policyConfigHash }),
178
212
  ...(sessionId === undefined ? {} : { sessionId })
179
213
  });
180
214
  }
181
215
  if (args.command === "review") {
216
+ const reviewSessionId = process.env.AGENT_OPS_SESSION_ID;
217
+ const reviewConfig = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
218
+ const reviewRole = resolveReviewRole("independent-review", reviewConfig.reviewRoles ?? []);
182
219
  return await runReviewCommand({
183
220
  args,
184
- authorized: args.yes
221
+ authorized: args.yes,
222
+ tasks: taskService,
223
+ ...(reviewSessionId === undefined
224
+ ? {}
225
+ : { sessionId: reviewSessionId }),
226
+ ...(reviewConfig.reviewRoles === undefined
227
+ ? {}
228
+ : { roles: reviewConfig.reviewRoles }),
229
+ root,
230
+ gitRunner: gitRunner(root),
231
+ policyConfigHash: calculateConfigHash(reviewConfig),
232
+ currentPolicyConfigHash: async () => calculateConfigHash((await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config),
233
+ config: reviewConfig,
234
+ evidenceStore: new FileEvidenceStore(root, root),
235
+ execute: createReviewExecutor({
236
+ targets: reviewRole?.targets ?? [],
237
+ cwd: root,
238
+ ...(reviewRole?.model === undefined
239
+ ? {}
240
+ : { model: reviewRole.model }),
241
+ ...(reviewRole?.effort === undefined
242
+ ? {}
243
+ : { effort: reviewRole.effort }),
244
+ ...(reviewRole?.timeoutMs === undefined
245
+ ? {}
246
+ : { timeoutMs: reviewRole.timeoutMs }),
247
+ onProgress: (line) => {
248
+ if (!args.json) {
249
+ process.stderr.write(`${line}\n`);
250
+ }
251
+ }
252
+ })
185
253
  });
186
254
  }
187
255
  if (args.command === "config") {
@@ -198,31 +266,12 @@ else {
198
266
  root,
199
267
  scope: args.scope === "user" ? "user" : "project",
200
268
  config,
201
- gitRunner: {
202
- run: async (gitArgs) => {
203
- try {
204
- return {
205
- exitCode: 0,
206
- stdout: execFileSync("git", [...gitArgs], {
207
- cwd: root,
208
- encoding: "buffer",
209
- stdio: ["ignore", "pipe", "ignore"]
210
- })
211
- };
212
- }
213
- catch (error) {
214
- const failure = error;
215
- return {
216
- exitCode: failure.status ?? 1,
217
- stdout: failure.stdout ?? new Uint8Array()
218
- };
219
- }
220
- }
221
- },
269
+ gitRunner: gitRunner(root),
222
270
  processRunner: new NodeVerificationProcessRunner(),
223
271
  taskService,
224
272
  evidenceStore: new FileEvidenceStore(root, root),
225
- trusted: trustStatus === "TRUSTED"
273
+ trusted: trustStatus === "TRUSTED",
274
+ ...(args.base === undefined ? {} : { base: args.base })
226
275
  })
227
276
  });
228
277
  }
@@ -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" : "";
@@ -32,12 +33,17 @@ Options:
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
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>
38
43
  --criterion <json> Repeatable
39
44
  --evidence <criterion-id=reference> Repeatable
40
45
  --session <id>
46
+ --base <git-ref> Verify/review a clean committed range
41
47
  --dry-run
42
48
  --json
43
49
  --yes
@@ -76,7 +82,13 @@ export async function runCli(argv, io, services) {
76
82
  }
77
83
  try {
78
84
  if (args.command === "init") {
79
- args = await completeInitChoices(args, args.json ? { ...io, isTTY: false } : io);
85
+ args = await completeInitChoices(args, args.json ? { ...io, isTTY: false } : io, {
86
+ probeReviewTarget: async (target) => (await probeReviewTarget(target, {
87
+ cwd: process.cwd(),
88
+ deep: true
89
+ })) === "ok",
90
+ warn: (message) => io.writeStderr(`${message}\n`)
91
+ });
80
92
  }
81
93
  const execute = args.command === "help" || args.command === "version"
82
94
  ? services.execute
@@ -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", {