@kylecheng3146/agent-ops 0.1.4 → 0.1.5

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 (78) hide show
  1. package/README.md +88 -13
  2. package/dist/packages/cli/src/args.js +39 -6
  3. package/dist/packages/cli/src/bin.js +33 -11
  4. package/dist/packages/cli/src/cli.js +6 -1
  5. package/dist/packages/cli/src/commands/doctor.js +31 -9
  6. package/dist/packages/cli/src/commands/hook.js +18 -16
  7. package/dist/packages/cli/src/commands/init.js +9 -5
  8. package/dist/packages/cli/src/commands/review.js +5 -1
  9. package/dist/packages/cli/src/commands/uninstall.js +6 -5
  10. package/dist/packages/cli/src/commands/update.js +13 -5
  11. package/dist/packages/cli/src/context.js +9 -3
  12. package/dist/packages/cli/src/hook-process.js +109 -7
  13. package/dist/packages/cli/src/plan-output.js +9 -4
  14. package/dist/packages/cli/src/public-plan.js +62 -0
  15. package/dist/packages/cli/src/version.js +1 -1
  16. package/dist/packages/cli/src/wizard.js +27 -11
  17. package/dist/runtime/src/adapters/claude/config.js +0 -8
  18. package/dist/runtime/src/adapters/claude/events.js +26 -0
  19. package/dist/runtime/src/adapters/claude/output.js +6 -6
  20. package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
  21. package/dist/runtime/src/adapters/codex/config.js +29 -11
  22. package/dist/runtime/src/adapters/codex/events.js +26 -0
  23. package/dist/runtime/src/adapters/codex/output.js +10 -0
  24. package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
  25. package/dist/runtime/src/adapters/opencode/config.js +170 -0
  26. package/dist/runtime/src/adapters/opencode/events.js +49 -0
  27. package/dist/runtime/src/adapters/opencode/input.js +32 -0
  28. package/dist/runtime/src/adapters/opencode/output.js +23 -0
  29. package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
  30. package/dist/runtime/src/config/explain.js +7 -0
  31. package/dist/runtime/src/config/hash.js +24 -0
  32. package/dist/runtime/src/config/merge.js +6 -2
  33. package/dist/runtime/src/config/migrate.js +19 -4
  34. package/dist/runtime/src/contracts.js +10 -1
  35. package/dist/runtime/src/fs/manifest.js +32 -1
  36. package/dist/runtime/src/fs/transaction.js +14 -2
  37. package/dist/runtime/src/hooks/advisory.js +16 -0
  38. package/dist/runtime/src/hooks/stop-service.js +70 -0
  39. package/dist/runtime/src/hooks/stop-verify.js +4 -1
  40. package/dist/runtime/src/install/doctor.js +119 -9
  41. package/dist/runtime/src/install/harness.js +296 -35
  42. package/dist/runtime/src/install/hooks.js +22 -17
  43. package/dist/runtime/src/install/ownership.js +110 -34
  44. package/dist/runtime/src/install/plan.js +207 -28
  45. package/dist/runtime/src/install/probes.js +9 -43
  46. package/dist/runtime/src/install/profiles.js +8 -1
  47. package/dist/runtime/src/install/surface-inspection.js +296 -0
  48. package/dist/runtime/src/install/surfaces.js +11 -0
  49. package/dist/runtime/src/install/uninstall.js +10 -3
  50. package/dist/runtime/src/install/update.js +8 -2
  51. package/dist/runtime/src/schema/validate.js +37 -18
  52. package/dist/runtime/src/task/service.js +3 -3
  53. package/dist/runtime/src/task/store.js +12 -3
  54. package/dist/runtime/src/verify/command-executor.js +113 -0
  55. package/dist/runtime/src/verify/evidence.js +4 -24
  56. package/dist/runtime/src/verify/service.js +20 -92
  57. package/dist/runtime/src/verify/spawn.js +6 -1
  58. package/docs/en/guides/configuration.md +83 -0
  59. package/docs/en/guides/quickstart.md +9 -0
  60. package/docs/en/guides/security.md +5 -0
  61. package/docs/en/spec/README.md +9 -0
  62. package/docs/en/spec/harness-adapters.md +66 -2
  63. package/docs/en/spec/maintenance.md +11 -0
  64. package/docs/en/spec/review.md +11 -0
  65. package/docs/zh-TW/guides/configuration.md +77 -0
  66. package/docs/zh-TW/guides/quickstart.md +9 -0
  67. package/docs/zh-TW/guides/security.md +5 -0
  68. package/docs/zh-TW/spec/README.md +9 -0
  69. package/docs/zh-TW/spec/harness-adapters.md +57 -3
  70. package/docs/zh-TW/spec/maintenance.md +11 -1
  71. package/docs/zh-TW/spec/review.md +10 -0
  72. package/package.json +4 -2
  73. package/schemas/config.schema.json +55 -1
  74. package/schemas/manifest.schema.json +7 -2
  75. package/templates/common/AGENTS.block.md +2 -1
  76. package/templates/common/CLAUDE.block.md +2 -1
  77. package/dist/runtime/src/review/claude-runner.js +0 -4
  78. package/dist/runtime/src/review/codex-runner.js +0 -4
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Loop Engineering Toolkit
2
2
 
3
3
  Loop Engineering Toolkit is an evidence-driven development-loop toolkit for
4
- Codex and Claude Code. It is designed to turn acceptance criteria, explicit
4
+ Codex, Claude Code, and opencode. It is designed to turn acceptance criteria, explicit
5
5
  verification, safe lifecycle hooks, and independent review into a repeatable
6
6
  engineering workflow.
7
7
 
@@ -33,17 +33,24 @@ Use `--help` for the complete command reference or provide explicit options in
33
33
  automation. The wizard never writes files until you review and confirm its
34
34
  installation plan.
35
35
 
36
+ The interactive multi-select screens start with no harness or profile selected.
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
39
+ scripted use, `--harness all` selects all three harnesses; comma-separated
40
+ selections such as `codex,opencode` are also supported. The legacy `both` value
41
+ remains an alias for `codex,claude`.
42
+
36
43
  Preview a project installation before changing files:
37
44
 
38
45
  ```bash
39
46
  agent-ops init \
40
- --dry-run --scope project --harness both --profile core --json
47
+ --dry-run --scope project --harness all --profile core --json
41
48
  ```
42
49
 
43
50
  After reviewing the plan, apply it explicitly with `--yes`:
44
51
 
45
52
  ```bash
46
- agent-ops init --scope project --harness both --profile core --yes
53
+ agent-ops init --scope project --harness all --profile core --yes
47
54
  ```
48
55
 
49
56
  The remaining day-to-day checks are:
@@ -60,7 +67,16 @@ agent-ops uninstall --dry-run --json
60
67
  Use `--scope user` with user-home installations. Keep `--dry-run` for any
61
68
  operation you want to inspect before applying; non-interactive automation should
62
69
  pass `--yes` only after reviewing the plan. Add `--json` when another tool will
63
- consume the result.
70
+ consume the result. `update` operates on an existing managed installation. Pass
71
+ `--target-version <version>` when the target must be explicit or the command
72
+ must work without a registry lookup, for example:
73
+
74
+ ```bash
75
+ agent-ops update \
76
+ --harness opencode \
77
+ --target-version 0.1.4 \
78
+ --dry-run --json
79
+ ```
64
80
 
65
81
  ## Quick start from a source checkout
66
82
 
@@ -79,14 +95,14 @@ Preview a project installation before changing files:
79
95
 
80
96
  ```bash
81
97
  node dist/packages/cli/src/bin.js init \
82
- --dry-run --scope project --harness both --profile core --json
98
+ --dry-run --scope project --harness all --profile core --json
83
99
  ```
84
100
 
85
101
  After reviewing the plan, apply it explicitly with `--yes`. Trust, diagnostics,
86
102
  updates, and removal are separate commands:
87
103
 
88
104
  ```bash
89
- node dist/packages/cli/src/bin.js init --scope project --harness both --profile core --yes
105
+ node dist/packages/cli/src/bin.js init --scope project --harness all --profile core --yes
90
106
  node dist/packages/cli/src/bin.js trust status --json
91
107
  node dist/packages/cli/src/bin.js doctor --json
92
108
  node dist/packages/cli/src/bin.js config explain --json
@@ -94,17 +110,76 @@ node dist/packages/cli/src/bin.js update --dry-run --json
94
110
  node dist/packages/cli/src/bin.js uninstall --dry-run --json
95
111
  ```
96
112
 
113
+ The `dist/...` path is relative to the source checkout. When testing from a
114
+ throwaway project, run the built CLI with its absolute checkout path (or use the
115
+ published `agent-ops` command); a new project does not contain its own `dist/`
116
+ directory. `update` also requires that the project already has a valid managed
117
+ `.agent-ops/manifest.json` created by `init`.
118
+
97
119
  The commands after `init --yes` are post-apply operations. `doctor` reports
98
120
  `UNKNOWN` for a probe that has nothing to verify yet: `repository-trust` until
99
121
  `trust grant` runs, and `smoke-availability` until the configuration declares a
100
122
  verification command.
101
123
 
102
- Installing the `advisory` or `guardrails` profile also registers lifecycle
103
- hooks in `.claude/settings.json` and `.codex/hooks.json`. Only agent-ops owned
104
- handlers are added, foreign settings in those files are preserved, and
105
- `uninstall` removes exactly the handlers it registered. The hooks call
106
- `agent-ops hook <harness> <event>`, which always exits 0 so a toolkit failure
107
- can never block the harness.
124
+ Installing the `advisory` or `guardrails` profile registers the lifecycle and
125
+ command-policy hooks implied by those profiles for the selected harnesses.
126
+ Claude Code and Codex use their native JSON settings files; opencode uses the agent-ops-owned
127
+ `.opencode/plugins/agent-ops.js` shim and never changes `opencode.json`. Only
128
+ agent-ops-owned handlers and artifacts are managed, foreign settings are
129
+ preserved, and `uninstall` removes exactly the content it registered. At user
130
+ scope, the opencode plugin is placed under
131
+ `.config/opencode/plugins/agent-ops.js`, or under the configured
132
+ `$XDG_CONFIG_HOME/opencode/` or `$OPENCODE_CONFIG_DIR/` when that location is
133
+ inside the managed user root. The shims call `agent-ops hook <harness> <event>`
134
+ 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.
140
+
141
+ `guardrails` enables command policy only; it does not imply Stop verification.
142
+ Stop verification is an explicit, disabled-by-default config feature. Enable it
143
+ only with confirmed commands, for example the relevant config fragment is:
144
+
145
+ ```json
146
+ {
147
+ "features": { "stopVerification": { "enabled": true } },
148
+ "verification": {
149
+ "commands": [
150
+ {
151
+ "id": "unit",
152
+ "command": "npm",
153
+ "args": ["test"],
154
+ "cwd": ".",
155
+ "required": true,
156
+ "evidence": { "kind": "test-count", "minimum": 1 }
157
+ }
158
+ ]
159
+ }
160
+ }
161
+ ```
162
+
163
+ After changing Stop configuration, run `agent-ops update` so native
164
+ registrations match the config, then `agent-ops trust grant` to bind the
165
+ current config. Stop is report-only: `PASS`, `FAIL`, and `UNKNOWN` continue
166
+ the harness, emit bounded command evidence, and never complete a task.
167
+ Config v1 migrates to config v2 with Stop disabled; migration invalidates the
168
+ old trust binding, and pre-v1 binaries cannot read the migrated config.
169
+
170
+ The generated `AGENTS.md` and `CLAUDE.md` routing blocks are supplemental: they
171
+ load the managed baseline while leaving project-specific instructions in those
172
+ files authoritative. Existing installations with the previous canonical
173
+ wording are migrated by `agent-ops update`; changed managed blocks still fail
174
+ closed.
175
+
176
+ Dry-run plans keep harness settings writes opaque: human and JSON output expose
177
+ only the expected hash, content hash, and a safe summary. Use
178
+ `--hook-target <harness>=<surface-id>` when selecting a non-default discovered
179
+ surface; project-local Claude settings are never selected implicitly. The
180
+ internal plan still retains the complete merged settings for transactional
181
+ apply. The routing migration is one-way once applied; review the release notes
182
+ before attempting a downgrade.
108
183
 
109
184
  For a full command reference, run `agent-ops --help`. The `task`, `verify`, and
110
185
  `review` commands support acceptance tracking and independent verification when
@@ -116,7 +191,7 @@ the project configuration defines those workflows.
116
191
  - Treat command output and current filesystem state as evidence.
117
192
  - Keep advisory automation separate from blocking guardrails.
118
193
  - Preserve user configuration through managed, reversible updates.
119
- - Support Codex and Claude Code without project-specific assumptions.
194
+ - Support Codex, Claude Code, and opencode without project-specific assumptions.
120
195
  - Collect no network telemetry.
121
196
 
122
197
  ## Project status
@@ -1,3 +1,4 @@
1
+ import { isHarnessId, resolveHarnessSelection } from "../../../runtime/src/install/harness.js";
1
2
  export const COMMAND_NAMES = [
2
3
  "init",
3
4
  "config",
@@ -11,7 +12,6 @@ export const COMMAND_NAMES = [
11
12
  ];
12
13
  const COMMAND_SET = new Set(COMMAND_NAMES);
13
14
  const SCOPES = new Set(["project", "user"]);
14
- const HARNESSES = new Set(["both", "claude", "codex"]);
15
15
  const PROFILES = new Set(["advisory", "core", "guardrails"]);
16
16
  export class CliArgumentError extends Error {
17
17
  code;
@@ -36,11 +36,30 @@ function duplicate(option) {
36
36
  function invalidValue(option, value) {
37
37
  throw new CliArgumentError("CLI_INVALID_VALUE", `Invalid value for ${option}: ${value}`, option);
38
38
  }
39
+ function parseHarness(option, value) {
40
+ return resolveHarnessSelection(value) ?? invalidValue(option, value);
41
+ }
42
+ function parseHookTarget(option, value) {
43
+ const separator = value.indexOf("=");
44
+ if (separator <= 0 ||
45
+ separator !== value.lastIndexOf("=") ||
46
+ separator === value.length - 1) {
47
+ return invalidValue(option, value);
48
+ }
49
+ const harness = value.slice(0, separator);
50
+ const surfaceId = value.slice(separator + 1);
51
+ if (!isHarnessId(harness) ||
52
+ !/^[a-z][a-z0-9-]{0,63}$/u.test(surfaceId)) {
53
+ return invalidValue(option, value);
54
+ }
55
+ return { harness: harness, surfaceId };
56
+ }
39
57
  export function parseArgs(argv) {
40
58
  let command;
41
59
  let action;
42
60
  let scope;
43
61
  let harness;
62
+ const hookTargets = [];
44
63
  let taskId;
45
64
  let targetVersion;
46
65
  let title;
@@ -76,10 +95,17 @@ export function parseArgs(argv) {
76
95
  duplicate(token);
77
96
  }
78
97
  const value = readOptionValue(argv, index, token);
79
- if (!HARNESSES.has(value)) {
80
- invalidValue(token, value);
98
+ harness = parseHarness(token, value);
99
+ index += 1;
100
+ break;
101
+ }
102
+ case "--hook-target": {
103
+ const value = readOptionValue(argv, index, token);
104
+ const target = parseHookTarget(token, value);
105
+ if (hookTargets.some(({ harness: id }) => id === target.harness)) {
106
+ duplicate(`${token} ${target.harness}`);
81
107
  }
82
- harness = value;
108
+ hookTargets.push(target);
83
109
  index += 1;
84
110
  break;
85
111
  }
@@ -216,6 +242,7 @@ export function parseArgs(argv) {
216
242
  if (helpSeen || versionSeen) {
217
243
  if (scope !== undefined ||
218
244
  harness !== undefined ||
245
+ hookTargets.length > 0 ||
219
246
  profiles.length > 0 ||
220
247
  taskId !== undefined ||
221
248
  targetVersion !== undefined ||
@@ -252,6 +279,11 @@ export function parseArgs(argv) {
252
279
  if (command !== "update" && targetVersion !== undefined) {
253
280
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--target-version may be used only with update.");
254
281
  }
282
+ if (hookTargets.length > 0 &&
283
+ command !== "init" &&
284
+ command !== "update") {
285
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--hook-target may be used only with init or update.");
286
+ }
255
287
  if (command === "task") {
256
288
  if (harness !== undefined ||
257
289
  profiles.length > 0 ||
@@ -300,14 +332,15 @@ export function parseArgs(argv) {
300
332
  (title !== undefined || sessionId !== undefined)) {
301
333
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Review accepts harness, criteria, evidence, scope, dry-run, json, and yes options.");
302
334
  }
303
- if (command === "review" && harness === "both") {
304
- invalidValue("--harness", harness);
335
+ if (command === "review" && harness !== undefined && harness.length !== 1) {
336
+ invalidValue("--harness", harness.join(","));
305
337
  }
306
338
  return {
307
339
  command,
308
340
  ...(action === undefined ? {} : { action }),
309
341
  ...(scope === undefined ? {} : { scope }),
310
342
  ...(harness === undefined ? {} : { harness }),
343
+ ...(hookTargets.length === 0 ? {} : { hookTargets }),
311
344
  profiles,
312
345
  ...(taskId === undefined ? {} : { taskId }),
313
346
  ...(targetVersion === undefined ? {} : { targetVersion }),
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
- import { commonHarnessAdapters } from "../../../runtime/src/install/harness.js";
7
+ import { commonHarnessAdapters, harnessHookPath, HARNESS_IDS } from "../../../runtime/src/install/harness.js";
8
8
  import { hookRegistrationSatisfied, repositoryTrustStatus, smokeAvailabilityStatus } from "../../../runtime/src/install/probes.js";
9
9
  import { parseInstallManifest } from "../../../runtime/src/fs/manifest.js";
10
10
  import { NpmRegistryClient } from "../../../runtime/src/registry/npm.js";
@@ -12,6 +12,7 @@ import { TaskService } from "../../../runtime/src/task/service.js";
12
12
  import { FileTaskStore } from "../../../runtime/src/task/store.js";
13
13
  import { FileTrustStore, calculateTrustBinding } from "../../../runtime/src/security/trust.js";
14
14
  import { localStatePaths } from "../../../runtime/src/security/permissions.js";
15
+ import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
15
16
  import { sha256 } from "../../../runtime/src/fs/hash.js";
16
17
  import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
17
18
  import { VerificationService } from "../../../runtime/src/verify/service.js";
@@ -33,23 +34,39 @@ import { runVerifyCommand } from "./commands/verify.js";
33
34
  import { formatUpdatePlan, runUpdateCommand } from "./commands/update.js";
34
35
  import { errorEnvelope } from "./output.js";
35
36
  const HOOK_RUNTIME_PATH = fileURLToPath(new URL("./hook-entry.js", import.meta.url));
36
- async function readOptionalJson(path) {
37
+ async function readOptionalText(path) {
37
38
  try {
38
- return JSON.parse(await readFile(path, "utf8"));
39
+ return await readFile(path, "utf8");
39
40
  }
40
41
  catch {
41
42
  return null;
42
43
  }
43
44
  }
44
- async function installedHarness(root) {
45
+ async function hookSources(root, scope) {
46
+ const sources = {};
47
+ const manifest = await installedManifest(root);
48
+ const recordedOpencodePluginPath = manifest?.artifacts.find(({ id }) => id === "opencode-plugin")?.path;
49
+ const recordedHookPaths = new Map((manifest?.hooks ?? []).map(({ harness, path }) => [harness, path]));
50
+ for (const id of HARNESS_IDS) {
51
+ const path = id === "opencode" && recordedOpencodePluginPath !== undefined
52
+ ? recordedOpencodePluginPath
53
+ : recordedHookPaths.get(id) ??
54
+ harnessHookPath(id, scope, root);
55
+ sources[id] = await readOptionalText(join(root, path));
56
+ }
57
+ return sources;
58
+ }
59
+ async function installedManifest(root) {
45
60
  try {
46
- return parseInstallManifest(await readFile(join(root, ".agent-ops", "manifest.json"), "utf8")).harness;
61
+ return parseInstallManifest(await readFile(join(root, ".agent-ops", "manifest.json"), "utf8"));
47
62
  }
48
63
  catch {
49
- // ponytail: no readable manifest means demand hooks for both harnesses.
50
- return "both";
64
+ return null;
51
65
  }
52
66
  }
67
+ async function installedHarness(root) {
68
+ return (await installedManifest(root))?.harness ?? [...HARNESS_IDS];
69
+ }
53
70
  async function confirmInit(plan) {
54
71
  writeBanner({
55
72
  isTTY: process.stdout.isTTY === true,
@@ -104,6 +121,9 @@ else {
104
121
  isTTY,
105
122
  toolkitVersion: CLI_VERSION,
106
123
  hookRuntimePath: HOOK_RUNTIME_PATH,
124
+ ...(args.hookTargets === undefined
125
+ ? {}
126
+ : { hookTargets: args.hookTargets }),
107
127
  confirm: async (plan) => await confirmInit(plan)
108
128
  });
109
129
  }
@@ -114,9 +134,8 @@ else {
114
134
  probes: {
115
135
  hookRegistration: async () => hookRegistrationSatisfied({
116
136
  harness: await installedHarness(root),
117
- profiles: config.profiles,
118
- claudeSettings: await readOptionalJson(join(root, ".claude", "settings.json")),
119
- codexHooks: await readOptionalJson(join(root, ".codex", "hooks.json"))
137
+ config,
138
+ sources: await hookSources(root, args.scope === "user" ? "user" : "project")
120
139
  }),
121
140
  repositoryTrust: async () => repositoryTrustStatus(await repositoryTrust(root, config, CLI_VERSION)),
122
141
  smokeAvailability: () => smokeAvailabilityStatus(config)
@@ -139,6 +158,9 @@ else {
139
158
  registry: new NpmRegistryClient(),
140
159
  isTTY,
141
160
  hookRuntimePath: HOOK_RUNTIME_PATH,
161
+ ...(args.hookTargets === undefined
162
+ ? {}
163
+ : { hookTargets: args.hookTargets }),
142
164
  confirm: async (plan) => await confirmPlan(formatUpdatePlan(plan)),
143
165
  ...(args.targetVersion === undefined
144
166
  ? {}
@@ -219,7 +241,7 @@ else {
219
241
  const binding = await calculateTrustBinding({
220
242
  repositoryPath: root,
221
243
  remoteUrl: remote,
222
- configHash: sha256(JSON.stringify(config)),
244
+ configHash: calculateConfigHash(config),
223
245
  runtimeHash: sha256(CLI_VERSION)
224
246
  });
225
247
  return await runTrustCommand({
@@ -1,4 +1,5 @@
1
1
  import { CliArgumentError, parseArgs } from "./args.js";
2
+ import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
2
3
  import { errorEnvelope, okEnvelope, writeEnvelope } from "./output.js";
3
4
  import { completeInitChoices } from "./wizard.js";
4
5
  import { BANNER } from "./ui.js";
@@ -28,7 +29,8 @@ Commands:
28
29
 
29
30
  Options:
30
31
  --scope <project|user>
31
- --harness <both|claude|codex>
32
+ --harness <all|both|claude|codex|opencode|comma-separated> Init/update
33
+ --hook-target <harness=surface-id> Repeatable advanced init/update option
32
34
  --profile <core|advisory|guardrails> Repeatable
33
35
  --task <id>
34
36
  --target-version <version> Update target version (offline-capable)
@@ -89,6 +91,9 @@ export async function runCli(argv, io, services) {
89
91
  if (error instanceof CliArgumentError) {
90
92
  return writeAndReturn(io, errorEnvelope(error.code, error.message), args.json, 2);
91
93
  }
94
+ if (error instanceof AgentOpsError) {
95
+ return writeAndReturn(io, errorEnvelope(error.code, error.message), args.json, 1);
96
+ }
92
97
  return writeAndReturn(io, errorEnvelope("CLI_INTERNAL_ERROR", "Command execution failed."), args.json, 1);
93
98
  }
94
99
  }
@@ -1,32 +1,54 @@
1
1
  import { doctorInstallation } from "../../../../runtime/src/install/doctor.js";
2
2
  function formatDoctorReport(report) {
3
+ const surfaces = report.surfaces ?? [];
3
4
  return `${[
4
5
  "Installation doctor",
5
- ...report.checks.map(({ id, status, message }) => `- ${status} ${id}: ${message}`)
6
+ ...report.checks.map(({ id, status, message, code }) => `- ${status} ${id}${code === undefined ? "" : ` [${code}]`}: ${message}`),
7
+ ...(surfaces.length === 0
8
+ ? []
9
+ : [
10
+ "Surfaces:",
11
+ ...surfaces.map(({ harness, surfaceId, path, status, managedHandlerCount, foreignHandlerCount }) => `- ${status} ${harness}/${surfaceId}: ${path} ` +
12
+ `(managed ${managedHandlerCount}, foreign ${foreignHandlerCount})`)
13
+ ])
6
14
  ].join("\n")}\n`;
7
15
  }
8
16
  export async function runDoctorCommand(options) {
9
17
  const report = await doctorInstallation(options);
10
18
  const hasFailure = report.checks.some(({ status }) => status === "FAIL");
11
19
  const hasUnknown = report.checks.some(({ status }) => status === "UNKNOWN");
20
+ const hasUnsupported = report.checks.some(({ status }) => status === "UNSUPPORTED");
21
+ const hasDegraded = report.checks.some(({ status }) => status === "DEGRADED");
12
22
  const code = hasFailure
13
23
  ? "DOCTOR_FAILED"
14
- : hasUnknown
15
- ? "DOCTOR_UNKNOWN"
16
- : "DOCTOR_OK";
24
+ : hasUnsupported
25
+ ? "DOCTOR_UNSUPPORTED"
26
+ : hasUnknown
27
+ ? "DOCTOR_UNKNOWN"
28
+ : hasDegraded
29
+ ? "DOCTOR_DEGRADED"
30
+ : "DOCTOR_OK";
17
31
  const message = hasFailure
18
32
  ? "Installation diagnostics found failures."
19
- : hasUnknown
20
- ? "Installation diagnostics contain unknown checks."
21
- : "Installation diagnostics passed.";
33
+ : hasUnsupported
34
+ ? "Installation diagnostics found unsupported capabilities."
35
+ : hasUnknown
36
+ ? "Installation diagnostics contain unknown checks."
37
+ : hasDegraded
38
+ ? "Installation diagnostics found degraded checks."
39
+ : "Installation diagnostics passed.";
22
40
  return {
23
41
  code,
24
- status: hasFailure || hasUnknown ? "error" : "ok",
42
+ status: hasFailure || hasUnsupported || hasUnknown || hasDegraded
43
+ ? "error"
44
+ : "ok",
25
45
  data: {
26
46
  report,
27
47
  message,
28
48
  text: formatDoctorReport(report)
29
49
  },
30
- errors: hasFailure || hasUnknown ? [{ code, message }] : []
50
+ errors: hasFailure || hasUnsupported || hasUnknown || hasDegraded
51
+ ? [{ code, message }]
52
+ : []
31
53
  };
32
54
  }
@@ -1,9 +1,7 @@
1
- import { normalizeClaudeHookInput } from "../../../../runtime/src/adapters/claude/input.js";
2
- import { claudeHookOutput } from "../../../../runtime/src/adapters/claude/output.js";
3
- import { normalizeCodexHookInput } from "../../../../runtime/src/adapters/codex/input.js";
4
- import { codexHookOutput } from "../../../../runtime/src/adapters/codex/output.js";
1
+ import { runLifecycleAdvisory } from "../../../../runtime/src/hooks/advisory.js";
5
2
  import { dispatchHookEvent } from "../../../../runtime/src/hooks/dispatch.js";
6
- import { resolveProfiles } from "../../../../runtime/src/install/profiles.js";
3
+ import { resolveCapabilities } from "../../../../runtime/src/install/profiles.js";
4
+ import { harnessDescriptor } from "../../../../runtime/src/install/harness.js";
7
5
  export const HOOK_EVENTS = [
8
6
  "SessionStart",
9
7
  "PreToolUse",
@@ -15,7 +13,9 @@ export const HOOK_EVENTS = [
15
13
  */
16
14
  export async function runHookCommand(options) {
17
15
  try {
18
- const { capabilities } = resolveProfiles(options.config.profiles);
16
+ const { capabilities } = options.config.profiles.length === 0
17
+ ? { capabilities: [] }
18
+ : resolveCapabilities(options.config);
19
19
  let input;
20
20
  try {
21
21
  input = JSON.parse(options.stdin);
@@ -23,18 +23,20 @@ export async function runHookCommand(options) {
23
23
  catch {
24
24
  return { exitCode: 0, stdout: "", stderr: "" };
25
25
  }
26
- const event = options.harness === "claude"
27
- ? normalizeClaudeHookInput(input)
28
- : normalizeCodexHookInput(input);
29
- const result = await dispatchHookEvent(event, {
26
+ const descriptor = harnessDescriptor(options.harness);
27
+ const normalized = descriptor.runtime.normalizeInput(input);
28
+ const stopRegistration = descriptor.control.registrations.find(({ capability }) => capability === "optional-stop-verify");
29
+ const stopVerification = options.stopVerification !== undefined &&
30
+ stopRegistration?.support !== "unsupported"
31
+ ? options.stopVerification
32
+ : undefined;
33
+ const result = await dispatchHookEvent(normalized, {
30
34
  capabilities,
31
- trusted: options.trusted
35
+ trusted: options.trusted,
36
+ advisory: options.advisory ?? runLifecycleAdvisory,
37
+ ...(stopVerification === undefined ? {} : { stopVerification })
32
38
  });
33
- if (options.harness === "claude") {
34
- return claudeHookOutput(options.event, result);
35
- }
36
- const codex = codexHookOutput(options.event, result);
37
- return { exitCode: 0, stdout: codex.stdout, stderr: "" };
39
+ return descriptor.runtime.formatOutput(options.event, result);
38
40
  }
39
41
  catch {
40
42
  return { exitCode: 0, stdout: "", stderr: "" };
@@ -3,6 +3,7 @@ import { createInstallPlan } from "../../../../runtime/src/install/plan.js";
3
3
  import { applyInstallPlan } from "../../../../runtime/src/install/apply.js";
4
4
  import { okEnvelope } from "../output.js";
5
5
  import { formatOperationPlan } from "../plan-output.js";
6
+ import { toPublicInstallPlan } from "../public-plan.js";
6
7
  export function formatInstallPlan(plan) {
7
8
  const hooks = plan.manifest.hooks ?? [];
8
9
  return formatOperationPlan({
@@ -18,7 +19,7 @@ export function formatInstallPlan(plan) {
18
19
  ...hooks.map((hook) => ` - ${hook.harness}: ${hook.path} (${hook.events.join(", ")})`)
19
20
  ])
20
21
  ],
21
- operations: plan.operations
22
+ operations: toPublicInstallPlan(plan).operations
22
23
  });
23
24
  }
24
25
  function appliedMessage(plan) {
@@ -36,7 +37,7 @@ function initError(code, message, plan) {
36
37
  return {
37
38
  code,
38
39
  status: "error",
39
- data: { applied: false, plan, message },
40
+ data: { applied: false, plan: toPublicInstallPlan(plan), message },
40
41
  errors: [{ code, message }]
41
42
  };
42
43
  }
@@ -59,12 +60,15 @@ export async function runInitCommand(options) {
59
60
  : { toolkitVersion: options.toolkitVersion }),
60
61
  ...(options.hookRuntimePath === undefined
61
62
  ? {}
62
- : { hookRuntimePath: options.hookRuntimePath })
63
+ : { hookRuntimePath: options.hookRuntimePath }),
64
+ ...((options.hookTargets ?? args.hookTargets) === undefined
65
+ ? {}
66
+ : { hookTargets: options.hookTargets ?? args.hookTargets })
63
67
  });
64
68
  if (args.dryRun) {
65
69
  return okEnvelope("INIT_PLAN_READY", {
66
70
  applied: false,
67
- plan,
71
+ plan: toPublicInstallPlan(plan),
68
72
  message: "Installation plan calculated; no files were written.",
69
73
  text: formatInstallPlan(plan)
70
74
  });
@@ -80,7 +84,7 @@ export async function runInitCommand(options) {
80
84
  await applyInstallPlan(options.root, plan);
81
85
  return okEnvelope("INIT_APPLIED", {
82
86
  applied: true,
83
- plan,
87
+ plan: toPublicInstallPlan(plan),
84
88
  message: appliedMessage(plan)
85
89
  });
86
90
  }
@@ -2,8 +2,12 @@ import { buildReviewPacket } from "../../../../runtime/src/review/packet.js";
2
2
  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
+ /**
6
+ * Review runs against one harness. Argument parsing already rejects a
7
+ * multi-harness selection here, so the first entry is the whole selection.
8
+ */
5
9
  function harness(value) {
6
- return value === "claude" ? "claude" : "codex";
10
+ return value?.[0] ?? "codex";
7
11
  }
8
12
  export async function runReviewCommand(options) {
9
13
  const ids = options.args.criteria ?? [];
@@ -1,6 +1,7 @@
1
1
  import { applyUninstallPlan, createUninstallPlan } from "../../../../runtime/src/install/uninstall.js";
2
2
  import { okEnvelope } from "../output.js";
3
3
  import { formatOperationPlan } from "../plan-output.js";
4
+ import { toPublicUninstallPlan } from "../public-plan.js";
4
5
  function formatUninstallPlan(plan) {
5
6
  return formatOperationPlan({
6
7
  title: "Uninstall plan",
@@ -13,14 +14,14 @@ function formatUninstallPlan(plan) {
13
14
  `Harness: ${plan.manifest.harness}`
14
15
  ])
15
16
  ],
16
- operations: plan.operations
17
+ operations: toPublicUninstallPlan(plan).operations
17
18
  });
18
19
  }
19
20
  function uninstallError(code, message, plan) {
20
21
  return {
21
22
  code,
22
23
  status: "error",
23
- data: { applied: false, plan, message },
24
+ data: { applied: false, plan: toPublicUninstallPlan(plan), message },
24
25
  errors: [{ code, message }]
25
26
  };
26
27
  }
@@ -29,14 +30,14 @@ export async function runUninstallCommand(options) {
29
30
  if (!plan.installed) {
30
31
  return okEnvelope("UNINSTALL_NOT_INSTALLED", {
31
32
  applied: false,
32
- plan,
33
+ plan: toPublicUninstallPlan(plan),
33
34
  message: "No managed installation exists."
34
35
  });
35
36
  }
36
37
  if (options.args.dryRun) {
37
38
  return okEnvelope("UNINSTALL_PLAN_READY", {
38
39
  applied: false,
39
- plan,
40
+ plan: toPublicUninstallPlan(plan),
40
41
  message: "Uninstall plan calculated; no files were changed.",
41
42
  text: formatUninstallPlan(plan)
42
43
  });
@@ -51,7 +52,7 @@ export async function runUninstallCommand(options) {
51
52
  await applyUninstallPlan(options.root, plan);
52
53
  return okEnvelope("UNINSTALL_APPLIED", {
53
54
  applied: true,
54
- plan,
55
+ plan: toPublicUninstallPlan(plan),
55
56
  message: "Managed installation content was removed."
56
57
  });
57
58
  }