@kylecheng3146/agent-ops 0.1.5 → 0.1.7
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 +104 -6
- package/dist/packages/cli/src/args.js +33 -1
- package/dist/packages/cli/src/bin.js +40 -3
- package/dist/packages/cli/src/cli.js +13 -2
- package/dist/packages/cli/src/codex-loop-process.js +70 -0
- package/dist/packages/cli/src/commands/hook.js +16 -1
- package/dist/packages/cli/src/commands/init.js +4 -1
- package/dist/packages/cli/src/commands/review.js +97 -10
- package/dist/packages/cli/src/commands/update.js +3 -0
- package/dist/packages/cli/src/context.js +60 -0
- package/dist/packages/cli/src/hook-process.js +128 -15
- package/dist/packages/cli/src/loop-entry.js +8 -0
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/wizard.js +71 -7
- package/dist/runtime/src/adapters/claude/config.js +57 -11
- package/dist/runtime/src/adapters/claude/events.js +7 -0
- package/dist/runtime/src/adapters/claude/output.js +2 -1
- package/dist/runtime/src/adapters/codex/config.js +39 -4
- package/dist/runtime/src/adapters/codex/events.js +7 -0
- package/dist/runtime/src/config/merge.js +17 -2
- package/dist/runtime/src/fs/managed-block.js +35 -18
- package/dist/runtime/src/hooks/codex-loop.js +439 -0
- package/dist/runtime/src/install/codex-loop.js +139 -0
- package/dist/runtime/src/install/doctor.js +108 -9
- package/dist/runtime/src/install/harness.js +8 -10
- package/dist/runtime/src/install/ownership.js +37 -2
- package/dist/runtime/src/install/plan.js +81 -9
- package/dist/runtime/src/install/profiles.js +5 -3
- package/dist/runtime/src/install/uninstall.js +1 -1
- package/dist/runtime/src/install/update.js +5 -1
- package/dist/runtime/src/logging/local-log.js +25 -0
- package/dist/runtime/src/review/execute.js +120 -0
- package/dist/runtime/src/review/extract.js +71 -0
- package/dist/runtime/src/review/invocation.js +52 -0
- package/dist/runtime/src/review/probe.js +48 -0
- package/dist/runtime/src/review/result.js +2 -2
- package/dist/runtime/src/review/roles.js +35 -0
- package/dist/runtime/src/review/runner.js +38 -4
- package/dist/runtime/src/schema/validate.js +70 -1
- package/dist/runtime/src/task/service.js +40 -0
- package/docs/en/guides/configuration.md +138 -2
- package/docs/en/spec/harness-adapters.md +50 -12
- package/docs/en/spec/review.md +37 -4
- package/docs/zh-TW/guides/configuration.md +126 -5
- package/docs/zh-TW/spec/harness-adapters.md +44 -12
- package/docs/zh-TW/spec/review.md +33 -3
- package/package.json +1 -1
- package/schemas/config.schema.json +30 -1
- package/schemas/manifest.schema.json +12 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ installation plan.
|
|
|
35
35
|
|
|
36
36
|
The interactive multi-select screens start with no harness or profile selected.
|
|
37
37
|
Choose at least one of `codex`, `claude`, and `opencode`, and at least one of
|
|
38
|
-
the `core`, `advisory`, and `
|
|
38
|
+
the `core`, `advisory`, `guardrails`, and `loop` profiles before confirming. For
|
|
39
39
|
scripted use, `--harness all` selects all three harnesses; comma-separated
|
|
40
40
|
selections such as `codex,opencode` are also supported. The legacy `both` value
|
|
41
41
|
remains an alias for `codex,claude`.
|
|
@@ -64,6 +64,54 @@ agent-ops update --yes --json
|
|
|
64
64
|
agent-ops uninstall --dry-run --json
|
|
65
65
|
```
|
|
66
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
|
+
|
|
67
115
|
Use `--scope user` with user-home installations. Keep `--dry-run` for any
|
|
68
116
|
operation you want to inspect before applying; non-interactive automation should
|
|
69
117
|
pass `--yes` only after reviewing the plan. Add `--json` when another tool will
|
|
@@ -121,6 +169,12 @@ The commands after `init --yes` are post-apply operations. `doctor` reports
|
|
|
121
169
|
`trust grant` runs, and `smoke-availability` until the configuration declares a
|
|
122
170
|
verification command.
|
|
123
171
|
|
|
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
|
+
|
|
124
178
|
Installing the `advisory` or `guardrails` profile registers the lifecycle and
|
|
125
179
|
command-policy hooks implied by those profiles for the selected harnesses.
|
|
126
180
|
Claude Code and Codex use their native JSON settings files; opencode uses the agent-ops-owned
|
|
@@ -132,11 +186,24 @@ scope, the opencode plugin is placed under
|
|
|
132
186
|
`$XDG_CONFIG_HOME/opencode/` or `$OPENCODE_CONFIG_DIR/` when that location is
|
|
133
187
|
inside the managed user root. The shims call `agent-ops hook <harness> <event>`
|
|
134
188
|
through the installed absolute runtime path. Advisory failures remain
|
|
135
|
-
fail-open
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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.
|
|
140
207
|
|
|
141
208
|
`guardrails` enables command policy only; it does not imply Stop verification.
|
|
142
209
|
Stop verification is an explicit, disabled-by-default config feature. Enable it
|
|
@@ -173,6 +240,15 @@ files authoritative. Existing installations with the previous canonical
|
|
|
173
240
|
wording are migrated by `agent-ops update`; changed managed blocks still fail
|
|
174
241
|
closed.
|
|
175
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
|
+
|
|
176
252
|
Dry-run plans keep harness settings writes opaque: human and JSON output expose
|
|
177
253
|
only the expected hash, content hash, and a safe summary. Use
|
|
178
254
|
`--hook-target <harness>=<surface-id>` when selecting a non-default discovered
|
|
@@ -185,6 +261,28 @@ For a full command reference, run `agent-ops --help`. The `task`, `verify`, and
|
|
|
185
261
|
`review` commands support acceptance tracking and independent verification when
|
|
186
262
|
the project configuration defines those workflows.
|
|
187
263
|
|
|
264
|
+
### External review
|
|
265
|
+
|
|
266
|
+
`agent-ops review` can hand the review to another agent CLI, so the work is not
|
|
267
|
+
judged by the agent that produced it. Enable it during `agent-ops init` (the
|
|
268
|
+
default is off) and pick an ordered fallback chain of targets: `codex`, `agy`
|
|
269
|
+
(Antigravity), and `claude`. Each is launched with its own read-only flag, and a
|
|
270
|
+
target without one is skipped rather than run unsandboxed — which is why
|
|
271
|
+
`opencode` is not a review target despite being a supported harness.
|
|
272
|
+
|
|
273
|
+
The first target that actually runs produces the verdict. A `FAIL` is final:
|
|
274
|
+
the chain never retries elsewhere after a real verdict. `--yes` is still
|
|
275
|
+
required for every run, since each run spends another provider's quota.
|
|
276
|
+
|
|
277
|
+
Authentication is diagnosed, never guessed:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
agent-ops doctor # presence only: no tokens, no network
|
|
281
|
+
agent-ops doctor --check-auth # one real print call per configured target
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
See [Configuration](docs/en/guides/configuration.md) for the full contract.
|
|
285
|
+
|
|
188
286
|
## Project principles
|
|
189
287
|
|
|
190
288
|
- Define verifiable success before making changes.
|
|
@@ -12,7 +12,9 @@ export const COMMAND_NAMES = [
|
|
|
12
12
|
];
|
|
13
13
|
const COMMAND_SET = new Set(COMMAND_NAMES);
|
|
14
14
|
const SCOPES = new Set(["project", "user"]);
|
|
15
|
-
const PROFILES = new Set(["advisory", "core", "guardrails"]);
|
|
15
|
+
const PROFILES = new Set(["advisory", "core", "guardrails", "loop"]);
|
|
16
|
+
// opencode is absent: it has no read-only flag, so it cannot review.
|
|
17
|
+
const REVIEW_TARGETS = new Set(["agy", "claude", "codex"]);
|
|
16
18
|
export class CliArgumentError extends Error {
|
|
17
19
|
code;
|
|
18
20
|
option;
|
|
@@ -65,8 +67,10 @@ export function parseArgs(argv) {
|
|
|
65
67
|
let title;
|
|
66
68
|
let sessionId;
|
|
67
69
|
const profiles = [];
|
|
70
|
+
const reviewTargets = [];
|
|
68
71
|
const criteria = [];
|
|
69
72
|
const evidence = [];
|
|
73
|
+
let checkAuth = false;
|
|
70
74
|
let dryRun = false;
|
|
71
75
|
let json = false;
|
|
72
76
|
let yes = false;
|
|
@@ -121,6 +125,18 @@ export function parseArgs(argv) {
|
|
|
121
125
|
index += 1;
|
|
122
126
|
break;
|
|
123
127
|
}
|
|
128
|
+
case "--review-target": {
|
|
129
|
+
const value = readOptionValue(argv, index, token);
|
|
130
|
+
if (!REVIEW_TARGETS.has(value)) {
|
|
131
|
+
invalidValue(token, value);
|
|
132
|
+
}
|
|
133
|
+
if (reviewTargets.includes(value)) {
|
|
134
|
+
duplicate(`${token} ${value}`);
|
|
135
|
+
}
|
|
136
|
+
reviewTargets.push(value);
|
|
137
|
+
index += 1;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
124
140
|
case "--task": {
|
|
125
141
|
if (taskId !== undefined) {
|
|
126
142
|
duplicate(token);
|
|
@@ -163,6 +179,12 @@ export function parseArgs(argv) {
|
|
|
163
179
|
index += 1;
|
|
164
180
|
break;
|
|
165
181
|
}
|
|
182
|
+
case "--check-auth":
|
|
183
|
+
if (checkAuth) {
|
|
184
|
+
duplicate(token);
|
|
185
|
+
}
|
|
186
|
+
checkAuth = true;
|
|
187
|
+
break;
|
|
166
188
|
case "--dry-run":
|
|
167
189
|
if (dryRun) {
|
|
168
190
|
duplicate(token);
|
|
@@ -249,7 +271,9 @@ export function parseArgs(argv) {
|
|
|
249
271
|
title !== undefined ||
|
|
250
272
|
criteria.length > 0 ||
|
|
251
273
|
evidence.length > 0 ||
|
|
274
|
+
reviewTargets.length > 0 ||
|
|
252
275
|
sessionId !== undefined ||
|
|
276
|
+
checkAuth ||
|
|
253
277
|
dryRun ||
|
|
254
278
|
yes) {
|
|
255
279
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Only --json may be combined with global help or version.");
|
|
@@ -284,6 +308,12 @@ export function parseArgs(argv) {
|
|
|
284
308
|
command !== "update") {
|
|
285
309
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--hook-target may be used only with init or update.");
|
|
286
310
|
}
|
|
311
|
+
if (checkAuth && command !== "doctor") {
|
|
312
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--check-auth may be used only with doctor.");
|
|
313
|
+
}
|
|
314
|
+
if (reviewTargets.length > 0 && command !== "init") {
|
|
315
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--review-target may be used only with init.");
|
|
316
|
+
}
|
|
287
317
|
if (command === "task") {
|
|
288
318
|
if (harness !== undefined ||
|
|
289
319
|
profiles.length > 0 ||
|
|
@@ -345,9 +375,11 @@ export function parseArgs(argv) {
|
|
|
345
375
|
...(taskId === undefined ? {} : { taskId }),
|
|
346
376
|
...(targetVersion === undefined ? {} : { targetVersion }),
|
|
347
377
|
...(title === undefined ? {} : { title }),
|
|
378
|
+
...(reviewTargets.length === 0 ? {} : { reviewTargets }),
|
|
348
379
|
...(criteria.length === 0 ? {} : { criteria }),
|
|
349
380
|
...(evidence.length === 0 ? {} : { evidence }),
|
|
350
381
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
382
|
+
...(checkAuth ? { checkAuth } : {}),
|
|
351
383
|
dryRun,
|
|
352
384
|
json,
|
|
353
385
|
yes
|
|
@@ -29,6 +29,9 @@ import { formatInstallPlan, runInitCommand } from "./commands/init.js";
|
|
|
29
29
|
import { formatUninstallPlan, runUninstallCommand } from "./commands/uninstall.js";
|
|
30
30
|
import { runTaskCommand } from "./commands/task.js";
|
|
31
31
|
import { runReviewCommand } from "./commands/review.js";
|
|
32
|
+
import { createReviewExecutor } from "../../../runtime/src/review/execute.js";
|
|
33
|
+
import { probeReviewTarget } from "../../../runtime/src/review/probe.js";
|
|
34
|
+
import { resolveReviewRole } from "../../../runtime/src/review/roles.js";
|
|
32
35
|
import { runTrustCommand } from "./commands/trust.js";
|
|
33
36
|
import { runVerifyCommand } from "./commands/verify.js";
|
|
34
37
|
import { formatUpdatePlan, runUpdateCommand } from "./commands/update.js";
|
|
@@ -131,6 +134,7 @@ else {
|
|
|
131
134
|
const config = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
|
|
132
135
|
return await runDoctorCommand({
|
|
133
136
|
root,
|
|
137
|
+
toolkitVersion: CLI_VERSION,
|
|
134
138
|
probes: {
|
|
135
139
|
hookRegistration: async () => hookRegistrationSatisfied({
|
|
136
140
|
harness: await installedHarness(root),
|
|
@@ -138,8 +142,12 @@ else {
|
|
|
138
142
|
sources: await hookSources(root, args.scope === "user" ? "user" : "project")
|
|
139
143
|
}),
|
|
140
144
|
repositoryTrust: async () => repositoryTrustStatus(await repositoryTrust(root, config, CLI_VERSION)),
|
|
141
|
-
smokeAvailability: () => smokeAvailabilityStatus(config)
|
|
142
|
-
|
|
145
|
+
smokeAvailability: () => smokeAvailabilityStatus(config),
|
|
146
|
+
reviewTarget: async (target, deep) => await probeReviewTarget(target, { cwd: root, deep })
|
|
147
|
+
},
|
|
148
|
+
...(args.checkAuth === true
|
|
149
|
+
? { checkReviewTargetAuth: true }
|
|
150
|
+
: {})
|
|
143
151
|
});
|
|
144
152
|
}
|
|
145
153
|
if (args.command === "uninstall") {
|
|
@@ -157,6 +165,7 @@ else {
|
|
|
157
165
|
adapters: commonHarnessAdapters(),
|
|
158
166
|
registry: new NpmRegistryClient(),
|
|
159
167
|
isTTY,
|
|
168
|
+
toolkitVersion: CLI_VERSION,
|
|
160
169
|
hookRuntimePath: HOOK_RUNTIME_PATH,
|
|
161
170
|
...(args.hookTargets === undefined
|
|
162
171
|
? {}
|
|
@@ -177,9 +186,37 @@ else {
|
|
|
177
186
|
});
|
|
178
187
|
}
|
|
179
188
|
if (args.command === "review") {
|
|
189
|
+
const reviewSessionId = process.env.AGENT_OPS_SESSION_ID;
|
|
190
|
+
const reviewConfig = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
|
|
191
|
+
const reviewRole = resolveReviewRole("independent-review", reviewConfig.reviewRoles ?? []);
|
|
180
192
|
return await runReviewCommand({
|
|
181
193
|
args,
|
|
182
|
-
authorized: args.yes
|
|
194
|
+
authorized: args.yes,
|
|
195
|
+
tasks: taskService,
|
|
196
|
+
...(reviewSessionId === undefined
|
|
197
|
+
? {}
|
|
198
|
+
: { sessionId: reviewSessionId }),
|
|
199
|
+
...(reviewConfig.reviewRoles === undefined
|
|
200
|
+
? {}
|
|
201
|
+
: { roles: reviewConfig.reviewRoles }),
|
|
202
|
+
execute: createReviewExecutor({
|
|
203
|
+
targets: reviewRole?.targets ?? [],
|
|
204
|
+
cwd: root,
|
|
205
|
+
...(reviewRole?.model === undefined
|
|
206
|
+
? {}
|
|
207
|
+
: { model: reviewRole.model }),
|
|
208
|
+
...(reviewRole?.effort === undefined
|
|
209
|
+
? {}
|
|
210
|
+
: { effort: reviewRole.effort }),
|
|
211
|
+
...(reviewRole?.timeoutMs === undefined
|
|
212
|
+
? {}
|
|
213
|
+
: { timeoutMs: reviewRole.timeoutMs }),
|
|
214
|
+
onProgress: (line) => {
|
|
215
|
+
if (!args.json) {
|
|
216
|
+
process.stderr.write(`${line}\n`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
})
|
|
183
220
|
});
|
|
184
221
|
}
|
|
185
222
|
if (args.command === "config") {
|
|
@@ -2,6 +2,7 @@ import { CliArgumentError, parseArgs } from "./args.js";
|
|
|
2
2
|
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
3
3
|
import { errorEnvelope, okEnvelope, writeEnvelope } from "./output.js";
|
|
4
4
|
import { completeInitChoices } from "./wizard.js";
|
|
5
|
+
import { probeReviewTarget } from "../../../runtime/src/review/probe.js";
|
|
5
6
|
import { BANNER } from "./ui.js";
|
|
6
7
|
export function renderWelcome(color) {
|
|
7
8
|
const cyan = color ? "\u001b[36m" : "";
|
|
@@ -31,7 +32,11 @@ Options:
|
|
|
31
32
|
--scope <project|user>
|
|
32
33
|
--harness <all|both|claude|codex|opencode|comma-separated> Init/update
|
|
33
34
|
--hook-target <harness=surface-id> Repeatable advanced init/update option
|
|
34
|
-
--profile <core|advisory|guardrails> Repeatable
|
|
35
|
+
--profile <core|advisory|guardrails|loop> Repeatable
|
|
36
|
+
--review-target <codex|agy|claude> Repeatable init option; external review
|
|
37
|
+
targets in fallback-chain order
|
|
38
|
+
--check-auth Doctor only: probe each review target's
|
|
39
|
+
authentication with one real call
|
|
35
40
|
--task <id>
|
|
36
41
|
--target-version <version> Update target version (offline-capable)
|
|
37
42
|
--title <text>
|
|
@@ -76,7 +81,13 @@ export async function runCli(argv, io, services) {
|
|
|
76
81
|
}
|
|
77
82
|
try {
|
|
78
83
|
if (args.command === "init") {
|
|
79
|
-
args = await completeInitChoices(args, args.json ? { ...io, isTTY: false } : io
|
|
84
|
+
args = await completeInitChoices(args, args.json ? { ...io, isTTY: false } : io, {
|
|
85
|
+
probeReviewTarget: async (target) => (await probeReviewTarget(target, {
|
|
86
|
+
cwd: process.cwd(),
|
|
87
|
+
deep: true
|
|
88
|
+
})) === "ok",
|
|
89
|
+
warn: (message) => io.writeStderr(`${message}\n`)
|
|
90
|
+
});
|
|
80
91
|
}
|
|
81
92
|
const execute = args.command === "help" || args.command === "version"
|
|
82
93
|
? services.execute
|
|
@@ -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
|
+
}
|
|
@@ -7,6 +7,18 @@ export const HOOK_EVENTS = [
|
|
|
7
7
|
"PreToolUse",
|
|
8
8
|
"Stop"
|
|
9
9
|
];
|
|
10
|
+
/**
|
|
11
|
+
* Keep adapter normalization at the command boundary so exceptional hook
|
|
12
|
+
* paths use the same native-event interpretation as normal dispatch.
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeHookInput(harness, input) {
|
|
15
|
+
try {
|
|
16
|
+
return harnessDescriptor(harness).runtime.normalizeInput(input);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
/**
|
|
11
23
|
* Hooks are advisory infrastructure: every failure path stays fail-open with
|
|
12
24
|
* exit code 0 so a broken toolkit can never wedge the harness.
|
|
@@ -24,7 +36,10 @@ export async function runHookCommand(options) {
|
|
|
24
36
|
return { exitCode: 0, stdout: "", stderr: "" };
|
|
25
37
|
}
|
|
26
38
|
const descriptor = harnessDescriptor(options.harness);
|
|
27
|
-
const normalized =
|
|
39
|
+
const normalized = normalizeHookInput(options.harness, input);
|
|
40
|
+
if (normalized === null) {
|
|
41
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
42
|
+
}
|
|
28
43
|
const stopRegistration = descriptor.control.registrations.find(({ capability }) => capability === "optional-stop-verify");
|
|
29
44
|
const stopVerification = options.stopVerification !== undefined &&
|
|
30
45
|
stopRegistration?.support !== "unsupported"
|
|
@@ -63,7 +63,10 @@ export async function runInitCommand(options) {
|
|
|
63
63
|
: { hookRuntimePath: options.hookRuntimePath }),
|
|
64
64
|
...((options.hookTargets ?? args.hookTargets) === undefined
|
|
65
65
|
? {}
|
|
66
|
-
: { hookTargets: options.hookTargets ?? args.hookTargets })
|
|
66
|
+
: { hookTargets: options.hookTargets ?? args.hookTargets }),
|
|
67
|
+
...(args.reviewTargets === undefined
|
|
68
|
+
? {}
|
|
69
|
+
: { reviewTargets: args.reviewTargets })
|
|
67
70
|
});
|
|
68
71
|
if (args.dryRun) {
|
|
69
72
|
return okEnvelope("INIT_PLAN_READY", {
|
|
@@ -3,18 +3,78 @@ 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
5
|
/**
|
|
6
|
-
* Review runs against one
|
|
6
|
+
* Review runs against one target. Argument parsing already rejects a
|
|
7
7
|
* multi-harness selection here, so the first entry is the whole selection.
|
|
8
|
+
* `opencode` is not a review target — it has no read-only flag — so selecting
|
|
9
|
+
* it leaves the target unresolved and the configured chain decides.
|
|
8
10
|
*/
|
|
9
11
|
function harness(value) {
|
|
10
|
-
|
|
12
|
+
const selected = value?.[0];
|
|
13
|
+
return selected === undefined || selected === "opencode"
|
|
14
|
+
? undefined
|
|
15
|
+
: selected;
|
|
11
16
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Criterion descriptions come from the task store, never from the id. A
|
|
19
|
+
* reviewer handed `criterion: tests` cannot review anything, so a review with
|
|
20
|
+
* no task context is reported as not run rather than run meaninglessly.
|
|
21
|
+
*/
|
|
22
|
+
async function taskContext(options) {
|
|
23
|
+
const tasks = options.tasks;
|
|
24
|
+
if (tasks === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const query = options.taskId !== undefined
|
|
28
|
+
? { taskId: options.taskId }
|
|
29
|
+
: options.sessionId === undefined
|
|
30
|
+
? undefined
|
|
31
|
+
: { sessionId: options.sessionId };
|
|
32
|
+
if (query === undefined) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
let record;
|
|
36
|
+
try {
|
|
37
|
+
record = await tasks.status(query);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const requested = options.args.criteria ?? [];
|
|
43
|
+
const criteria = record.task.criteria
|
|
44
|
+
.filter((criterion) => requested.length === 0 || requested.includes(criterion.id))
|
|
45
|
+
.map((criterion) => ({
|
|
46
|
+
id: criterion.id,
|
|
47
|
+
description: criterion.description,
|
|
48
|
+
verifierIds: [...criterion.verifierIds]
|
|
17
49
|
}));
|
|
50
|
+
if (criteria.length === 0 ||
|
|
51
|
+
(requested.length > 0 && criteria.length !== requested.length)) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
taskId: record.task.id,
|
|
56
|
+
active: record.status === "active",
|
|
57
|
+
criteria
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function notRunEnvelope(result) {
|
|
61
|
+
const message = "Independent review was not run.";
|
|
62
|
+
return {
|
|
63
|
+
code: "REVIEW_NOT_RUN",
|
|
64
|
+
status: "error",
|
|
65
|
+
data: {
|
|
66
|
+
message,
|
|
67
|
+
result,
|
|
68
|
+
text: [message, `Reason: ${result.reason ?? "unknown"}.`, ""].join("\n")
|
|
69
|
+
},
|
|
70
|
+
errors: [{ code: "REVIEW_NOT_RUN", message }]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export async function runReviewCommand(options) {
|
|
74
|
+
const role = resolveReviewRole(options.role ?? "independent-review", options.roles ?? []);
|
|
75
|
+
const selectedHarness = harness(options.args.harness);
|
|
76
|
+
const target = role?.targets[0] ?? selectedHarness ?? "codex";
|
|
77
|
+
const context = await taskContext(options);
|
|
18
78
|
const evidenceRequirements = (options.args.evidence ?? []).map((value) => {
|
|
19
79
|
const separator = value.indexOf("=");
|
|
20
80
|
return {
|
|
@@ -22,11 +82,22 @@ export async function runReviewCommand(options) {
|
|
|
22
82
|
requirement: separator < 0 ? value : value.slice(separator + 1)
|
|
23
83
|
};
|
|
24
84
|
});
|
|
25
|
-
|
|
26
|
-
|
|
85
|
+
if (options.tasks !== undefined && context === undefined) {
|
|
86
|
+
return notRunEnvelope({
|
|
87
|
+
status: "NOT_RUN",
|
|
88
|
+
reason: "no-task-context",
|
|
89
|
+
harness: target,
|
|
90
|
+
model: role?.model ?? options.model ?? "configured",
|
|
91
|
+
effort: role?.effort ?? options.effort ?? "configured",
|
|
92
|
+
prompt: ""
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const criteria = context?.criteria !== undefined
|
|
96
|
+
? [...context.criteria]
|
|
97
|
+
: (options.args.criteria ?? []).map((id) => ({ id, description: id }));
|
|
27
98
|
const result = await runIndependentReview({
|
|
28
99
|
invocation: {
|
|
29
|
-
harness:
|
|
100
|
+
harness: target,
|
|
30
101
|
model: role?.model ?? options.model ?? "configured",
|
|
31
102
|
effort: role?.effort ?? options.effort ?? "configured",
|
|
32
103
|
packet: buildReviewPacket({
|
|
@@ -42,6 +113,17 @@ export async function runReviewCommand(options) {
|
|
|
42
113
|
reason: "missing-cli"
|
|
43
114
|
}))
|
|
44
115
|
});
|
|
116
|
+
// Evidence is only appended while the task is active: a completed record
|
|
117
|
+
// must stay exactly as it was verified.
|
|
118
|
+
if (options.tasks !== undefined &&
|
|
119
|
+
context !== undefined &&
|
|
120
|
+
context.active &&
|
|
121
|
+
result.results !== undefined) {
|
|
122
|
+
await options.tasks.recordEvidence(context.taskId, Object.fromEntries(result.results.map((item) => [
|
|
123
|
+
item.criterionId,
|
|
124
|
+
item.evidence.map((reference) => `review:${target}:${reference}`)
|
|
125
|
+
])));
|
|
126
|
+
}
|
|
45
127
|
const message = result.status === "PASS"
|
|
46
128
|
? "Independent review passed."
|
|
47
129
|
: result.status === "FAIL"
|
|
@@ -55,6 +137,11 @@ export async function runReviewCommand(options) {
|
|
|
55
137
|
`Status: ${result.status}`,
|
|
56
138
|
`Harness: ${result.harness}; model: ${result.model}; effort: ${result.effort}.`,
|
|
57
139
|
...(result.reason === undefined ? [] : [`Reason: ${result.reason}.`]),
|
|
140
|
+
...(result.status === "NOT_RUN"
|
|
141
|
+
? [
|
|
142
|
+
"Run: agent-ops doctor --check-auth to verify target authentication."
|
|
143
|
+
]
|
|
144
|
+
: []),
|
|
58
145
|
...(result.results === undefined
|
|
59
146
|
? []
|
|
60
147
|
: result.results.map((item) => `${item.criterionId}: ${item.status} [${item.evidence.join(", ")}]`)),
|
|
@@ -38,6 +38,9 @@ export async function runUpdateCommand(options) {
|
|
|
38
38
|
...(options.targetVersion === undefined
|
|
39
39
|
? {}
|
|
40
40
|
: { targetVersion: options.targetVersion }),
|
|
41
|
+
...(options.toolkitVersion === undefined
|
|
42
|
+
? {}
|
|
43
|
+
: { toolkitVersion: options.toolkitVersion }),
|
|
41
44
|
...(options.hookRuntimePath === undefined
|
|
42
45
|
? {}
|
|
43
46
|
: { hookRuntimePath: options.hookRuntimePath }),
|