@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.
- package/README.md +164 -13
- package/dist/packages/cli/src/args.js +40 -7
- package/dist/packages/cli/src/bin.js +35 -11
- package/dist/packages/cli/src/cli.js +7 -2
- package/dist/packages/cli/src/codex-loop-process.js +70 -0
- package/dist/packages/cli/src/commands/doctor.js +31 -9
- package/dist/packages/cli/src/commands/hook.js +33 -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 +16 -5
- package/dist/packages/cli/src/context.js +69 -3
- package/dist/packages/cli/src/hook-process.js +231 -16
- package/dist/packages/cli/src/loop-entry.js +8 -0
- 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 +36 -15
- package/dist/runtime/src/adapters/claude/config.js +57 -19
- package/dist/runtime/src/adapters/claude/events.js +33 -0
- package/dist/runtime/src/adapters/claude/output.js +8 -7
- package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
- package/dist/runtime/src/adapters/codex/config.js +66 -13
- package/dist/runtime/src/adapters/codex/events.js +33 -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/managed-block.js +35 -18
- 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/codex-loop.js +439 -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/codex-loop.js +139 -0
- package/dist/runtime/src/install/doctor.js +182 -14
- package/dist/runtime/src/install/harness.js +294 -35
- package/dist/runtime/src/install/hooks.js +22 -17
- package/dist/runtime/src/install/ownership.js +146 -35
- package/dist/runtime/src/install/plan.js +276 -31
- package/dist/runtime/src/install/probes.js +9 -43
- package/dist/runtime/src/install/profiles.js +12 -3
- 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 +11 -4
- package/dist/runtime/src/install/update.js +13 -3
- package/dist/runtime/src/logging/local-log.js +25 -0
- package/dist/runtime/src/schema/validate.js +45 -19
- 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 +159 -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 +104 -2
- package/docs/en/spec/maintenance.md +11 -0
- package/docs/en/spec/review.md +11 -0
- package/docs/zh-TW/guides/configuration.md +145 -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 +89 -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 +56 -2
- package/schemas/manifest.schema.json +19 -3
- 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`, `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
|
|
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:
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`
|
|
107
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
118
|
-
|
|
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:
|
|
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
|
-
--
|
|
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
|
-
:
|
|
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
|
}
|