@kylecheng3146/agent-ops 0.1.2 → 0.1.3

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 CHANGED
@@ -94,8 +94,17 @@ node dist/packages/cli/src/bin.js update --dry-run --json
94
94
  node dist/packages/cli/src/bin.js uninstall --dry-run --json
95
95
  ```
96
96
 
97
- The commands after `init --yes` are post-apply operations; `doctor` may report
98
- an unknown probe status until a repository-specific verification setup exists.
97
+ The commands after `init --yes` are post-apply operations. `doctor` reports
98
+ `UNKNOWN` for a probe that has nothing to verify yet: `repository-trust` until
99
+ `trust grant` runs, and `smoke-availability` until the configuration declares a
100
+ verification command.
101
+
102
+ Installing the `advisory` or `guardrails` profile also registers lifecycle
103
+ hooks in `.claude/settings.json` and `.codex/hooks.json`. Only agent-ops owned
104
+ handlers are added, foreign settings in those files are preserved, and
105
+ `uninstall` removes exactly the handlers it registered. The hooks call
106
+ `agent-ops hook <harness> <event>`, which always exits 0 so a toolkit failure
107
+ can never block the harness.
99
108
 
100
109
  For a full command reference, run `agent-ops --help`. The `task`, `verify`, and
101
110
  `review` commands support acceptance tracking and independent verification when
@@ -2,17 +2,14 @@
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { createInterface } from "node:readline/promises";
6
5
  import { execFileSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
7
  import { commonHarnessAdapters } 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";
11
11
  import { TaskService } from "../../../runtime/src/task/service.js";
12
12
  import { FileTaskStore } from "../../../runtime/src/task/store.js";
13
- import { mergeConfigLayers } from "../../../runtime/src/config/merge.js";
14
- import { loadConfigFile } from "../../../runtime/src/config/load.js";
15
- import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
16
13
  import { FileTrustStore, calculateTrustBinding } from "../../../runtime/src/security/trust.js";
17
14
  import { localStatePaths } from "../../../runtime/src/security/permissions.js";
18
15
  import { sha256 } from "../../../runtime/src/fs/hash.js";
@@ -20,6 +17,10 @@ import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
20
17
  import { VerificationService } from "../../../runtime/src/verify/service.js";
21
18
  import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
22
19
  import { runCli } from "./cli.js";
20
+ import { loadEffectiveConfig, repositoryTrust } from "./context.js";
21
+ import { runHookProcess } from "./hook-process.js";
22
+ import { selectYesNo, writeBanner } from "./ui.js";
23
+ import { CLI_VERSION } from "./version.js";
23
24
  import { createCommandRegistry } from "./commands/index.js";
24
25
  import { explainConfigCommand } from "./commands/config.js";
25
26
  import { runDoctorCommand } from "./commands/doctor.js";
@@ -31,73 +32,7 @@ import { runTrustCommand } from "./commands/trust.js";
31
32
  import { runVerifyCommand } from "./commands/verify.js";
32
33
  import { formatUpdatePlan, runUpdateCommand } from "./commands/update.js";
33
34
  import { errorEnvelope } from "./output.js";
34
- const CLI_VERSION = "0.1.2";
35
- const DEFAULT_CONFIG = {
36
- schemaVersion: 1,
37
- profiles: [],
38
- verification: { commands: [] },
39
- pathMappings: [],
40
- securityExceptions: []
41
- };
42
- function defaultConfigLayer() {
43
- return {
44
- source: "default",
45
- sourcePath: "built-in defaults",
46
- config: DEFAULT_CONFIG
47
- };
48
- }
49
- async function loadOptionalConfig(path) {
50
- try {
51
- return await loadConfigFile(path);
52
- }
53
- catch (error) {
54
- if (error instanceof AgentOpsError &&
55
- error.code === "CONFIG_READ_FAILED" &&
56
- typeof error.cause === "object" &&
57
- error.cause !== null &&
58
- "code" in error.cause &&
59
- error.cause.code === "ENOENT") {
60
- return null;
61
- }
62
- throw error;
63
- }
64
- }
65
- async function loadEffectiveConfig(root, scope) {
66
- const home = process.env.AGENT_OPS_HOME ?? homedir();
67
- const userPath = join(home, ".agent-ops", "config.json");
68
- const projectPath = join(root, ".agent-ops", "config.json");
69
- const layers = [defaultConfigLayer()];
70
- if (scope === "user") {
71
- const user = await loadOptionalConfig(userPath);
72
- if (user !== null) {
73
- layers.push({
74
- source: "user",
75
- sourcePath: user.sourcePath,
76
- config: user.config
77
- });
78
- }
79
- return mergeConfigLayers(layers);
80
- }
81
- if (projectPath !== userPath) {
82
- const user = await loadOptionalConfig(userPath);
83
- if (user !== null) {
84
- layers.push({
85
- source: "user",
86
- sourcePath: user.sourcePath,
87
- config: user.config
88
- });
89
- }
90
- }
91
- const project = await loadOptionalConfig(projectPath);
92
- if (project !== null) {
93
- layers.push({
94
- source: "project",
95
- sourcePath: project.sourcePath,
96
- config: project.config
97
- });
98
- }
99
- return mergeConfigLayers(layers);
100
- }
35
+ const HOOK_RUNTIME_PATH = fileURLToPath(new URL("./hook-entry.js", import.meta.url));
101
36
  async function readOptionalJson(path) {
102
37
  try {
103
38
  return JSON.parse(await readFile(path, "utf8"));
@@ -115,209 +50,189 @@ async function installedHarness(root) {
115
50
  return "both";
116
51
  }
117
52
  }
118
- async function repositoryTrust(root, config) {
119
- const home = process.env.AGENT_OPS_HOME ?? homedir();
120
- const state = localStatePaths(home);
121
- const remote = (() => {
122
- try {
123
- return execFileSync("git", ["config", "--get", "remote.origin.url"], {
124
- cwd: root,
125
- encoding: "utf8"
126
- }).trim();
127
- }
128
- catch {
129
- return `local:${root}`;
130
- }
131
- })();
132
- try {
133
- const binding = await calculateTrustBinding({
134
- repositoryPath: root,
135
- remoteUrl: remote,
136
- configHash: sha256(JSON.stringify(config)),
137
- runtimeHash: sha256(CLI_VERSION)
138
- });
139
- return (await new FileTrustStore(state.trustStore, state.anchorDirectory).status(binding)).status;
140
- }
141
- catch {
142
- return "UNTRUSTED";
143
- }
144
- }
145
53
  async function confirmInit(plan) {
54
+ writeBanner({
55
+ isTTY: process.stdout.isTTY === true,
56
+ columns: process.stdout.columns,
57
+ write: (value) => process.stdout.write(value)
58
+ });
146
59
  return await confirmPlan(formatInstallPlan(plan));
147
60
  }
148
61
  async function confirmPlan(text) {
149
62
  process.stdout.write(text);
150
- const prompt = createInterface({
151
- input: process.stdin,
152
- output: process.stdout
153
- });
154
- try {
155
- const answer = await prompt.question("Apply this installation plan? [y/N]: ");
156
- return ["y", "yes"].includes(answer.trim().toLowerCase());
157
- }
158
- finally {
159
- prompt.close();
160
- }
63
+ return await selectYesNo("Apply this installation plan?", { input: process.stdin, output: process.stdout }, false);
64
+ }
65
+ const argv = process.argv.slice(2);
66
+ if (argv[0] === "hook") {
67
+ process.exitCode = await runHookProcess(argv.slice(1), {
68
+ stdin: process.stdin,
69
+ writeStdout: (value) => process.stdout.write(value),
70
+ writeStderr: (value) => process.stderr.write(value)
71
+ }, CLI_VERSION);
161
72
  }
162
- process.exitCode = await runCli(process.argv.slice(2), {
163
- isTTY: process.stdin.isTTY === true && process.stdout.isTTY === true,
164
- input: process.stdin,
165
- output: process.stdout,
166
- writeStdout: (value) => process.stdout.write(value),
167
- writeStderr: (value) => process.stderr.write(value)
168
- }, {
169
- version: CLI_VERSION,
170
- registry: createCommandRegistry(Object.fromEntries([
171
- "init",
172
- "config",
173
- "trust",
174
- "doctor",
175
- "update",
176
- "uninstall",
177
- "task",
178
- "verify",
179
- "review"
180
- ].map((command) => [command, async (args) => {
181
- const root = args.scope === "user"
182
- ? process.env.AGENT_OPS_HOME ?? homedir()
183
- : process.cwd();
184
- const isTTY = !args.json &&
185
- process.stdin.isTTY === true &&
186
- process.stdout.isTTY === true;
187
- if (args.command === "init") {
188
- return await runInitCommand({
189
- args,
190
- root,
191
- adapters: commonHarnessAdapters(),
192
- isTTY,
193
- toolkitVersion: CLI_VERSION,
194
- confirm: async (plan) => await confirmInit(plan)
195
- });
196
- }
197
- if (args.command === "doctor") {
198
- const config = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
199
- return await runDoctorCommand({
200
- root,
201
- probes: {
202
- hookRegistration: async () => hookRegistrationSatisfied({
203
- harness: await installedHarness(root),
204
- profiles: config.profiles,
205
- claudeSettings: await readOptionalJson(join(root, ".claude", "settings.json")),
206
- codexHooks: await readOptionalJson(join(root, ".codex", "hooks.json"))
207
- }),
208
- repositoryTrust: async () => repositoryTrustStatus(await repositoryTrust(root, config)),
209
- smokeAvailability: () => smokeAvailabilityStatus(config)
210
- }
211
- });
212
- }
213
- if (args.command === "uninstall") {
214
- return await runUninstallCommand({
215
- args,
216
- root,
217
- isTTY,
218
- confirm: async (plan) => await confirmPlan(formatUninstallPlan(plan))
219
- });
220
- }
221
- if (args.command === "update") {
222
- return await runUpdateCommand({
223
- args,
224
- root,
225
- adapters: commonHarnessAdapters(),
226
- registry: new NpmRegistryClient(),
227
- isTTY,
228
- confirm: async (plan) => await confirmPlan(formatUpdatePlan(plan)),
229
- ...(args.targetVersion === undefined
230
- ? {}
231
- : { targetVersion: args.targetVersion })
232
- });
233
- }
234
- const taskService = new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root));
235
- if (args.command === "task") {
236
- const sessionId = process.env.AGENT_OPS_SESSION_ID;
237
- return await runTaskCommand({
238
- args,
239
- service: taskService,
240
- ...(sessionId === undefined ? {} : { sessionId })
241
- });
242
- }
243
- if (args.command === "review") {
244
- return await runReviewCommand({
245
- args,
246
- authorized: args.yes
247
- });
248
- }
249
- if (args.command === "config") {
250
- return explainConfigCommand(await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project"));
251
- }
252
- if (args.command === "verify") {
253
- const merged = await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project");
254
- const config = merged.config;
255
- const trustStatus = await repositoryTrust(root, config);
256
- return await runVerifyCommand({
257
- args,
258
- taskService,
259
- service: new VerificationService({
73
+ else {
74
+ process.exitCode = await runCli(argv, {
75
+ isTTY: process.stdin.isTTY === true && process.stdout.isTTY === true,
76
+ input: process.stdin,
77
+ output: process.stdout,
78
+ writeStdout: (value) => process.stdout.write(value),
79
+ writeStderr: (value) => process.stderr.write(value)
80
+ }, {
81
+ version: CLI_VERSION,
82
+ registry: createCommandRegistry(Object.fromEntries([
83
+ "init",
84
+ "config",
85
+ "trust",
86
+ "doctor",
87
+ "update",
88
+ "uninstall",
89
+ "task",
90
+ "verify",
91
+ "review"
92
+ ].map((command) => [command, async (args) => {
93
+ const root = args.scope === "user"
94
+ ? process.env.AGENT_OPS_HOME ?? homedir()
95
+ : process.cwd();
96
+ const isTTY = !args.json &&
97
+ process.stdin.isTTY === true &&
98
+ process.stdout.isTTY === true;
99
+ if (args.command === "init") {
100
+ return await runInitCommand({
101
+ args,
260
102
  root,
261
- scope: args.scope === "user" ? "user" : "project",
262
- config,
263
- gitRunner: {
264
- run: async (gitArgs) => {
265
- try {
266
- return {
267
- exitCode: 0,
268
- stdout: execFileSync("git", [...gitArgs], {
269
- cwd: root,
270
- encoding: "buffer",
271
- stdio: ["ignore", "pipe", "ignore"]
272
- })
273
- };
274
- }
275
- catch (error) {
276
- const failure = error;
277
- return {
278
- exitCode: failure.status ?? 1,
279
- stdout: failure.stdout ?? new Uint8Array()
280
- };
281
- }
282
- }
283
- },
284
- processRunner: new NodeVerificationProcessRunner(),
103
+ adapters: commonHarnessAdapters(),
104
+ isTTY,
105
+ toolkitVersion: CLI_VERSION,
106
+ hookRuntimePath: HOOK_RUNTIME_PATH,
107
+ confirm: async (plan) => await confirmInit(plan)
108
+ });
109
+ }
110
+ if (args.command === "doctor") {
111
+ const config = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
112
+ return await runDoctorCommand({
113
+ root,
114
+ probes: {
115
+ hookRegistration: async () => hookRegistrationSatisfied({
116
+ harness: await installedHarness(root),
117
+ profiles: config.profiles,
118
+ claudeSettings: await readOptionalJson(join(root, ".claude", "settings.json")),
119
+ codexHooks: await readOptionalJson(join(root, ".codex", "hooks.json"))
120
+ }),
121
+ repositoryTrust: async () => repositoryTrustStatus(await repositoryTrust(root, config, CLI_VERSION)),
122
+ smokeAvailability: () => smokeAvailabilityStatus(config)
123
+ }
124
+ });
125
+ }
126
+ if (args.command === "uninstall") {
127
+ return await runUninstallCommand({
128
+ args,
129
+ root,
130
+ isTTY,
131
+ confirm: async (plan) => await confirmPlan(formatUninstallPlan(plan))
132
+ });
133
+ }
134
+ if (args.command === "update") {
135
+ return await runUpdateCommand({
136
+ args,
137
+ root,
138
+ adapters: commonHarnessAdapters(),
139
+ registry: new NpmRegistryClient(),
140
+ isTTY,
141
+ hookRuntimePath: HOOK_RUNTIME_PATH,
142
+ confirm: async (plan) => await confirmPlan(formatUpdatePlan(plan)),
143
+ ...(args.targetVersion === undefined
144
+ ? {}
145
+ : { targetVersion: args.targetVersion })
146
+ });
147
+ }
148
+ const taskService = new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root));
149
+ if (args.command === "task") {
150
+ const sessionId = process.env.AGENT_OPS_SESSION_ID;
151
+ return await runTaskCommand({
152
+ args,
153
+ service: taskService,
154
+ ...(sessionId === undefined ? {} : { sessionId })
155
+ });
156
+ }
157
+ if (args.command === "review") {
158
+ return await runReviewCommand({
159
+ args,
160
+ authorized: args.yes
161
+ });
162
+ }
163
+ if (args.command === "config") {
164
+ return explainConfigCommand(await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project"));
165
+ }
166
+ if (args.command === "verify") {
167
+ const merged = await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project");
168
+ const config = merged.config;
169
+ const trustStatus = await repositoryTrust(root, config, CLI_VERSION);
170
+ return await runVerifyCommand({
171
+ args,
285
172
  taskService,
286
- evidenceStore: new FileEvidenceStore(root, root),
287
- trusted: trustStatus === "TRUSTED"
288
- })
289
- });
290
- }
291
- if (args.command === "trust") {
292
- const config = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
293
- const state = localStatePaths(process.env.AGENT_OPS_HOME ?? homedir());
294
- const remote = (() => {
295
- try {
296
- return execFileSync("git", ["config", "--get", "remote.origin.url"], {
297
- cwd: root,
298
- encoding: "utf8"
299
- }).trim();
300
- }
301
- catch {
302
- return `local:${root}`;
303
- }
304
- })();
305
- const binding = await calculateTrustBinding({
306
- repositoryPath: root,
307
- remoteUrl: remote,
308
- configHash: sha256(JSON.stringify(config)),
309
- runtimeHash: sha256(CLI_VERSION)
310
- });
311
- return await runTrustCommand({
312
- action: args.action,
313
- yes: args.yes,
314
- isTTY,
315
- calculateBinding: async () => binding,
316
- presentBinding: async () => undefined,
317
- confirmGrant: async () => await confirmPlan(JSON.stringify(binding, null, 2)),
318
- store: new FileTrustStore(state.trustStore, state.anchorDirectory)
319
- });
320
- }
321
- return errorEnvelope("CLI_COMMAND_UNAVAILABLE", `Command is not implemented yet: ${args.command}`);
322
- }])))
323
- });
173
+ service: new VerificationService({
174
+ root,
175
+ scope: args.scope === "user" ? "user" : "project",
176
+ config,
177
+ gitRunner: {
178
+ run: async (gitArgs) => {
179
+ try {
180
+ return {
181
+ exitCode: 0,
182
+ stdout: execFileSync("git", [...gitArgs], {
183
+ cwd: root,
184
+ encoding: "buffer",
185
+ stdio: ["ignore", "pipe", "ignore"]
186
+ })
187
+ };
188
+ }
189
+ catch (error) {
190
+ const failure = error;
191
+ return {
192
+ exitCode: failure.status ?? 1,
193
+ stdout: failure.stdout ?? new Uint8Array()
194
+ };
195
+ }
196
+ }
197
+ },
198
+ processRunner: new NodeVerificationProcessRunner(),
199
+ taskService,
200
+ evidenceStore: new FileEvidenceStore(root, root),
201
+ trusted: trustStatus === "TRUSTED"
202
+ })
203
+ });
204
+ }
205
+ if (args.command === "trust") {
206
+ const config = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
207
+ const state = localStatePaths(process.env.AGENT_OPS_HOME ?? homedir());
208
+ const remote = (() => {
209
+ try {
210
+ return execFileSync("git", ["config", "--get", "remote.origin.url"], {
211
+ cwd: root,
212
+ encoding: "utf8"
213
+ }).trim();
214
+ }
215
+ catch {
216
+ return `local:${root}`;
217
+ }
218
+ })();
219
+ const binding = await calculateTrustBinding({
220
+ repositoryPath: root,
221
+ remoteUrl: remote,
222
+ configHash: sha256(JSON.stringify(config)),
223
+ runtimeHash: sha256(CLI_VERSION)
224
+ });
225
+ return await runTrustCommand({
226
+ action: args.action,
227
+ yes: args.yes,
228
+ isTTY,
229
+ calculateBinding: async () => binding,
230
+ presentBinding: async () => undefined,
231
+ confirmGrant: async () => await confirmPlan(JSON.stringify(binding, null, 2)),
232
+ store: new FileTrustStore(state.trustStore, state.anchorDirectory)
233
+ });
234
+ }
235
+ return errorEnvelope("CLI_COMMAND_UNAVAILABLE", `Command is not implemented yet: ${args.command}`);
236
+ }])))
237
+ });
238
+ }
@@ -0,0 +1,42 @@
1
+ import { normalizeClaudeHookInput } from "../../../../runtime/src/adapters/claude/input.js";
2
+ import { claudeHookOutput } from "../../../../runtime/src/adapters/claude/output.js";
3
+ import { normalizeCodexHookInput } from "../../../../runtime/src/adapters/codex/input.js";
4
+ import { codexHookOutput } from "../../../../runtime/src/adapters/codex/output.js";
5
+ import { dispatchHookEvent } from "../../../../runtime/src/hooks/dispatch.js";
6
+ import { resolveProfiles } from "../../../../runtime/src/install/profiles.js";
7
+ export const HOOK_EVENTS = [
8
+ "SessionStart",
9
+ "PreToolUse",
10
+ "Stop"
11
+ ];
12
+ /**
13
+ * Hooks are advisory infrastructure: every failure path stays fail-open with
14
+ * exit code 0 so a broken toolkit can never wedge the harness.
15
+ */
16
+ export async function runHookCommand(options) {
17
+ try {
18
+ const { capabilities } = resolveProfiles(options.config.profiles);
19
+ let input;
20
+ try {
21
+ input = JSON.parse(options.stdin);
22
+ }
23
+ catch {
24
+ return { exitCode: 0, stdout: "", stderr: "" };
25
+ }
26
+ const event = options.harness === "claude"
27
+ ? normalizeClaudeHookInput(input)
28
+ : normalizeCodexHookInput(input);
29
+ const result = await dispatchHookEvent(event, {
30
+ capabilities,
31
+ trusted: options.trusted
32
+ });
33
+ if (options.harness === "claude") {
34
+ return claudeHookOutput(options.event, result);
35
+ }
36
+ const codex = codexHookOutput(options.event, result);
37
+ return { exitCode: 0, stdout: codex.stdout, stderr: "" };
38
+ }
39
+ catch {
40
+ return { exitCode: 0, stdout: "", stderr: "" };
41
+ }
42
+ }
@@ -38,7 +38,10 @@ export async function runInitCommand(options) {
38
38
  adapters: options.adapters,
39
39
  ...(options.toolkitVersion === undefined
40
40
  ? {}
41
- : { toolkitVersion: options.toolkitVersion })
41
+ : { toolkitVersion: options.toolkitVersion }),
42
+ ...(options.hookRuntimePath === undefined
43
+ ? {}
44
+ : { hookRuntimePath: options.hookRuntimePath })
42
45
  });
43
46
  if (args.dryRun) {
44
47
  return okEnvelope("INIT_PLAN_READY", {
@@ -32,7 +32,10 @@ export async function runUpdateCommand(options) {
32
32
  : { registry: options.registry }),
33
33
  ...(options.targetVersion === undefined
34
34
  ? {}
35
- : { targetVersion: options.targetVersion })
35
+ : { targetVersion: options.targetVersion }),
36
+ ...(options.hookRuntimePath === undefined
37
+ ? {}
38
+ : { hookRuntimePath: options.hookRuntimePath })
36
39
  });
37
40
  if (options.args.dryRun) {
38
41
  return okEnvelope("UPDATE_PLAN_READY", {