@kylecheng3146/agent-ops 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +88 -13
  2. package/dist/packages/cli/src/args.js +39 -6
  3. package/dist/packages/cli/src/bin.js +33 -11
  4. package/dist/packages/cli/src/cli.js +6 -1
  5. package/dist/packages/cli/src/commands/doctor.js +31 -9
  6. package/dist/packages/cli/src/commands/hook.js +18 -16
  7. package/dist/packages/cli/src/commands/init.js +9 -5
  8. package/dist/packages/cli/src/commands/review.js +5 -1
  9. package/dist/packages/cli/src/commands/uninstall.js +6 -5
  10. package/dist/packages/cli/src/commands/update.js +13 -5
  11. package/dist/packages/cli/src/context.js +9 -3
  12. package/dist/packages/cli/src/hook-process.js +109 -7
  13. package/dist/packages/cli/src/plan-output.js +9 -4
  14. package/dist/packages/cli/src/public-plan.js +62 -0
  15. package/dist/packages/cli/src/version.js +1 -1
  16. package/dist/packages/cli/src/wizard.js +27 -11
  17. package/dist/runtime/src/adapters/claude/config.js +0 -8
  18. package/dist/runtime/src/adapters/claude/events.js +26 -0
  19. package/dist/runtime/src/adapters/claude/output.js +6 -6
  20. package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
  21. package/dist/runtime/src/adapters/codex/config.js +29 -11
  22. package/dist/runtime/src/adapters/codex/events.js +26 -0
  23. package/dist/runtime/src/adapters/codex/output.js +10 -0
  24. package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
  25. package/dist/runtime/src/adapters/opencode/config.js +170 -0
  26. package/dist/runtime/src/adapters/opencode/events.js +49 -0
  27. package/dist/runtime/src/adapters/opencode/input.js +32 -0
  28. package/dist/runtime/src/adapters/opencode/output.js +23 -0
  29. package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
  30. package/dist/runtime/src/config/explain.js +7 -0
  31. package/dist/runtime/src/config/hash.js +24 -0
  32. package/dist/runtime/src/config/merge.js +6 -2
  33. package/dist/runtime/src/config/migrate.js +19 -4
  34. package/dist/runtime/src/contracts.js +10 -1
  35. package/dist/runtime/src/fs/manifest.js +32 -1
  36. package/dist/runtime/src/fs/transaction.js +14 -2
  37. package/dist/runtime/src/hooks/advisory.js +16 -0
  38. package/dist/runtime/src/hooks/stop-service.js +70 -0
  39. package/dist/runtime/src/hooks/stop-verify.js +4 -1
  40. package/dist/runtime/src/install/doctor.js +119 -9
  41. package/dist/runtime/src/install/harness.js +296 -35
  42. package/dist/runtime/src/install/hooks.js +22 -17
  43. package/dist/runtime/src/install/ownership.js +110 -34
  44. package/dist/runtime/src/install/plan.js +207 -28
  45. package/dist/runtime/src/install/probes.js +9 -43
  46. package/dist/runtime/src/install/profiles.js +8 -1
  47. package/dist/runtime/src/install/surface-inspection.js +296 -0
  48. package/dist/runtime/src/install/surfaces.js +11 -0
  49. package/dist/runtime/src/install/uninstall.js +10 -3
  50. package/dist/runtime/src/install/update.js +8 -2
  51. package/dist/runtime/src/schema/validate.js +37 -18
  52. package/dist/runtime/src/task/service.js +3 -3
  53. package/dist/runtime/src/task/store.js +12 -3
  54. package/dist/runtime/src/verify/command-executor.js +113 -0
  55. package/dist/runtime/src/verify/evidence.js +4 -24
  56. package/dist/runtime/src/verify/service.js +20 -92
  57. package/dist/runtime/src/verify/spawn.js +6 -1
  58. package/docs/en/guides/configuration.md +83 -0
  59. package/docs/en/guides/quickstart.md +9 -0
  60. package/docs/en/guides/security.md +5 -0
  61. package/docs/en/spec/README.md +9 -0
  62. package/docs/en/spec/harness-adapters.md +66 -2
  63. package/docs/en/spec/maintenance.md +11 -0
  64. package/docs/en/spec/review.md +11 -0
  65. package/docs/zh-TW/guides/configuration.md +77 -0
  66. package/docs/zh-TW/guides/quickstart.md +9 -0
  67. package/docs/zh-TW/guides/security.md +5 -0
  68. package/docs/zh-TW/spec/README.md +9 -0
  69. package/docs/zh-TW/spec/harness-adapters.md +57 -3
  70. package/docs/zh-TW/spec/maintenance.md +11 -1
  71. package/docs/zh-TW/spec/review.md +10 -0
  72. package/package.json +4 -2
  73. package/schemas/config.schema.json +55 -1
  74. package/schemas/manifest.schema.json +7 -2
  75. package/templates/common/AGENTS.block.md +2 -1
  76. package/templates/common/CLAUDE.block.md +2 -1
  77. package/dist/runtime/src/review/claude-runner.js +0 -4
  78. package/dist/runtime/src/review/codex-runner.js +0 -4
@@ -1,25 +1,27 @@
1
1
  import { applyUpdatePlan, createUpdatePlan } from "../../../../runtime/src/install/update.js";
2
2
  import { okEnvelope } from "../output.js";
3
3
  import { formatOperationPlan } from "../plan-output.js";
4
+ import { toPublicUpdatePlan } from "../public-plan.js";
4
5
  export function formatUpdatePlan(plan) {
5
6
  return formatOperationPlan({
6
7
  title: "Update plan",
7
8
  metadata: [
8
9
  `Target version: ${plan.targetVersion}`,
10
+ `Harness: ${plan.installation.harness.join(", ")}`,
9
11
  `Schema migrations: ${plan.migrationSteps.length === 0
10
12
  ? "none"
11
13
  : plan.migrationSteps
12
14
  .map(({ fromVersion, toVersion }) => `${fromVersion} -> ${toVersion}`)
13
15
  .join(", ")}`
14
16
  ],
15
- operations: plan.installation.operations
17
+ operations: toPublicUpdatePlan(plan).installation.operations
16
18
  });
17
19
  }
18
20
  function updateError(code, message, plan) {
19
21
  return {
20
22
  code,
21
23
  status: "error",
22
- data: { applied: false, plan, message },
24
+ data: { applied: false, plan: toPublicUpdatePlan(plan), message },
23
25
  errors: [{ code, message }]
24
26
  };
25
27
  }
@@ -27,6 +29,9 @@ export async function runUpdateCommand(options) {
27
29
  const plan = await createUpdatePlan({
28
30
  root: options.root,
29
31
  adapters: options.adapters,
32
+ ...(options.args.harness === undefined
33
+ ? {}
34
+ : { harness: options.args.harness }),
30
35
  ...(options.registry === undefined
31
36
  ? {}
32
37
  : { registry: options.registry }),
@@ -35,12 +40,15 @@ export async function runUpdateCommand(options) {
35
40
  : { targetVersion: options.targetVersion }),
36
41
  ...(options.hookRuntimePath === undefined
37
42
  ? {}
38
- : { hookRuntimePath: options.hookRuntimePath })
43
+ : { hookRuntimePath: options.hookRuntimePath }),
44
+ ...((options.hookTargets ?? options.args.hookTargets) === undefined
45
+ ? {}
46
+ : { hookTargets: options.hookTargets ?? options.args.hookTargets })
39
47
  });
40
48
  if (options.args.dryRun) {
41
49
  return okEnvelope("UPDATE_PLAN_READY", {
42
50
  applied: false,
43
- plan,
51
+ plan: toPublicUpdatePlan(plan),
44
52
  message: "Update plan calculated; no files were changed.",
45
53
  text: formatUpdatePlan(plan)
46
54
  });
@@ -55,7 +63,7 @@ export async function runUpdateCommand(options) {
55
63
  await applyUpdatePlan(options.root, plan);
56
64
  return okEnvelope("UPDATE_APPLIED", {
57
65
  applied: true,
58
- plan,
66
+ plan: toPublicUpdatePlan(plan),
59
67
  message: `Managed installation updated to ${plan.targetVersion}.`
60
68
  });
61
69
  }
@@ -1,16 +1,22 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
5
+ import { sha256 } from "../../../runtime/src/fs/hash.js";
4
6
  import { loadConfigFile } from "../../../runtime/src/config/load.js";
5
7
  import { mergeConfigLayers } from "../../../runtime/src/config/merge.js";
6
- import { sha256 } from "../../../runtime/src/fs/hash.js";
7
8
  import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
8
9
  import { localStatePaths } from "../../../runtime/src/security/permissions.js";
9
10
  import { calculateTrustBinding, FileTrustStore } from "../../../runtime/src/security/trust.js";
10
11
  export const DEFAULT_CONFIG = {
11
- schemaVersion: 1,
12
+ schemaVersion: 2,
12
13
  profiles: [],
13
14
  verification: { commands: [] },
15
+ features: {
16
+ stopVerification: {
17
+ enabled: false
18
+ }
19
+ },
14
20
  pathMappings: [],
15
21
  securityExceptions: []
16
22
  };
@@ -91,7 +97,7 @@ export async function repositoryTrust(root, config, cliVersion) {
91
97
  const binding = await calculateTrustBinding({
92
98
  repositoryPath: root,
93
99
  remoteUrl: repositoryRemoteUrl(root),
94
- configHash: sha256(JSON.stringify(config)),
100
+ configHash: calculateConfigHash(config),
95
101
  runtimeHash: sha256(cliVersion)
96
102
  });
97
103
  return (await new FileTrustStore(state.trustStore, state.anchorDirectory).status(binding)).status;
@@ -1,7 +1,15 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
4
+ import { claudeStopRecursionMarker } from "../../../runtime/src/adapters/claude/input.js";
5
+ import { HARNESS_IDS, harnessDescriptor } from "../../../runtime/src/install/harness.js";
6
+ import { STOP_VERIFICATION_ENV, StopVerificationService } from "../../../runtime/src/hooks/stop-service.js";
7
+ import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
1
8
  import { runHookCommand, HOOK_EVENTS } from "./commands/hook.js";
2
9
  import { loadEffectiveConfig, repositoryTrust } from "./context.js";
3
- const HARNESSES = new Set(["codex", "claude"]);
10
+ const HARNESSES = new Set(HARNESS_IDS);
4
11
  const MAX_HOOK_INPUT_BYTES = 1024 * 1024;
12
+ const execFile = promisify(execFileCallback);
5
13
  async function readStdin(stream) {
6
14
  const chunks = [];
7
15
  let total = 0;
@@ -17,28 +25,122 @@ async function readStdin(stream) {
17
25
  }
18
26
  return Buffer.concat(chunks, total).toString("utf8");
19
27
  }
28
+ function parseInput(source) {
29
+ try {
30
+ return JSON.parse(source);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ function defaultGitRunner(root) {
37
+ return {
38
+ run: async (args) => {
39
+ try {
40
+ const result = await execFile("git", [...args], {
41
+ cwd: root,
42
+ encoding: "buffer"
43
+ });
44
+ return {
45
+ exitCode: 0,
46
+ stdout: result.stdout
47
+ };
48
+ }
49
+ catch (error) {
50
+ const failure = error;
51
+ return {
52
+ exitCode: failure.status ?? 1,
53
+ stdout: failure.stdout ?? new Uint8Array()
54
+ };
55
+ }
56
+ }
57
+ };
58
+ }
59
+ function stopVerificationOptions(input) {
60
+ const nativeRecursion = input.harness === "claude" &&
61
+ claudeStopRecursionMarker(input.rawInput);
62
+ const recursionActive = nativeRecursion || process.env[STOP_VERIFICATION_ENV] === "1";
63
+ const configHash = calculateConfigHash(input.config);
64
+ return {
65
+ confirmedConfig: true,
66
+ trusted: input.trusted,
67
+ scopeMapped: true,
68
+ recursionMarker: recursionActive,
69
+ configHash,
70
+ verify: async () => await new StopVerificationService({
71
+ root: input.root,
72
+ config: input.config,
73
+ trusted: input.trusted,
74
+ gitRunner: input.gitRunner,
75
+ processRunner: input.processRunner,
76
+ recursionActive,
77
+ configHash
78
+ }).verify()
79
+ };
80
+ }
81
+ function shouldBuildStopVerification(harness, event, config, rawInput) {
82
+ if (event !== "Stop" || !config.features.stopVerification.enabled) {
83
+ return false;
84
+ }
85
+ let normalized;
86
+ try {
87
+ normalized = harnessDescriptor(harness).runtime.normalizeInput(rawInput);
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ if (normalized.event !== "stop") {
93
+ return false;
94
+ }
95
+ return harnessDescriptor(harness).control.registrations.some(({ capability, support }) => capability === "optional-stop-verify" && support !== "unsupported");
96
+ }
20
97
  /**
21
98
  * Runs one hook invocation. Always resolves to exit code 0: a hook that
22
99
  * cannot answer must never block the harness it advises.
23
100
  */
24
- export async function runHookProcess(argv, io, cliVersion) {
101
+ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
25
102
  const [harness, event] = argv;
26
103
  if (harness === undefined ||
27
104
  !HARNESSES.has(harness) ||
28
105
  event === undefined ||
29
106
  !HOOK_EVENTS.includes(event)) {
30
- io.writeStderr("Usage: agent-ops hook <codex|claude> <SessionStart|PreToolUse|Stop>\n");
107
+ io.writeStderr(`Usage: agent-ops hook <${HARNESS_IDS.join("|")}> <SessionStart|PreToolUse|Stop>\n`);
31
108
  return 0;
32
109
  }
33
110
  try {
34
- const root = process.cwd();
35
- const config = (await loadEffectiveConfig(root, "project")).config;
111
+ const root = dependencies.root ?? process.cwd();
112
+ const config = dependencies.loadConfig === undefined
113
+ ? (await loadEffectiveConfig(root, "project")).config
114
+ : await dependencies.loadConfig(root);
115
+ const trustStatus = dependencies.trust === undefined
116
+ ? await repositoryTrust(root, config, cliVersion)
117
+ : await dependencies.trust(root, config, cliVersion);
118
+ const rawInput = await readStdin(io.stdin);
119
+ const parsedInput = parseInput(rawInput);
120
+ const trusted = trustStatus === "TRUSTED";
121
+ const gitRunner = dependencies.gitRunner ?? defaultGitRunner(root);
122
+ const processRunner = dependencies.processRunner ?? new NodeVerificationProcessRunner();
123
+ const stopVerification = shouldBuildStopVerification(harness, event, config, parsedInput)
124
+ ? stopVerificationOptions({
125
+ harness: harness,
126
+ rawInput: parsedInput,
127
+ root,
128
+ config,
129
+ trusted,
130
+ gitRunner,
131
+ processRunner
132
+ })
133
+ : undefined;
36
134
  const output = await runHookCommand({
37
135
  harness: harness,
38
136
  event: event,
39
- stdin: await readStdin(io.stdin),
137
+ stdin: rawInput,
40
138
  config,
41
- trusted: (await repositoryTrust(root, config, cliVersion)) === "TRUSTED"
139
+ trusted,
140
+ ...(dependencies.advisory === undefined
141
+ ? {}
142
+ : { advisory: dependencies.advisory }),
143
+ ...(stopVerification === undefined ? {} : { stopVerification })
42
144
  });
43
145
  if (output.stdout.length > 0) {
44
146
  io.writeStdout(output.stdout);
@@ -18,10 +18,15 @@ export function formatOperationPlan(options) {
18
18
  for (const operation of options.operations) {
19
19
  lines.push(`- ${operation.kind} ${escapeTerminalText(operation.path)}`, ` expected: ${operation.expectedHash ?? "<absent>"}`);
20
20
  if (operation.kind === "write") {
21
- lines.push(" content:", ...operation.content
22
- .replace(/\n$/u, "")
23
- .split("\n")
24
- .map((line) => ` ${escapeTerminalText(line)}`));
21
+ if ("contentHash" in operation) {
22
+ lines.push(` content: <opaque ${escapeTerminalText(operation.contentHash)}>`, ` summary: ${escapeTerminalText(operation.summary)}`);
23
+ }
24
+ else {
25
+ lines.push(" content:", ...operation.content
26
+ .replace(/\n$/u, "")
27
+ .split("\n")
28
+ .map((line) => ` ${escapeTerminalText(line)}`));
29
+ }
25
30
  }
26
31
  }
27
32
  return `${lines.join("\n")}\n`;
@@ -0,0 +1,62 @@
1
+ import { redactSecrets } from "../../../runtime/src/security/redact.js";
2
+ import { sha256 } from "../../../runtime/src/fs/hash.js";
3
+ const OPAQUE_SUMMARY = "Opaque managed settings content withheld.";
4
+ function publicPath(path) {
5
+ return redactSecrets(path);
6
+ }
7
+ export function disclosureOf(operation) {
8
+ return operation.disclosure ?? "full";
9
+ }
10
+ export function toPublicOperation(operation) {
11
+ const path = publicPath(operation.path);
12
+ if (operation.kind === "remove") {
13
+ return {
14
+ kind: "remove",
15
+ path,
16
+ expectedHash: operation.expectedHash
17
+ };
18
+ }
19
+ if (disclosureOf(operation) === "opaque") {
20
+ return {
21
+ kind: "write",
22
+ path,
23
+ expectedHash: operation.expectedHash,
24
+ contentHash: sha256(operation.content),
25
+ summary: redactSecrets(OPAQUE_SUMMARY)
26
+ };
27
+ }
28
+ return {
29
+ kind: "write",
30
+ path,
31
+ expectedHash: operation.expectedHash,
32
+ content: operation.content
33
+ };
34
+ }
35
+ export function toPublicOperations(operations) {
36
+ return operations.map(toPublicOperation);
37
+ }
38
+ export function toPublicInstallPlan(plan) {
39
+ return {
40
+ scope: plan.scope,
41
+ harness: plan.harness,
42
+ profiles: plan.profiles,
43
+ capabilities: plan.capabilities,
44
+ manifest: plan.manifest,
45
+ operations: toPublicOperations(plan.operations)
46
+ };
47
+ }
48
+ export function toPublicUpdatePlan(plan) {
49
+ return {
50
+ targetVersion: plan.targetVersion,
51
+ migrationSteps: plan.migrationSteps,
52
+ installation: toPublicInstallPlan(plan.installation)
53
+ };
54
+ }
55
+ export function toPublicUninstallPlan(plan) {
56
+ return {
57
+ installed: plan.installed,
58
+ manifest: plan.manifest,
59
+ manifestHash: plan.manifestHash,
60
+ operations: toPublicOperations(plan.operations)
61
+ };
62
+ }
@@ -1,3 +1,3 @@
1
1
  // Single source of the published CLI version. Release preparation updates
2
2
  // this constant; bin.ts and the hook entry both read it from here.
3
- export const CLI_VERSION = "0.1.4";
3
+ export const CLI_VERSION = "0.1.5";
@@ -1,17 +1,14 @@
1
1
  import { CliArgumentError } from "./args.js";
2
+ import { HARNESS_IDS, resolveHarnessSelection } from "../../../runtime/src/install/harness.js";
2
3
  import { selectOption, selectOptions } from "./ui.js";
3
4
  const SCOPES = new Set(["project", "user"]);
4
- const HARNESSES = new Set(["both", "claude", "codex"]);
5
5
  const PROFILES = new Set(["advisory", "core", "guardrails"]);
6
+ const DEFAULT_HARNESS = [];
6
7
  const SCOPE_CHOICES = [
7
8
  { label: "project", value: "project" },
8
9
  { label: "user", value: "user" }
9
10
  ];
10
- const HARNESS_CHOICES = [
11
- { label: "both", value: "both" },
12
- { label: "claude", value: "claude" },
13
- { label: "codex", value: "codex" }
14
- ];
11
+ const HARNESS_CHOICES = HARNESS_IDS.map((id) => ({ label: id, value: id }));
15
12
  const PROFILE_CHOICES = [
16
13
  {
17
14
  label: "core",
@@ -26,10 +23,10 @@ const PROFILE_CHOICES = [
26
23
  {
27
24
  label: "guardrails",
28
25
  value: "guardrails",
29
- description: "Blocks high-confidence unsafe commands and enables optional Stop verification."
26
+ description: "Blocks high-confidence unsafe commands; Stop verification is a separate opt-in feature."
30
27
  }
31
28
  ];
32
- const WIZARD_SUBTITLE = "Safe setup for Codex + Claude Code with profile-driven rules, verification, and hooks.";
29
+ const WIZARD_SUBTITLE = "Safe setup for Codex, Claude Code, and opencode with profile-driven rules, verification, and hooks.";
33
30
  async function createPromptSession(io) {
34
31
  if (!io.isTTY) {
35
32
  throw new CliArgumentError("CLI_INTERACTIVE_REQUIRED", "Missing init choices require an interactive terminal.");
@@ -60,6 +57,16 @@ function selectValue(raw, fallback, allowed, label) {
60
57
  }
61
58
  return value;
62
59
  }
60
+ function selectHarness(raw) {
61
+ if (raw.trim() === "") {
62
+ throw new CliArgumentError("CLI_INVALID_VALUE", "Choose at least one harness.");
63
+ }
64
+ const selection = resolveHarnessSelection(raw);
65
+ if (selection === null) {
66
+ throw new CliArgumentError("CLI_INVALID_VALUE", `Harness must be a unique comma-separated list of ${HARNESS_IDS.join(", ")}.`);
67
+ }
68
+ return selection;
69
+ }
63
70
  function selectProfiles(raw) {
64
71
  const values = raw.trim() === ""
65
72
  ? ["core"]
@@ -81,14 +88,23 @@ export async function completeInitChoices(args, io) {
81
88
  if (!io.isTTY) {
82
89
  throw new CliArgumentError("CLI_INTERACTIVE_REQUIRED", "Non-interactive init requires --scope, --harness, and at least one --profile.");
83
90
  }
84
- if (io.input !== undefined && io.output !== undefined) {
91
+ const canUseRawSelectors = io.input !== undefined &&
92
+ io.output !== undefined &&
93
+ typeof io.input.setRawMode === "function" &&
94
+ io.input.isTTY === true;
95
+ if (canUseRawSelectors) {
85
96
  const selectorIo = {
86
97
  input: io.input,
87
98
  output: io.output
88
99
  };
89
100
  selectorIo.output.write(`${WIZARD_SUBTITLE}\n\n`);
90
101
  const scope = args.scope ?? await selectOption("Scope", SCOPE_CHOICES, selectorIo);
91
- const harness = args.harness ?? await selectOption("Harness", HARNESS_CHOICES, selectorIo);
102
+ const harness = args.harness ??
103
+ await selectOptions("Harness (multi-select: ↑↓ move, Space toggle, Enter confirm)", HARNESS_CHOICES, selectorIo, [...DEFAULT_HARNESS], {
104
+ selectAll: true,
105
+ selectAllLabel: "Select all",
106
+ selectAllDescription: "Install for every supported harness."
107
+ });
92
108
  const profiles = args.profiles.length > 0
93
109
  ? args.profiles
94
110
  : await selectOptions("Profiles (multi-select: ↑↓ move, Space toggle, Enter confirm)", PROFILE_CHOICES, selectorIo, [], {
@@ -108,7 +124,7 @@ export async function completeInitChoices(args, io) {
108
124
  const scope = args.scope ??
109
125
  selectValue(await session.question("Scope (project/user) [project]: "), "project", SCOPES, "scope");
110
126
  const harness = args.harness ??
111
- selectValue(await session.question("Harness (both/claude/codex) [both]: "), "both", HARNESSES, "harness");
127
+ selectHarness(await session.question(`Harness (${HARNESS_IDS.join(",")}): `));
112
128
  const profiles = args.profiles.length > 0
113
129
  ? args.profiles
114
130
  : selectProfiles(await session.question("Profiles (core,advisory,guardrails) [core]: "));
@@ -10,14 +10,6 @@ export function claudeSettingsTarget(scope) {
10
10
  requiresWorkspaceTrust: false
11
11
  };
12
12
  }
13
- export function claudeRoutingBlock() {
14
- return [
15
- "## Loop Engineering",
16
- "",
17
- "Load `.agent-ops/CLAUDE.md` as concise project context.",
18
- ""
19
- ].join("\n");
20
- }
21
13
  function commandHook(event, runtimePath) {
22
14
  return {
23
15
  type: "command",
@@ -6,3 +6,29 @@ export const CLAUDE_SUPPORTED_EVENTS = [
6
6
  export function claudeNonInteractiveTrust(printMode) {
7
7
  return printMode ? "dialog-skipped" : "interactive-dialog";
8
8
  }
9
+ export const CLAUDE_CAPABILITY_REGISTRATIONS = [
10
+ {
11
+ capability: "lifecycle-summary",
12
+ normalizedEvent: "session-start",
13
+ nativeEvent: "SessionStart",
14
+ surfaceId: "claude-settings",
15
+ support: "supported",
16
+ runtimeFailure: "fail-open"
17
+ },
18
+ {
19
+ capability: "command-policy",
20
+ normalizedEvent: "command",
21
+ nativeEvent: "PreToolUse",
22
+ surfaceId: "claude-settings",
23
+ support: "supported",
24
+ runtimeFailure: "fail-closed"
25
+ },
26
+ {
27
+ capability: "optional-stop-verify",
28
+ normalizedEvent: "stop",
29
+ nativeEvent: "Stop",
30
+ surfaceId: "claude-settings",
31
+ support: "supported",
32
+ runtimeFailure: "fail-open"
33
+ }
34
+ ];
@@ -6,6 +6,12 @@ function json(value) {
6
6
  };
7
7
  }
8
8
  export function claudeHookOutput(event, result) {
9
+ if (event === "Stop" && result.evidence !== undefined) {
10
+ return json({
11
+ systemMessage: `agent-ops: ${result.code}`,
12
+ evidence: result.evidence
13
+ });
14
+ }
9
15
  if (result.action === "continue" && result.status === "PASS") {
10
16
  return { exitCode: 0, stdout: "", stderr: "" };
11
17
  }
@@ -18,12 +24,6 @@ export function claudeHookOutput(event, result) {
18
24
  }
19
25
  });
20
26
  }
21
- if (event === "Stop" && result.action === "block") {
22
- return json({
23
- decision: "block",
24
- reason: result.code
25
- });
26
- }
27
27
  return json({
28
28
  systemMessage: `agent-ops: ${result.code}`
29
29
  });
@@ -0,0 +1,70 @@
1
+ export const CLAUDE_SURFACE_IDS = {
2
+ settings: "claude-settings",
3
+ projectLocal: "project-local",
4
+ userSettings: "user-settings",
5
+ plugin: "plugin"
6
+ };
7
+ export function claudeSurfaces(scope) {
8
+ if (scope === "project") {
9
+ return [
10
+ {
11
+ id: CLAUDE_SURFACE_IDS.settings,
12
+ path: ".claude/settings.json",
13
+ scope: "project",
14
+ access: "managed-default",
15
+ representation: "json"
16
+ },
17
+ {
18
+ id: CLAUDE_SURFACE_IDS.projectLocal,
19
+ path: ".claude/settings.local.json",
20
+ scope: "project",
21
+ access: "managed-opt-in",
22
+ representation: "json"
23
+ },
24
+ {
25
+ id: CLAUDE_SURFACE_IDS.userSettings,
26
+ path: "~/.claude/settings.json",
27
+ scope: "external",
28
+ access: "inspect-only",
29
+ representation: "json"
30
+ },
31
+ {
32
+ id: CLAUDE_SURFACE_IDS.plugin,
33
+ path: "~/.claude/plugins",
34
+ scope: "external",
35
+ access: "inspect-only",
36
+ representation: "javascript"
37
+ }
38
+ ];
39
+ }
40
+ return [
41
+ {
42
+ id: CLAUDE_SURFACE_IDS.settings,
43
+ path: ".claude/settings.json",
44
+ scope: "user",
45
+ access: "managed-default",
46
+ representation: "json"
47
+ },
48
+ {
49
+ id: CLAUDE_SURFACE_IDS.projectLocal,
50
+ path: "project/.claude/settings.local.json",
51
+ scope: "external",
52
+ access: "inspect-only",
53
+ representation: "json"
54
+ },
55
+ {
56
+ id: CLAUDE_SURFACE_IDS.userSettings,
57
+ path: "project/.claude/settings.json",
58
+ scope: "external",
59
+ access: "inspect-only",
60
+ representation: "json"
61
+ },
62
+ {
63
+ id: CLAUDE_SURFACE_IDS.plugin,
64
+ path: "~/.claude/plugins",
65
+ scope: "external",
66
+ access: "inspect-only",
67
+ representation: "javascript"
68
+ }
69
+ ];
70
+ }
@@ -1,10 +1,26 @@
1
1
  import { AgentOpsError } from "../../fs/paths.js";
2
- const COMMAND_PREFIX = "agent-ops hook codex ";
2
+ export const CODEX_MANAGED_MARKER = "--managed-by=agent-ops";
3
+ /**
4
+ * Releases up to 0.1.4 registered a bare `agent-ops` command resolved through
5
+ * PATH. Detection still recognizes it so an update replaces it instead of
6
+ * leaving the hijackable handler behind.
7
+ */
8
+ const LEGACY_COMMAND_PREFIX = "agent-ops hook codex ";
3
9
  function isRecord(value) {
4
10
  return typeof value === "object" && value !== null && !Array.isArray(value);
5
11
  }
6
- function commandHook(event) {
7
- const command = `${COMMAND_PREFIX}${event}`;
12
+ function assertRuntimePath(runtimePath) {
13
+ if (runtimePath.length === 0 ||
14
+ runtimePath.length > 4096 ||
15
+ runtimePath.includes("\0") ||
16
+ // The command is a single shell string, so a quote would break the
17
+ // argument boundary the surrounding quotes establish.
18
+ runtimePath.includes('"')) {
19
+ throw new AgentOpsError("CODEX_HOOK_PATH_INVALID", "Codex hook runtime path is invalid.");
20
+ }
21
+ }
22
+ function commandHook(event, runtimePath) {
23
+ const command = `node "${runtimePath}" codex ${event} ${CODEX_MANAGED_MARKER}`;
8
24
  return {
9
25
  type: "command",
10
26
  command,
@@ -13,10 +29,10 @@ function commandHook(event) {
13
29
  statusMessage: `Running agent-ops ${event}`
14
30
  };
15
31
  }
16
- function matcherGroup(event) {
32
+ function matcherGroup(event, runtimePath) {
17
33
  return {
18
34
  ...(event === "PreToolUse" ? { matcher: "^Bash$" } : {}),
19
- hooks: [commandHook(event)]
35
+ hooks: [commandHook(event, runtimePath)]
20
36
  };
21
37
  }
22
38
  export function codexHookTarget(scope) {
@@ -26,26 +42,28 @@ export function codexHookTarget(scope) {
26
42
  requiresProjectTrust: scope === "project"
27
43
  };
28
44
  }
29
- export function buildCodexHookConfig(capabilities) {
45
+ export function buildCodexHookConfig(capabilities, runtimePath) {
46
+ assertRuntimePath(runtimePath);
30
47
  const hooks = {};
31
48
  if (capabilities.includes("lifecycle-summary")) {
32
- hooks.SessionStart = [matcherGroup("SessionStart")];
49
+ hooks.SessionStart = [matcherGroup("SessionStart", runtimePath)];
33
50
  }
34
51
  if (capabilities.includes("command-policy")) {
35
- hooks.PreToolUse = [matcherGroup("PreToolUse")];
52
+ hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
36
53
  }
37
54
  if (capabilities.includes("optional-stop-verify")) {
38
- hooks.Stop = [matcherGroup("Stop")];
55
+ hooks.Stop = [matcherGroup("Stop", runtimePath)];
39
56
  }
40
57
  return {
41
- description: "Portable agent-ops lifecycle hooks.",
58
+ description: "agent-ops lifecycle hooks.",
42
59
  hooks
43
60
  };
44
61
  }
45
62
  function isOwnedHandler(hook) {
46
63
  return (isRecord(hook) &&
47
64
  typeof hook.command === "string" &&
48
- hook.command.startsWith(COMMAND_PREFIX));
65
+ (hook.command.includes(CODEX_MANAGED_MARKER) ||
66
+ hook.command.startsWith(LEGACY_COMMAND_PREFIX)));
49
67
  }
50
68
  function withoutOwnedHandlers(value) {
51
69
  if (!isRecord(value) || !Array.isArray(value.hooks)) {
@@ -6,3 +6,29 @@ export const CODEX_SUPPORTED_EVENTS = [
6
6
  export function codexMatcherSupport(event) {
7
7
  return event === "PreToolUse" ? "tool-name" : "unsupported";
8
8
  }
9
+ export const CODEX_CAPABILITY_REGISTRATIONS = [
10
+ {
11
+ capability: "lifecycle-summary",
12
+ normalizedEvent: "session-start",
13
+ nativeEvent: "SessionStart",
14
+ surfaceId: "codex-hooks",
15
+ support: "supported",
16
+ runtimeFailure: "fail-open"
17
+ },
18
+ {
19
+ capability: "command-policy",
20
+ normalizedEvent: "command",
21
+ nativeEvent: "PreToolUse",
22
+ surfaceId: "codex-hooks",
23
+ support: "unknown",
24
+ runtimeFailure: "native-unknown"
25
+ },
26
+ {
27
+ capability: "optional-stop-verify",
28
+ normalizedEvent: "stop",
29
+ nativeEvent: "Stop",
30
+ surfaceId: "codex-hooks",
31
+ support: "unsupported",
32
+ runtimeFailure: "fail-open"
33
+ }
34
+ ];