@kylecheng3146/agent-ops 0.1.1 → 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
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
2
3
  import { homedir } from "node:os";
3
4
  import { join } from "node:path";
4
- import { createInterface } from "node:readline/promises";
5
5
  import { execFileSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
6
7
  import { commonHarnessAdapters } from "../../../runtime/src/install/harness.js";
8
+ import { hookRegistrationSatisfied, repositoryTrustStatus, smokeAvailabilityStatus } from "../../../runtime/src/install/probes.js";
9
+ import { parseInstallManifest } from "../../../runtime/src/fs/manifest.js";
7
10
  import { NpmRegistryClient } from "../../../runtime/src/registry/npm.js";
8
11
  import { TaskService } from "../../../runtime/src/task/service.js";
9
12
  import { FileTaskStore } from "../../../runtime/src/task/store.js";
10
- import { mergeConfigLayers } from "../../../runtime/src/config/merge.js";
11
- import { loadConfigFile } from "../../../runtime/src/config/load.js";
12
- import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
13
13
  import { FileTrustStore, calculateTrustBinding } from "../../../runtime/src/security/trust.js";
14
14
  import { localStatePaths } from "../../../runtime/src/security/permissions.js";
15
15
  import { sha256 } from "../../../runtime/src/fs/hash.js";
@@ -17,6 +17,10 @@ import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
17
17
  import { VerificationService } from "../../../runtime/src/verify/service.js";
18
18
  import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
19
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";
20
24
  import { createCommandRegistry } from "./commands/index.js";
21
25
  import { explainConfigCommand } from "./commands/config.js";
22
26
  import { runDoctorCommand } from "./commands/doctor.js";
@@ -28,263 +32,207 @@ import { runTrustCommand } from "./commands/trust.js";
28
32
  import { runVerifyCommand } from "./commands/verify.js";
29
33
  import { formatUpdatePlan, runUpdateCommand } from "./commands/update.js";
30
34
  import { errorEnvelope } from "./output.js";
31
- const CLI_VERSION = "0.1.0";
32
- const DEFAULT_CONFIG = {
33
- schemaVersion: 1,
34
- profiles: [],
35
- verification: { commands: [] },
36
- pathMappings: [],
37
- securityExceptions: []
38
- };
39
- function defaultConfigLayer() {
40
- return {
41
- source: "default",
42
- sourcePath: "built-in defaults",
43
- config: DEFAULT_CONFIG
44
- };
45
- }
46
- async function loadOptionalConfig(path) {
35
+ const HOOK_RUNTIME_PATH = fileURLToPath(new URL("./hook-entry.js", import.meta.url));
36
+ async function readOptionalJson(path) {
47
37
  try {
48
- return await loadConfigFile(path);
49
- }
50
- catch (error) {
51
- if (error instanceof AgentOpsError &&
52
- error.code === "CONFIG_READ_FAILED" &&
53
- typeof error.cause === "object" &&
54
- error.cause !== null &&
55
- "code" in error.cause &&
56
- error.cause.code === "ENOENT") {
57
- return null;
58
- }
59
- throw error;
60
- }
61
- }
62
- async function loadEffectiveConfig(root, scope) {
63
- const home = process.env.AGENT_OPS_HOME ?? homedir();
64
- const userPath = join(home, ".agent-ops", "config.json");
65
- const projectPath = join(root, ".agent-ops", "config.json");
66
- const layers = [defaultConfigLayer()];
67
- if (scope === "user") {
68
- const user = await loadOptionalConfig(userPath);
69
- if (user !== null) {
70
- layers.push({
71
- source: "user",
72
- sourcePath: user.sourcePath,
73
- config: user.config
74
- });
75
- }
76
- return mergeConfigLayers(layers);
77
- }
78
- if (projectPath !== userPath) {
79
- const user = await loadOptionalConfig(userPath);
80
- if (user !== null) {
81
- layers.push({
82
- source: "user",
83
- sourcePath: user.sourcePath,
84
- config: user.config
85
- });
86
- }
38
+ return JSON.parse(await readFile(path, "utf8"));
87
39
  }
88
- const project = await loadOptionalConfig(projectPath);
89
- if (project !== null) {
90
- layers.push({
91
- source: "project",
92
- sourcePath: project.sourcePath,
93
- config: project.config
94
- });
40
+ catch {
41
+ return null;
95
42
  }
96
- return mergeConfigLayers(layers);
97
43
  }
98
- async function repositoryTrust(root, config) {
99
- const home = process.env.AGENT_OPS_HOME ?? homedir();
100
- const state = localStatePaths(home);
101
- const remote = (() => {
102
- try {
103
- return execFileSync("git", ["config", "--get", "remote.origin.url"], {
104
- cwd: root,
105
- encoding: "utf8"
106
- }).trim();
107
- }
108
- catch {
109
- return `local:${root}`;
110
- }
111
- })();
44
+ async function installedHarness(root) {
112
45
  try {
113
- const binding = await calculateTrustBinding({
114
- repositoryPath: root,
115
- remoteUrl: remote,
116
- configHash: sha256(JSON.stringify(config)),
117
- runtimeHash: sha256(CLI_VERSION)
118
- });
119
- return (await new FileTrustStore(state.trustStore, state.anchorDirectory).status(binding)).status;
46
+ return parseInstallManifest(await readFile(join(root, ".agent-ops", "manifest.json"), "utf8")).harness;
120
47
  }
121
48
  catch {
122
- return "UNTRUSTED";
49
+ // ponytail: no readable manifest means demand hooks for both harnesses.
50
+ return "both";
123
51
  }
124
52
  }
125
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
+ });
126
59
  return await confirmPlan(formatInstallPlan(plan));
127
60
  }
128
61
  async function confirmPlan(text) {
129
62
  process.stdout.write(text);
130
- const prompt = createInterface({
131
- input: process.stdin,
132
- output: process.stdout
133
- });
134
- try {
135
- const answer = await prompt.question("Apply this installation plan? [y/N]: ");
136
- return ["y", "yes"].includes(answer.trim().toLowerCase());
137
- }
138
- finally {
139
- prompt.close();
140
- }
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);
141
72
  }
142
- process.exitCode = await runCli(process.argv.slice(2), {
143
- isTTY: process.stdin.isTTY === true && process.stdout.isTTY === true,
144
- input: process.stdin,
145
- output: process.stdout,
146
- writeStdout: (value) => process.stdout.write(value),
147
- writeStderr: (value) => process.stderr.write(value)
148
- }, {
149
- version: CLI_VERSION,
150
- registry: createCommandRegistry(Object.fromEntries([
151
- "init",
152
- "config",
153
- "trust",
154
- "doctor",
155
- "update",
156
- "uninstall",
157
- "task",
158
- "verify",
159
- "review"
160
- ].map((command) => [command, async (args) => {
161
- const root = args.scope === "user"
162
- ? process.env.AGENT_OPS_HOME ?? homedir()
163
- : process.cwd();
164
- const isTTY = !args.json &&
165
- process.stdin.isTTY === true &&
166
- process.stdout.isTTY === true;
167
- if (args.command === "init") {
168
- return await runInitCommand({
169
- args,
170
- root,
171
- adapters: commonHarnessAdapters(),
172
- isTTY,
173
- toolkitVersion: CLI_VERSION,
174
- confirm: async (plan) => await confirmInit(plan)
175
- });
176
- }
177
- if (args.command === "doctor") {
178
- return await runDoctorCommand({ root });
179
- }
180
- if (args.command === "uninstall") {
181
- return await runUninstallCommand({
182
- args,
183
- root,
184
- isTTY,
185
- confirm: async (plan) => await confirmPlan(formatUninstallPlan(plan))
186
- });
187
- }
188
- if (args.command === "update") {
189
- return await runUpdateCommand({
190
- args,
191
- root,
192
- adapters: commonHarnessAdapters(),
193
- registry: new NpmRegistryClient(),
194
- isTTY,
195
- confirm: async (plan) => await confirmPlan(formatUpdatePlan(plan)),
196
- ...(args.targetVersion === undefined
197
- ? {}
198
- : { targetVersion: args.targetVersion })
199
- });
200
- }
201
- const taskService = new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root));
202
- if (args.command === "task") {
203
- const sessionId = process.env.AGENT_OPS_SESSION_ID;
204
- return await runTaskCommand({
205
- args,
206
- service: taskService,
207
- ...(sessionId === undefined ? {} : { sessionId })
208
- });
209
- }
210
- if (args.command === "review") {
211
- return await runReviewCommand({
212
- args,
213
- authorized: args.yes
214
- });
215
- }
216
- if (args.command === "config") {
217
- return explainConfigCommand(await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project"));
218
- }
219
- if (args.command === "verify") {
220
- const merged = await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project");
221
- const config = merged.config;
222
- const trustStatus = await repositoryTrust(root, config);
223
- return await runVerifyCommand({
224
- args,
225
- taskService,
226
- 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,
227
102
  root,
228
- scope: args.scope === "user" ? "user" : "project",
229
- config,
230
- gitRunner: {
231
- run: async (gitArgs) => {
232
- try {
233
- return {
234
- exitCode: 0,
235
- stdout: execFileSync("git", [...gitArgs], {
236
- cwd: root,
237
- encoding: "buffer",
238
- stdio: ["ignore", "pipe", "ignore"]
239
- })
240
- };
241
- }
242
- catch (error) {
243
- const failure = error;
244
- return {
245
- exitCode: failure.status ?? 1,
246
- stdout: failure.stdout ?? new Uint8Array()
247
- };
248
- }
249
- }
250
- },
251
- 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,
252
172
  taskService,
253
- evidenceStore: new FileEvidenceStore(root, root),
254
- trusted: trustStatus === "TRUSTED"
255
- })
256
- });
257
- }
258
- if (args.command === "trust") {
259
- const config = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
260
- const state = localStatePaths(process.env.AGENT_OPS_HOME ?? homedir());
261
- const remote = (() => {
262
- try {
263
- return execFileSync("git", ["config", "--get", "remote.origin.url"], {
264
- cwd: root,
265
- encoding: "utf8"
266
- }).trim();
267
- }
268
- catch {
269
- return `local:${root}`;
270
- }
271
- })();
272
- const binding = await calculateTrustBinding({
273
- repositoryPath: root,
274
- remoteUrl: remote,
275
- configHash: sha256(JSON.stringify(config)),
276
- runtimeHash: sha256(CLI_VERSION)
277
- });
278
- return await runTrustCommand({
279
- action: args.action,
280
- yes: args.yes,
281
- isTTY,
282
- calculateBinding: async () => binding,
283
- presentBinding: async () => undefined,
284
- confirmGrant: async () => await confirmPlan(JSON.stringify(binding, null, 2)),
285
- store: new FileTrustStore(state.trustStore, state.anchorDirectory)
286
- });
287
- }
288
- return errorEnvelope("CLI_COMMAND_UNAVAILABLE", `Command is not implemented yet: ${args.command}`);
289
- }])))
290
- });
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", {