@kylecheng3146/agent-ops 0.1.4 → 0.1.6

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 (84) hide show
  1. package/README.md +164 -13
  2. package/dist/packages/cli/src/args.js +40 -7
  3. package/dist/packages/cli/src/bin.js +35 -11
  4. package/dist/packages/cli/src/cli.js +7 -2
  5. package/dist/packages/cli/src/codex-loop-process.js +70 -0
  6. package/dist/packages/cli/src/commands/doctor.js +31 -9
  7. package/dist/packages/cli/src/commands/hook.js +33 -16
  8. package/dist/packages/cli/src/commands/init.js +9 -5
  9. package/dist/packages/cli/src/commands/review.js +5 -1
  10. package/dist/packages/cli/src/commands/uninstall.js +6 -5
  11. package/dist/packages/cli/src/commands/update.js +16 -5
  12. package/dist/packages/cli/src/context.js +69 -3
  13. package/dist/packages/cli/src/hook-process.js +231 -16
  14. package/dist/packages/cli/src/loop-entry.js +8 -0
  15. package/dist/packages/cli/src/plan-output.js +9 -4
  16. package/dist/packages/cli/src/public-plan.js +62 -0
  17. package/dist/packages/cli/src/version.js +1 -1
  18. package/dist/packages/cli/src/wizard.js +36 -15
  19. package/dist/runtime/src/adapters/claude/config.js +57 -19
  20. package/dist/runtime/src/adapters/claude/events.js +33 -0
  21. package/dist/runtime/src/adapters/claude/output.js +8 -7
  22. package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
  23. package/dist/runtime/src/adapters/codex/config.js +66 -13
  24. package/dist/runtime/src/adapters/codex/events.js +33 -0
  25. package/dist/runtime/src/adapters/codex/output.js +10 -0
  26. package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
  27. package/dist/runtime/src/adapters/opencode/config.js +170 -0
  28. package/dist/runtime/src/adapters/opencode/events.js +49 -0
  29. package/dist/runtime/src/adapters/opencode/input.js +32 -0
  30. package/dist/runtime/src/adapters/opencode/output.js +23 -0
  31. package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
  32. package/dist/runtime/src/config/explain.js +7 -0
  33. package/dist/runtime/src/config/hash.js +24 -0
  34. package/dist/runtime/src/config/merge.js +6 -2
  35. package/dist/runtime/src/config/migrate.js +19 -4
  36. package/dist/runtime/src/contracts.js +10 -1
  37. package/dist/runtime/src/fs/managed-block.js +35 -18
  38. package/dist/runtime/src/fs/manifest.js +32 -1
  39. package/dist/runtime/src/fs/transaction.js +14 -2
  40. package/dist/runtime/src/hooks/advisory.js +16 -0
  41. package/dist/runtime/src/hooks/codex-loop.js +439 -0
  42. package/dist/runtime/src/hooks/stop-service.js +70 -0
  43. package/dist/runtime/src/hooks/stop-verify.js +4 -1
  44. package/dist/runtime/src/install/codex-loop.js +139 -0
  45. package/dist/runtime/src/install/doctor.js +182 -14
  46. package/dist/runtime/src/install/harness.js +294 -35
  47. package/dist/runtime/src/install/hooks.js +22 -17
  48. package/dist/runtime/src/install/ownership.js +146 -35
  49. package/dist/runtime/src/install/plan.js +276 -31
  50. package/dist/runtime/src/install/probes.js +9 -43
  51. package/dist/runtime/src/install/profiles.js +12 -3
  52. package/dist/runtime/src/install/surface-inspection.js +296 -0
  53. package/dist/runtime/src/install/surfaces.js +11 -0
  54. package/dist/runtime/src/install/uninstall.js +11 -4
  55. package/dist/runtime/src/install/update.js +13 -3
  56. package/dist/runtime/src/logging/local-log.js +25 -0
  57. package/dist/runtime/src/schema/validate.js +45 -19
  58. package/dist/runtime/src/task/service.js +3 -3
  59. package/dist/runtime/src/task/store.js +12 -3
  60. package/dist/runtime/src/verify/command-executor.js +113 -0
  61. package/dist/runtime/src/verify/evidence.js +4 -24
  62. package/dist/runtime/src/verify/service.js +20 -92
  63. package/dist/runtime/src/verify/spawn.js +6 -1
  64. package/docs/en/guides/configuration.md +159 -0
  65. package/docs/en/guides/quickstart.md +9 -0
  66. package/docs/en/guides/security.md +5 -0
  67. package/docs/en/spec/README.md +9 -0
  68. package/docs/en/spec/harness-adapters.md +104 -2
  69. package/docs/en/spec/maintenance.md +11 -0
  70. package/docs/en/spec/review.md +11 -0
  71. package/docs/zh-TW/guides/configuration.md +145 -0
  72. package/docs/zh-TW/guides/quickstart.md +9 -0
  73. package/docs/zh-TW/guides/security.md +5 -0
  74. package/docs/zh-TW/spec/README.md +9 -0
  75. package/docs/zh-TW/spec/harness-adapters.md +89 -3
  76. package/docs/zh-TW/spec/maintenance.md +11 -1
  77. package/docs/zh-TW/spec/review.md +10 -0
  78. package/package.json +4 -2
  79. package/schemas/config.schema.json +56 -2
  80. package/schemas/manifest.schema.json +19 -3
  81. package/templates/common/AGENTS.block.md +2 -1
  82. package/templates/common/CLAUDE.block.md +2 -1
  83. package/dist/runtime/src/review/claude-runner.js +0 -4
  84. 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`, `guardrails`, and `loop` 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:
@@ -57,10 +64,67 @@ agent-ops update --yes --json
57
64
  agent-ops uninstall --dry-run --json
58
65
  ```
59
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
+
60
115
  Use `--scope user` with user-home installations. Keep `--dry-run` for any
61
116
  operation you want to inspect before applying; non-interactive automation should
62
117
  pass `--yes` only after reviewing the plan. Add `--json` when another tool will
63
- consume the result.
118
+ consume the result. `update` operates on an existing managed installation. Pass
119
+ `--target-version <version>` when the target must be explicit or the command
120
+ must work without a registry lookup, for example:
121
+
122
+ ```bash
123
+ agent-ops update \
124
+ --harness opencode \
125
+ --target-version 0.1.4 \
126
+ --dry-run --json
127
+ ```
64
128
 
65
129
  ## Quick start from a source checkout
66
130
 
@@ -79,14 +143,14 @@ Preview a project installation before changing files:
79
143
 
80
144
  ```bash
81
145
  node dist/packages/cli/src/bin.js init \
82
- --dry-run --scope project --harness both --profile core --json
146
+ --dry-run --scope project --harness all --profile core --json
83
147
  ```
84
148
 
85
149
  After reviewing the plan, apply it explicitly with `--yes`. Trust, diagnostics,
86
150
  updates, and removal are separate commands:
87
151
 
88
152
  ```bash
89
- node dist/packages/cli/src/bin.js init --scope project --harness both --profile core --yes
153
+ node dist/packages/cli/src/bin.js init --scope project --harness all --profile core --yes
90
154
  node dist/packages/cli/src/bin.js trust status --json
91
155
  node dist/packages/cli/src/bin.js doctor --json
92
156
  node dist/packages/cli/src/bin.js config explain --json
@@ -94,17 +158,104 @@ node dist/packages/cli/src/bin.js update --dry-run --json
94
158
  node dist/packages/cli/src/bin.js uninstall --dry-run --json
95
159
  ```
96
160
 
161
+ The `dist/...` path is relative to the source checkout. When testing from a
162
+ throwaway project, run the built CLI with its absolute checkout path (or use the
163
+ published `agent-ops` command); a new project does not contain its own `dist/`
164
+ directory. `update` also requires that the project already has a valid managed
165
+ `.agent-ops/manifest.json` created by `init`.
166
+
97
167
  The commands after `init --yes` are post-apply operations. `doctor` reports
98
168
  `UNKNOWN` for a probe that has nothing to verify yet: `repository-trust` until
99
169
  `trust grant` runs, and `smoke-availability` until the configuration declares a
100
170
  verification command.
101
171
 
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.
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
+
178
+ Installing the `advisory` or `guardrails` profile registers the lifecycle and
179
+ command-policy hooks implied by those profiles for the selected harnesses.
180
+ Claude Code and Codex use their native JSON settings files; opencode uses the agent-ops-owned
181
+ `.opencode/plugins/agent-ops.js` shim and never changes `opencode.json`. Only
182
+ agent-ops-owned handlers and artifacts are managed, foreign settings are
183
+ preserved, and `uninstall` removes exactly the content it registered. At user
184
+ scope, the opencode plugin is placed under
185
+ `.config/opencode/plugins/agent-ops.js`, or under the configured
186
+ `$XDG_CONFIG_HOME/opencode/` or `$OPENCODE_CONFIG_DIR/` when that location is
187
+ inside the managed user root. The shims call `agent-ops hook <harness> <event>`
188
+ through the installed absolute runtime path. Advisory failures remain
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.
207
+
208
+ `guardrails` enables command policy only; it does not imply Stop verification.
209
+ Stop verification is an explicit, disabled-by-default config feature. Enable it
210
+ only with confirmed commands, for example the relevant config fragment is:
211
+
212
+ ```json
213
+ {
214
+ "features": { "stopVerification": { "enabled": true } },
215
+ "verification": {
216
+ "commands": [
217
+ {
218
+ "id": "unit",
219
+ "command": "npm",
220
+ "args": ["test"],
221
+ "cwd": ".",
222
+ "required": true,
223
+ "evidence": { "kind": "test-count", "minimum": 1 }
224
+ }
225
+ ]
226
+ }
227
+ }
228
+ ```
229
+
230
+ After changing Stop configuration, run `agent-ops update` so native
231
+ registrations match the config, then `agent-ops trust grant` to bind the
232
+ current config. Stop is report-only: `PASS`, `FAIL`, and `UNKNOWN` continue
233
+ the harness, emit bounded command evidence, and never complete a task.
234
+ Config v1 migrates to config v2 with Stop disabled; migration invalidates the
235
+ old trust binding, and pre-v1 binaries cannot read the migrated config.
236
+
237
+ The generated `AGENTS.md` and `CLAUDE.md` routing blocks are supplemental: they
238
+ load the managed baseline while leaving project-specific instructions in those
239
+ files authoritative. Existing installations with the previous canonical
240
+ wording are migrated by `agent-ops update`; changed managed blocks still fail
241
+ closed.
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
+
252
+ Dry-run plans keep harness settings writes opaque: human and JSON output expose
253
+ only the expected hash, content hash, and a safe summary. Use
254
+ `--hook-target <harness>=<surface-id>` when selecting a non-default discovered
255
+ surface; project-local Claude settings are never selected implicitly. The
256
+ internal plan still retains the complete merged settings for transactional
257
+ apply. The routing migration is one-way once applied; review the release notes
258
+ before attempting a downgrade.
108
259
 
109
260
  For a full command reference, run `agent-ops --help`. The `task`, `verify`, and
110
261
  `review` commands support acceptance tracking and independent verification when
@@ -116,7 +267,7 @@ the project configuration defines those workflows.
116
267
  - Treat command output and current filesystem state as evidence.
117
268
  - Keep advisory automation separate from blocking guardrails.
118
269
  - Preserve user configuration through managed, reversible updates.
119
- - Support Codex and Claude Code without project-specific assumptions.
270
+ - Support Codex, Claude Code, and opencode without project-specific assumptions.
120
271
  - Collect no network telemetry.
121
272
 
122
273
  ## 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,8 +12,7 @@ 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
- const PROFILES = new Set(["advisory", "core", "guardrails"]);
15
+ const PROFILES = new Set(["advisory", "core", "guardrails", "loop"]);
16
16
  export class CliArgumentError extends Error {
17
17
  code;
18
18
  option;
@@ -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
  }
@@ -111,12 +131,12 @@ else {
111
131
  const config = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
112
132
  return await runDoctorCommand({
113
133
  root,
134
+ toolkitVersion: CLI_VERSION,
114
135
  probes: {
115
136
  hookRegistration: async () => hookRegistrationSatisfied({
116
137
  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"))
138
+ config,
139
+ sources: await hookSources(root, args.scope === "user" ? "user" : "project")
120
140
  }),
121
141
  repositoryTrust: async () => repositoryTrustStatus(await repositoryTrust(root, config, CLI_VERSION)),
122
142
  smokeAvailability: () => smokeAvailabilityStatus(config)
@@ -138,7 +158,11 @@ else {
138
158
  adapters: commonHarnessAdapters(),
139
159
  registry: new NpmRegistryClient(),
140
160
  isTTY,
161
+ toolkitVersion: CLI_VERSION,
141
162
  hookRuntimePath: HOOK_RUNTIME_PATH,
163
+ ...(args.hookTargets === undefined
164
+ ? {}
165
+ : { hookTargets: args.hookTargets }),
142
166
  confirm: async (plan) => await confirmPlan(formatUpdatePlan(plan)),
143
167
  ...(args.targetVersion === undefined
144
168
  ? {}
@@ -219,7 +243,7 @@ else {
219
243
  const binding = await calculateTrustBinding({
220
244
  repositoryPath: root,
221
245
  remoteUrl: remote,
222
- configHash: sha256(JSON.stringify(config)),
246
+ configHash: calculateConfigHash(config),
223
247
  runtimeHash: sha256(CLI_VERSION)
224
248
  });
225
249
  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,8 +29,9 @@ Commands:
28
29
 
29
30
  Options:
30
31
  --scope <project|user>
31
- --harness <both|claude|codex>
32
- --profile <core|advisory|guardrails> Repeatable
32
+ --harness <all|both|claude|codex|opencode|comma-separated> Init/update
33
+ --hook-target <harness=surface-id> Repeatable advanced init/update option
34
+ --profile <core|advisory|guardrails|loop> Repeatable
33
35
  --task <id>
34
36
  --target-version <version> Update target version (offline-capable)
35
37
  --title <text>
@@ -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
  }
@@ -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
+ }
@@ -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
  }