@kylecheng3146/agent-ops 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +82 -6
  2. package/dist/packages/cli/src/args.js +1 -1
  3. package/dist/packages/cli/src/bin.js +2 -0
  4. package/dist/packages/cli/src/cli.js +1 -1
  5. package/dist/packages/cli/src/codex-loop-process.js +70 -0
  6. package/dist/packages/cli/src/commands/hook.js +16 -1
  7. package/dist/packages/cli/src/commands/update.js +3 -0
  8. package/dist/packages/cli/src/context.js +60 -0
  9. package/dist/packages/cli/src/hook-process.js +128 -15
  10. package/dist/packages/cli/src/loop-entry.js +8 -0
  11. package/dist/packages/cli/src/version.js +1 -1
  12. package/dist/packages/cli/src/wizard.js +9 -4
  13. package/dist/runtime/src/adapters/claude/config.js +57 -11
  14. package/dist/runtime/src/adapters/claude/events.js +7 -0
  15. package/dist/runtime/src/adapters/claude/output.js +2 -1
  16. package/dist/runtime/src/adapters/codex/config.js +39 -4
  17. package/dist/runtime/src/adapters/codex/events.js +7 -0
  18. package/dist/runtime/src/fs/managed-block.js +35 -18
  19. package/dist/runtime/src/hooks/codex-loop.js +439 -0
  20. package/dist/runtime/src/install/codex-loop.js +139 -0
  21. package/dist/runtime/src/install/doctor.js +66 -8
  22. package/dist/runtime/src/install/harness.js +8 -10
  23. package/dist/runtime/src/install/ownership.js +37 -2
  24. package/dist/runtime/src/install/plan.js +70 -4
  25. package/dist/runtime/src/install/profiles.js +5 -3
  26. package/dist/runtime/src/install/uninstall.js +1 -1
  27. package/dist/runtime/src/install/update.js +5 -1
  28. package/dist/runtime/src/logging/local-log.js +25 -0
  29. package/dist/runtime/src/schema/validate.js +8 -1
  30. package/docs/en/guides/configuration.md +78 -2
  31. package/docs/en/spec/harness-adapters.md +50 -12
  32. package/docs/zh-TW/guides/configuration.md +73 -5
  33. package/docs/zh-TW/spec/harness-adapters.md +44 -12
  34. package/package.json +1 -1
  35. package/schemas/config.schema.json +1 -1
  36. package/schemas/manifest.schema.json +12 -1
@@ -1,4 +1,17 @@
1
1
  import { AgentOpsError } from "../../fs/paths.js";
2
+ const CLAUDE_LOOP_EVENTS = [
3
+ "SessionStart",
4
+ "UserPromptSubmit",
5
+ "PreToolUse",
6
+ "PermissionRequest",
7
+ "PostToolUse",
8
+ "PreCompact",
9
+ "PostCompact",
10
+ "SubagentStart",
11
+ "SubagentStop"
12
+ ];
13
+ const CLAUDE_HOOK_MARKER = "--managed-by=agent-ops";
14
+ const CLAUDE_LOOP_LAUNCHER = "${CLAUDE_PROJECT_DIR}/.claude/hooks/agent-ops-loop.sh";
2
15
  export function claudeSettingsTarget(scope) {
3
16
  return scope === "project"
4
17
  ? {
@@ -18,7 +31,7 @@ function commandHook(event, runtimePath) {
18
31
  runtimePath,
19
32
  "claude",
20
33
  event,
21
- "--managed-by=agent-ops"
34
+ CLAUDE_HOOK_MARKER
22
35
  ],
23
36
  timeout: 30
24
37
  };
@@ -29,6 +42,25 @@ function matcherGroup(event, runtimePath) {
29
42
  hooks: [commandHook(event, runtimePath)]
30
43
  };
31
44
  }
45
+ function loopMatcherGroup(event) {
46
+ return {
47
+ ...(event === "PreToolUse" || event === "PermissionRequest"
48
+ ? { matcher: "Bash" }
49
+ : {}),
50
+ hooks: [
51
+ {
52
+ type: "command",
53
+ command: "bash",
54
+ args: [
55
+ CLAUDE_LOOP_LAUNCHER,
56
+ event,
57
+ CLAUDE_HOOK_MARKER
58
+ ],
59
+ timeout: 30
60
+ }
61
+ ]
62
+ };
63
+ }
32
64
  export function buildClaudeHookSettings(capabilities, runtimePath) {
33
65
  if (runtimePath.length === 0 ||
34
66
  runtimePath.length > 4096 ||
@@ -36,11 +68,18 @@ export function buildClaudeHookSettings(capabilities, runtimePath) {
36
68
  throw new AgentOpsError("CLAUDE_HOOK_PATH_INVALID", "Claude hook runtime path is invalid.");
37
69
  }
38
70
  const hooks = {};
39
- if (capabilities.includes("lifecycle-summary")) {
40
- hooks.SessionStart = [matcherGroup("SessionStart", runtimePath)];
71
+ if (capabilities.includes("project-loop")) {
72
+ for (const event of CLAUDE_LOOP_EVENTS) {
73
+ hooks[event] = [loopMatcherGroup(event)];
74
+ }
41
75
  }
42
- if (capabilities.includes("command-policy")) {
43
- hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
76
+ else {
77
+ if (capabilities.includes("lifecycle-summary")) {
78
+ hooks.SessionStart = [matcherGroup("SessionStart", runtimePath)];
79
+ }
80
+ if (capabilities.includes("command-policy")) {
81
+ hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
82
+ }
44
83
  }
45
84
  if (capabilities.includes("optional-stop-verify")) {
46
85
  hooks.Stop = [matcherGroup("Stop", runtimePath)];
@@ -50,19 +89,26 @@ export function buildClaudeHookSettings(capabilities, runtimePath) {
50
89
  function isRecord(value) {
51
90
  return typeof value === "object" && value !== null && !Array.isArray(value);
52
91
  }
53
- function isOwnedHandler(handler) {
54
- if (!isRecord(handler) ||
55
- handler.command !== "node" ||
56
- !Array.isArray(handler.args)) {
92
+ /**
93
+ * Matches only the two command shapes agent-ops actually generates. This is
94
+ * reused by installation inspection so a foreign hook cannot masquerade as
95
+ * ours merely by carrying the marker string.
96
+ */
97
+ export function isClaudeManagedHandler(handler) {
98
+ if (!isRecord(handler) || !Array.isArray(handler.args)) {
57
99
  return false;
58
100
  }
59
- return handler.args[3] === "--managed-by=agent-ops";
101
+ return ((handler.command === "node" &&
102
+ handler.args[3] === CLAUDE_HOOK_MARKER) ||
103
+ (handler.command === "bash" &&
104
+ handler.args[0] === CLAUDE_LOOP_LAUNCHER &&
105
+ handler.args[2] === CLAUDE_HOOK_MARKER));
60
106
  }
61
107
  function withoutOwnedHandlers(value) {
62
108
  if (!isRecord(value) || !Array.isArray(value.hooks)) {
63
109
  return value;
64
110
  }
65
- const hooks = value.hooks.filter((handler) => !isOwnedHandler(handler));
111
+ const hooks = value.hooks.filter((handler) => !isClaudeManagedHandler(handler));
66
112
  return hooks.length === 0 ? null : { ...value, hooks };
67
113
  }
68
114
  function hookRecord(settings) {
@@ -1,6 +1,13 @@
1
1
  export const CLAUDE_SUPPORTED_EVENTS = [
2
2
  "SessionStart",
3
+ "UserPromptSubmit",
3
4
  "PreToolUse",
5
+ "PermissionRequest",
6
+ "PostToolUse",
7
+ "PreCompact",
8
+ "PostCompact",
9
+ "SubagentStart",
10
+ "SubagentStop",
4
11
  "Stop"
5
12
  ];
6
13
  export function claudeNonInteractiveTrust(printMode) {
@@ -6,6 +6,7 @@ function json(value) {
6
6
  };
7
7
  }
8
8
  export function claudeHookOutput(event, result) {
9
+ const denialReason = result.remedy === undefined ? result.code : `${result.code}: ${result.remedy}`;
9
10
  if (event === "Stop" && result.evidence !== undefined) {
10
11
  return json({
11
12
  systemMessage: `agent-ops: ${result.code}`,
@@ -20,7 +21,7 @@ export function claudeHookOutput(event, result) {
20
21
  hookSpecificOutput: {
21
22
  hookEventName: "PreToolUse",
22
23
  permissionDecision: "deny",
23
- permissionDecisionReason: result.code
24
+ permissionDecisionReason: denialReason
24
25
  }
25
26
  });
26
27
  }
@@ -1,5 +1,16 @@
1
1
  import { AgentOpsError } from "../../fs/paths.js";
2
2
  export const CODEX_MANAGED_MARKER = "--managed-by=agent-ops";
3
+ const CODEX_LOOP_EVENTS = [
4
+ "SessionStart",
5
+ "UserPromptSubmit",
6
+ "PreToolUse",
7
+ "PermissionRequest",
8
+ "PostToolUse",
9
+ "PreCompact",
10
+ "PostCompact",
11
+ "SubagentStart",
12
+ "SubagentStop"
13
+ ];
3
14
  /**
4
15
  * Releases up to 0.1.4 registered a bare `agent-ops` command resolved through
5
16
  * PATH. Detection still recognizes it so an update replaces it instead of
@@ -35,6 +46,23 @@ function matcherGroup(event, runtimePath) {
35
46
  hooks: [commandHook(event, runtimePath)]
36
47
  };
37
48
  }
49
+ function loopMatcherGroup(event) {
50
+ const command = `bash "$(git rev-parse --show-toplevel)/.codex/hooks/agent-ops-loop.sh" ${event} ${CODEX_MANAGED_MARKER}`;
51
+ return {
52
+ ...(event === "PreToolUse" || event === "PermissionRequest"
53
+ ? { matcher: "^Bash$" }
54
+ : {}),
55
+ hooks: [
56
+ {
57
+ type: "command",
58
+ command,
59
+ commandWindows: command,
60
+ timeout: 30,
61
+ statusMessage: `Running agent-ops ${event}`
62
+ }
63
+ ]
64
+ };
65
+ }
38
66
  export function codexHookTarget(scope) {
39
67
  return {
40
68
  path: ".codex/hooks.json",
@@ -45,11 +73,18 @@ export function codexHookTarget(scope) {
45
73
  export function buildCodexHookConfig(capabilities, runtimePath) {
46
74
  assertRuntimePath(runtimePath);
47
75
  const hooks = {};
48
- if (capabilities.includes("lifecycle-summary")) {
49
- hooks.SessionStart = [matcherGroup("SessionStart", runtimePath)];
76
+ if (capabilities.includes("project-loop")) {
77
+ for (const event of CODEX_LOOP_EVENTS) {
78
+ hooks[event] = [loopMatcherGroup(event)];
79
+ }
50
80
  }
51
- if (capabilities.includes("command-policy")) {
52
- hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
81
+ else {
82
+ if (capabilities.includes("lifecycle-summary")) {
83
+ hooks.SessionStart = [matcherGroup("SessionStart", runtimePath)];
84
+ }
85
+ if (capabilities.includes("command-policy")) {
86
+ hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
87
+ }
53
88
  }
54
89
  if (capabilities.includes("optional-stop-verify")) {
55
90
  hooks.Stop = [matcherGroup("Stop", runtimePath)];
@@ -1,6 +1,13 @@
1
1
  export const CODEX_SUPPORTED_EVENTS = [
2
2
  "SessionStart",
3
+ "UserPromptSubmit",
3
4
  "PreToolUse",
5
+ "PermissionRequest",
6
+ "PostToolUse",
7
+ "PreCompact",
8
+ "PostCompact",
9
+ "SubagentStart",
10
+ "SubagentStop",
4
11
  "Stop"
5
12
  ];
6
13
  export function codexMatcherSupport(event) {
@@ -7,37 +7,54 @@ function assertBlockId(id) {
7
7
  throw new AgentOpsError("INVALID_BLOCK_ID", `Invalid block ID: ${id}`);
8
8
  }
9
9
  }
10
- function assertExactMarkerSyntax(source) {
11
- const validMarker = /^<!-- agent-ops:(?:start [a-z][a-z0-9-]{0,127} v[1-9][0-9]*|end [a-z][a-z0-9-]{0,127}) -->$/;
10
+ function assertExactMarkerSyntax(source, markerStyle) {
11
+ const validMarker = markerStyle === "html"
12
+ ? /^<!-- agent-ops:(?:start [a-z][a-z0-9-]{0,127} v[1-9][0-9]*|end [a-z][a-z0-9-]{0,127}) -->$/
13
+ : /^# agent-ops:(?:start [a-z][a-z0-9-]{0,127} v[1-9][0-9]*|end [a-z][a-z0-9-]{0,127})$/;
14
+ const markerPrefix = markerStyle === "html"
15
+ ? /<!--\s*agent-ops:/
16
+ : /#\s*agent-ops:/;
12
17
  for (const line of source.split(/\r?\n/)) {
13
- if (/<!--\s*agent-ops:/.test(line) && !validMarker.test(line)) {
18
+ if (markerPrefix.test(line) && !validMarker.test(line)) {
14
19
  throw new AgentOpsError("MALFORMED_MANAGED_BLOCK", "Managed block markers must use the exact agent-ops marker syntax.");
15
20
  }
16
21
  }
17
22
  }
18
- export function managedBlockMarkers(id, version) {
23
+ export function managedBlockMarkers(id, version, markerStyle = "html") {
19
24
  assertBlockId(id);
20
25
  if (!Number.isSafeInteger(version) || version < 1) {
21
26
  throw new AgentOpsError("INVALID_BLOCK_VERSION", `Invalid block version: ${version}`);
22
27
  }
23
- return {
24
- start: `<!-- agent-ops:start ${id} v${version} -->`,
25
- end: `<!-- agent-ops:end ${id} -->`
26
- };
28
+ return markerStyle === "html"
29
+ ? {
30
+ start: `<!-- agent-ops:start ${id} v${version} -->`,
31
+ end: `<!-- agent-ops:end ${id} -->`
32
+ }
33
+ : {
34
+ start: `# agent-ops:start ${id} v${version}`,
35
+ end: `# agent-ops:end ${id}`
36
+ };
27
37
  }
28
- function locateMarkers(source, id, version) {
38
+ function locateMarkers(source, id, version, markerStyle = "html") {
29
39
  assertBlockId(id);
30
- assertExactMarkerSyntax(source);
40
+ assertExactMarkerSyntax(source, markerStyle);
31
41
  const escapedId = escapeRegExp(id);
42
+ const startPattern = markerStyle === "html"
43
+ ? `<!-- agent-ops:start ${escapedId} v[0-9]+ -->`
44
+ : `# agent-ops:start ${escapedId} v[0-9]+`;
45
+ const end = markerStyle === "html"
46
+ ? `<!-- agent-ops:end ${id} -->`
47
+ : `# agent-ops:end ${id}`;
32
48
  const startMatches = [
33
- ...source.matchAll(new RegExp(`<!-- agent-ops:start ${escapedId} v[0-9]+ -->`, "g"))
49
+ ...source.matchAll(new RegExp(startPattern, "g"))
34
50
  ];
35
- const end = `<!-- agent-ops:end ${id} -->`;
36
51
  const endMatches = [...source.matchAll(new RegExp(escapeRegExp(end), "g"))];
37
52
  if (startMatches.length === 0 && endMatches.length === 0) {
38
53
  return null;
39
54
  }
40
- const expectedStart = version === undefined ? startMatches[0]?.[0] : managedBlockMarkers(id, version).start;
55
+ const expectedStart = version === undefined
56
+ ? startMatches[0]?.[0]
57
+ : managedBlockMarkers(id, version, markerStyle).start;
41
58
  const start = startMatches[0]?.[0];
42
59
  const startIndex = startMatches[0]?.index;
43
60
  const endIndex = endMatches[0]?.index;
@@ -54,16 +71,16 @@ function locateMarkers(source, id, version) {
54
71
  return { start, end, startIndex, endIndex };
55
72
  }
56
73
  function renderBlock(options) {
57
- if (/<!--\s*agent-ops:/.test(options.content)) {
74
+ if (/(?:<!--|#)\s*agent-ops:/.test(options.content)) {
58
75
  throw new AgentOpsError("AMBIGUOUS_MANAGED_CONTENT", "Managed content must not contain agent-ops marker boundaries.");
59
76
  }
60
- const { start, end } = managedBlockMarkers(options.id, options.version);
77
+ const { start, end } = managedBlockMarkers(options.id, options.version, options.markerStyle);
61
78
  const content = options.content.replace(/\r\n/g, "\n").replace(/\n+$/g, "");
62
79
  return `${start}\n${content}\n${end}`;
63
80
  }
64
81
  export function applyManagedBlock(source, options) {
65
82
  const block = renderBlock(options);
66
- const located = locateMarkers(source, options.id, options.version);
83
+ const located = locateMarkers(source, options.id, options.version, options.markerStyle);
67
84
  if (located === null) {
68
85
  if (source.length === 0) {
69
86
  return `${block}\n`;
@@ -72,8 +89,8 @@ export function applyManagedBlock(source, options) {
72
89
  }
73
90
  return `${source.slice(0, located.startIndex)}${block}${source.slice(located.endIndex + located.end.length)}`;
74
91
  }
75
- export function removeManagedBlock(source, id) {
76
- const located = locateMarkers(source, id);
92
+ export function removeManagedBlock(source, id, markerStyle = "html") {
93
+ const located = locateMarkers(source, id, undefined, markerStyle);
77
94
  if (located === null) {
78
95
  return source;
79
96
  }