@kylecheng3146/agent-ops 0.1.15 → 0.1.17

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 CHANGED
@@ -287,14 +287,16 @@ task criteria and require current verification evidence. The detailed report
287
287
  is displayed; a minimal source-fingerprint attestation is persisted after PASS.
288
288
 
289
289
  Each attempt uses a fresh temporary cwd, a small allowlisted environment, and a
290
- target-native read-only/context-isolation mode. Currently only Claude safe mode
291
- meets the full isolation contract; configured Codex and Agy entries return
292
- `capability-unavailable` rather than run with a weaker boundary. `opencode` is
293
- not a review target.
294
-
295
- The first target that actually runs produces the verdict. A `FAIL` is final:
296
- the chain never retries elsewhere after a real verdict. `--yes` is still
297
- required for every run, since each run spends another provider's quota.
290
+ target-native read-only mode. Claude additionally uses its complete safe mode;
291
+ Codex ignores user config and persistence, while Agy runs in sandboxed plan
292
+ mode against a disposable repository clone. Codex and Agy preserve their
293
+ existing login environment, so their context isolation is intentionally weaker
294
+ than Claude's. `opencode` is not a review target.
295
+
296
+ The first valid `PASS` or `FAIL` is final. Attempts that produce no verdict —
297
+ including login failures and unparseable output advance to the next target
298
+ and remain visible in the result's `attempts`. `--yes` is still required for
299
+ every run, since each run spends another provider's quota.
298
300
 
299
301
  Authentication is diagnosed, never guessed:
300
302
 
@@ -63,6 +63,7 @@ export function parseArgs(argv) {
63
63
  let harness;
64
64
  const hookTargets = [];
65
65
  let taskId;
66
+ let parentTaskId;
66
67
  let targetVersion;
67
68
  let title;
68
69
  let sessionId;
@@ -146,6 +147,14 @@ export function parseArgs(argv) {
146
147
  index += 1;
147
148
  break;
148
149
  }
150
+ case "--parent": {
151
+ if (parentTaskId !== undefined) {
152
+ duplicate(token);
153
+ }
154
+ parentTaskId = readOptionValue(argv, index, token);
155
+ index += 1;
156
+ break;
157
+ }
149
158
  case "--target-version": {
150
159
  if (targetVersion !== undefined) {
151
160
  duplicate(token);
@@ -276,6 +285,7 @@ export function parseArgs(argv) {
276
285
  hookTargets.length > 0 ||
277
286
  profiles.length > 0 ||
278
287
  taskId !== undefined ||
288
+ parentTaskId !== undefined ||
279
289
  targetVersion !== undefined ||
280
290
  title !== undefined ||
281
291
  criteria.length > 0 ||
@@ -310,6 +320,9 @@ export function parseArgs(argv) {
310
320
  (hasTaskTargetOptions || hasTaskMutationOptions)) {
311
321
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Task target options may be used only with task or verify.");
312
322
  }
323
+ if (command !== "task" && parentTaskId !== undefined) {
324
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--parent may be used only with task create or task status.");
325
+ }
313
326
  if (command !== "update" && targetVersion !== undefined) {
314
327
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--target-version may be used only with update.");
315
328
  }
@@ -342,20 +355,26 @@ export function parseArgs(argv) {
342
355
  (title !== undefined ||
343
356
  criteria.length > 0 ||
344
357
  evidence.length > 0 ||
358
+ // --parent filters the listing, so it cannot name a single target.
359
+ (parentTaskId !== undefined &&
360
+ (taskId !== undefined || sessionId !== undefined)) ||
345
361
  (taskId !== undefined && sessionId !== undefined))) ||
346
362
  (action === "attach" &&
347
363
  (title !== undefined ||
348
364
  criteria.length > 0 ||
349
- evidence.length > 0)) ||
365
+ evidence.length > 0 ||
366
+ parentTaskId !== undefined)) ||
350
367
  (action === "complete" &&
351
368
  (title !== undefined ||
352
369
  criteria.length > 0 ||
353
- sessionId !== undefined)) ||
370
+ sessionId !== undefined ||
371
+ parentTaskId !== undefined)) ||
354
372
  ((action === "archive" || action === "export") &&
355
373
  (title !== undefined ||
356
374
  criteria.length > 0 ||
357
375
  evidence.length > 0 ||
358
- sessionId !== undefined));
376
+ sessionId !== undefined ||
377
+ parentTaskId !== undefined));
359
378
  if (taskOptionInvalid) {
360
379
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", `Unsupported option for task ${action}.`);
361
380
  }
@@ -392,6 +411,7 @@ export function parseArgs(argv) {
392
411
  ...(hookTargets.length === 0 ? {} : { hookTargets }),
393
412
  profiles,
394
413
  ...(taskId === undefined ? {} : { taskId }),
414
+ ...(parentTaskId === undefined ? {} : { parentTaskId }),
395
415
  ...(targetVersion === undefined ? {} : { targetVersion }),
396
416
  ...(title === undefined ? {} : { title }),
397
417
  ...(reviewTargets.length === 0 ? {} : { reviewTargets }),
@@ -38,6 +38,8 @@ Options:
38
38
  --check-auth Doctor only: probe each review target's
39
39
  authentication with one real call
40
40
  --task <id>
41
+ --parent <task-id> Task create: record a subtask of this
42
+ task; task status: list its subtasks
41
43
  --target-version <version> Update target version (offline-capable)
42
44
  --title <text>
43
45
  --criterion <json> Repeatable
@@ -217,6 +217,7 @@ function sourceChangedResult(result) {
217
217
  ...(result.independence === undefined
218
218
  ? {}
219
219
  : { independence: result.independence }),
220
+ ...(result.attempts === undefined ? {} : { attempts: result.attempts }),
220
221
  ...(result.verification === undefined
221
222
  ? {}
222
223
  : { verification: result.verification })
@@ -357,7 +358,10 @@ export async function runReviewCommand(options) {
357
358
  });
358
359
  }
359
360
  try {
360
- await assertSafeSupportingPaths(options.root, result.report.supportingFilesInspected);
361
+ await assertSafeSupportingPaths(options.root, [
362
+ ...result.report.supportingFilesInspected,
363
+ ...(result.adversarial?.report.supportingFilesInspected ?? [])
364
+ ]);
361
365
  }
362
366
  catch (error) {
363
367
  if (scopeReason(error) === "unsafe-review-path") {
@@ -92,16 +92,22 @@ export async function runTaskCommand(options) {
92
92
  criteria: (options.args.criteria ?? []).map(parseCriterion),
93
93
  ...(options.policyConfigHash === undefined
94
94
  ? {}
95
- : { policyConfigHash: options.policyConfigHash })
95
+ : { policyConfigHash: options.policyConfigHash }),
96
+ ...(options.args.parentTaskId === undefined
97
+ ? {}
98
+ : { parentTaskId: options.args.parentTaskId })
96
99
  });
97
100
  return taskEnvelope(action, "TASK_CREATED", `Created task ${record.task.id}.`, record);
98
101
  }
99
102
  if (action === "status") {
100
103
  if (options.args.taskId === undefined && sessionId === undefined) {
101
- const records = await options.service.list();
104
+ const parentTaskId = options.args.parentTaskId;
105
+ const records = await options.service.list(parentTaskId === undefined ? {} : { parentTaskId });
102
106
  return okEnvelope("TASK_LISTED", {
103
107
  action,
104
- message: `Listed ${records.length} task(s).`,
108
+ message: parentTaskId === undefined
109
+ ? `Listed ${records.length} task(s).`
110
+ : `Listed ${records.length} subtask(s) of ${parentTaskId}.`,
105
111
  records,
106
112
  text: renderTaskList(records)
107
113
  });
@@ -1,18 +1,18 @@
1
1
  import { CliArgumentError } from "./args.js";
2
- import { DEFAULT_REVIEW_TARGETS } from "../../../runtime/src/review/roles.js";
2
+ import { DEFAULT_REVIEW_TARGETS, REVIEW_TARGET_ORDER } from "../../../runtime/src/review/roles.js";
3
3
  import { HARNESS_IDS, resolveHarnessSelection } from "../../../runtime/src/install/harness.js";
4
4
  import { selectOption, selectOptions } from "./ui.js";
5
5
  const SCOPES = new Set(["project", "user"]);
6
6
  const PROFILES = new Set(["advisory", "core", "guardrails", "loop"]);
7
7
  const DEFAULT_HARNESS = [];
8
- const REVIEW_TARGET_SET = new Set(DEFAULT_REVIEW_TARGETS);
8
+ const REVIEW_TARGET_SET = new Set(REVIEW_TARGET_ORDER);
9
9
  const REVIEW_TARGET_CHOICES = DEFAULT_REVIEW_TARGETS.map((id) => ({
10
10
  label: id,
11
11
  value: id,
12
12
  description: id === "codex"
13
- ? "Retained for future isolation support; currently not auto-run."
13
+ ? "Runs read-only from its own credential home."
14
14
  : id === "agy"
15
- ? "Retained for future isolation support; currently not auto-run."
15
+ ? "Runs in sandboxed plan mode against a disposable clone."
16
16
  : "Runs in fresh safe mode with context isolation."
17
17
  }));
18
18
  function selectReviewTargets(raw) {
@@ -26,7 +26,7 @@ function selectReviewTargets(raw) {
26
26
  }
27
27
  }
28
28
  // Declared order wins: the chain order is the option list, not click order.
29
- return DEFAULT_REVIEW_TARGETS.filter((target) => values.includes(target));
29
+ return REVIEW_TARGET_ORDER.filter((target) => values.includes(target));
30
30
  }
31
31
  function affirmative(raw) {
32
32
  return /^(y|yes)$/i.test(raw.trim());
@@ -370,21 +370,35 @@ async function checkReviewTargets(config, probe, checkAuth) {
370
370
  return check("review-targets", "PASS", `External review targets: ${targets.join(", ")}. ` +
371
371
  "Login state unverified; run: agent-ops doctor --check-auth");
372
372
  }
373
+ // Every target is probed before reporting: an ineligible entry is only
374
+ // DEGRADED, so returning at the first one would hide a genuinely broken
375
+ // eligible target behind it and make the verdict depend on ordering.
376
+ const failures = [];
377
+ const degraded = [];
373
378
  for (const target of targets) {
374
379
  const result = await probe(target, checkAuth);
375
380
  if (result === "missing-executable") {
376
- return check("review-targets", "FAIL", `${target} not found.`, undefined, `Install ${target}, or remove "${target}" from reviewRoles[].targets.`);
381
+ failures.push(check("review-targets", "FAIL", `${target} not found.`, undefined, `Install ${target}, or remove "${target}" from reviewRoles[].targets.`));
382
+ continue;
377
383
  }
378
384
  if (result === "ineligible") {
379
- return check("review-targets", "FAIL", `${target} has no read-only mode and cannot review.`, undefined, `Remove "${target}" from reviewRoles[].targets.`);
385
+ // Not a broken installation: review simply skips this target. A FAIL here
386
+ // would light up every config that merely still names one.
387
+ degraded.push(check("review-targets", "DEGRADED", `${target} has no read-only mode and is skipped when reviewing.`, undefined, `Remove "${target}" from reviewRoles[].targets.`));
388
+ continue;
380
389
  }
381
390
  if (result === "timeout") {
382
- return check("review-targets", "FAIL", `${target} did not answer in time.`, undefined, "Re-run: agent-ops doctor --check-auth");
391
+ failures.push(check("review-targets", "FAIL", `${target} did not answer in time.`, undefined, "Re-run: agent-ops doctor --check-auth"));
392
+ continue;
383
393
  }
384
394
  if (checkAuth && result !== "ok") {
385
- return check("review-targets", "FAIL", `${target} is installed but not authenticated, or it rejected the call.`, undefined, `Run: ${target} login`);
395
+ failures.push(check("review-targets", "FAIL", `${target} is installed but not authenticated, or it rejected the call.`, undefined, `Run: ${target} login`));
386
396
  }
387
397
  }
398
+ const worst = failures[0] ?? degraded[0];
399
+ if (worst !== undefined) {
400
+ return worst;
401
+ }
388
402
  return check("review-targets", "PASS", checkAuth
389
403
  ? `External review targets authenticated: ${targets.join(", ")}.`
390
404
  : `External review targets: ${targets.join(", ")}. ` +
@@ -244,7 +244,10 @@ export function managedRules(descriptor, context) {
244
244
  ""
245
245
  ];
246
246
  if (context.capabilities.includes("rules")) {
247
- lines.push("For every change:", "", "1. Define two to five mechanically verifiable acceptance criteria.", "2. Inspect the smallest relevant scope and preserve unrelated changes.", "3. Apply the smallest safe change.", "4. Run evidence-producing verification for every criterion.", "5. Obtain independent review before claiming completion, via", " `agent-ops review` (or the CLI's equivalent invocation). Never call a", " review-target CLI (agy, codex, claude) directly — direct calls skip", " the enforced read-only sandbox flags and can hang or fail on command", " permission prompts.", "", "Treat `.agent-ops/config.json` as verifier authority. Discovery output is", "only a proposal until a user confirms it. Repository commands require an", "exact matching trust record. Confirmed project init/update grants it", "automatically when verification commands are configured.", "");
247
+ lines.push("For every change:", "", "1. Define two to five mechanically verifiable acceptance criteria.", "2. Inspect the smallest relevant scope and preserve unrelated changes.", "3. Apply the smallest safe change.", "4. Run evidence-producing verification for every criterion.", "5. Obtain independent review before claiming completion, via", " `agent-ops review --yes` (or the CLI's equivalent invocation). Never call a", " review-target CLI (agy, codex, claude) directly — direct calls skip", " the enforced read-only sandbox flags and can hang or fail on command", " permission prompts.", "", "Treat `.agent-ops/config.json` as verifier authority. Discovery output is", "only a proposal until a user confirms it. Repository commands require an", "exact matching trust record. Confirmed project init/update grants it", "automatically when verification commands are configured.", "");
248
+ }
249
+ if (context.capabilities.includes("task")) {
250
+ lines.push("Split a change that exceeds five acceptance criteria into subtasks:", "`agent-ops task create --parent <task-id>` records one, and", "`agent-ops task status --parent <task-id>` lists them. Each subtask", "carries its own criteria, verification, and independent review;", "completing one never completes its parent.", "");
248
251
  }
249
252
  if (context.capabilities.includes("lifecycle-summary")) {
250
253
  lines.push("Advisory lifecycle summaries and local logs are informational. Advisory", "failures must remain fail-open and cannot become verification evidence.", "");