@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.
- package/README.md +88 -13
- package/dist/packages/cli/src/args.js +39 -6
- package/dist/packages/cli/src/bin.js +33 -11
- package/dist/packages/cli/src/cli.js +6 -1
- package/dist/packages/cli/src/commands/doctor.js +31 -9
- package/dist/packages/cli/src/commands/hook.js +18 -16
- package/dist/packages/cli/src/commands/init.js +9 -5
- package/dist/packages/cli/src/commands/review.js +5 -1
- package/dist/packages/cli/src/commands/uninstall.js +6 -5
- package/dist/packages/cli/src/commands/update.js +13 -5
- package/dist/packages/cli/src/context.js +9 -3
- package/dist/packages/cli/src/hook-process.js +109 -7
- package/dist/packages/cli/src/plan-output.js +9 -4
- package/dist/packages/cli/src/public-plan.js +62 -0
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/wizard.js +27 -11
- package/dist/runtime/src/adapters/claude/config.js +0 -8
- package/dist/runtime/src/adapters/claude/events.js +26 -0
- package/dist/runtime/src/adapters/claude/output.js +6 -6
- package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
- package/dist/runtime/src/adapters/codex/config.js +29 -11
- package/dist/runtime/src/adapters/codex/events.js +26 -0
- package/dist/runtime/src/adapters/codex/output.js +10 -0
- package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
- package/dist/runtime/src/adapters/opencode/config.js +170 -0
- package/dist/runtime/src/adapters/opencode/events.js +49 -0
- package/dist/runtime/src/adapters/opencode/input.js +32 -0
- package/dist/runtime/src/adapters/opencode/output.js +23 -0
- package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
- package/dist/runtime/src/config/explain.js +7 -0
- package/dist/runtime/src/config/hash.js +24 -0
- package/dist/runtime/src/config/merge.js +6 -2
- package/dist/runtime/src/config/migrate.js +19 -4
- package/dist/runtime/src/contracts.js +10 -1
- package/dist/runtime/src/fs/manifest.js +32 -1
- package/dist/runtime/src/fs/transaction.js +14 -2
- package/dist/runtime/src/hooks/advisory.js +16 -0
- package/dist/runtime/src/hooks/stop-service.js +70 -0
- package/dist/runtime/src/hooks/stop-verify.js +4 -1
- package/dist/runtime/src/install/doctor.js +119 -9
- package/dist/runtime/src/install/harness.js +296 -35
- package/dist/runtime/src/install/hooks.js +22 -17
- package/dist/runtime/src/install/ownership.js +110 -34
- package/dist/runtime/src/install/plan.js +207 -28
- package/dist/runtime/src/install/probes.js +9 -43
- package/dist/runtime/src/install/profiles.js +8 -1
- package/dist/runtime/src/install/surface-inspection.js +296 -0
- package/dist/runtime/src/install/surfaces.js +11 -0
- package/dist/runtime/src/install/uninstall.js +10 -3
- package/dist/runtime/src/install/update.js +8 -2
- package/dist/runtime/src/schema/validate.js +37 -18
- package/dist/runtime/src/task/service.js +3 -3
- package/dist/runtime/src/task/store.js +12 -3
- package/dist/runtime/src/verify/command-executor.js +113 -0
- package/dist/runtime/src/verify/evidence.js +4 -24
- package/dist/runtime/src/verify/service.js +20 -92
- package/dist/runtime/src/verify/spawn.js +6 -1
- package/docs/en/guides/configuration.md +83 -0
- package/docs/en/guides/quickstart.md +9 -0
- package/docs/en/guides/security.md +5 -0
- package/docs/en/spec/README.md +9 -0
- package/docs/en/spec/harness-adapters.md +66 -2
- package/docs/en/spec/maintenance.md +11 -0
- package/docs/en/spec/review.md +11 -0
- package/docs/zh-TW/guides/configuration.md +77 -0
- package/docs/zh-TW/guides/quickstart.md +9 -0
- package/docs/zh-TW/guides/security.md +5 -0
- package/docs/zh-TW/spec/README.md +9 -0
- package/docs/zh-TW/spec/harness-adapters.md +57 -3
- package/docs/zh-TW/spec/maintenance.md +11 -1
- package/docs/zh-TW/spec/review.md +10 -0
- package/package.json +4 -2
- package/schemas/config.schema.json +55 -1
- package/schemas/manifest.schema.json +7 -2
- package/templates/common/AGENTS.block.md +2 -1
- package/templates/common/CLAUDE.block.md +2 -1
- package/dist/runtime/src/review/claude-runner.js +0 -4
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
103
|
-
hooks
|
|
104
|
-
|
|
105
|
-
`
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
37
|
+
async function readOptionalText(path) {
|
|
37
38
|
try {
|
|
38
|
-
return
|
|
39
|
+
return await readFile(path, "utf8");
|
|
39
40
|
}
|
|
40
41
|
catch {
|
|
41
42
|
return null;
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
|
-
async function
|
|
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"))
|
|
61
|
+
return parseInstallManifest(await readFile(join(root, ".agent-ops", "manifest.json"), "utf8"));
|
|
47
62
|
}
|
|
48
63
|
catch {
|
|
49
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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:
|
|
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
|
-
:
|
|
15
|
-
? "
|
|
16
|
-
:
|
|
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
|
-
:
|
|
20
|
-
? "Installation diagnostics
|
|
21
|
-
:
|
|
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 ||
|
|
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 ||
|
|
50
|
+
errors: hasFailure || hasUnsupported || hasUnknown || hasDegraded
|
|
51
|
+
? [{ code, message }]
|
|
52
|
+
: []
|
|
31
53
|
};
|
|
32
54
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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 } =
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
}
|