@kylecheng3146/agent-ops 0.1.4 → 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 (84) hide show
  1. package/README.md +164 -13
  2. package/dist/packages/cli/src/args.js +40 -7
  3. package/dist/packages/cli/src/bin.js +35 -11
  4. package/dist/packages/cli/src/cli.js +7 -2
  5. package/dist/packages/cli/src/codex-loop-process.js +70 -0
  6. package/dist/packages/cli/src/commands/doctor.js +31 -9
  7. package/dist/packages/cli/src/commands/hook.js +33 -16
  8. package/dist/packages/cli/src/commands/init.js +9 -5
  9. package/dist/packages/cli/src/commands/review.js +5 -1
  10. package/dist/packages/cli/src/commands/uninstall.js +6 -5
  11. package/dist/packages/cli/src/commands/update.js +16 -5
  12. package/dist/packages/cli/src/context.js +69 -3
  13. package/dist/packages/cli/src/hook-process.js +231 -16
  14. package/dist/packages/cli/src/loop-entry.js +8 -0
  15. package/dist/packages/cli/src/plan-output.js +9 -4
  16. package/dist/packages/cli/src/public-plan.js +62 -0
  17. package/dist/packages/cli/src/version.js +1 -1
  18. package/dist/packages/cli/src/wizard.js +36 -15
  19. package/dist/runtime/src/adapters/claude/config.js +57 -19
  20. package/dist/runtime/src/adapters/claude/events.js +33 -0
  21. package/dist/runtime/src/adapters/claude/output.js +8 -7
  22. package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
  23. package/dist/runtime/src/adapters/codex/config.js +66 -13
  24. package/dist/runtime/src/adapters/codex/events.js +33 -0
  25. package/dist/runtime/src/adapters/codex/output.js +10 -0
  26. package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
  27. package/dist/runtime/src/adapters/opencode/config.js +170 -0
  28. package/dist/runtime/src/adapters/opencode/events.js +49 -0
  29. package/dist/runtime/src/adapters/opencode/input.js +32 -0
  30. package/dist/runtime/src/adapters/opencode/output.js +23 -0
  31. package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
  32. package/dist/runtime/src/config/explain.js +7 -0
  33. package/dist/runtime/src/config/hash.js +24 -0
  34. package/dist/runtime/src/config/merge.js +6 -2
  35. package/dist/runtime/src/config/migrate.js +19 -4
  36. package/dist/runtime/src/contracts.js +10 -1
  37. package/dist/runtime/src/fs/managed-block.js +35 -18
  38. package/dist/runtime/src/fs/manifest.js +32 -1
  39. package/dist/runtime/src/fs/transaction.js +14 -2
  40. package/dist/runtime/src/hooks/advisory.js +16 -0
  41. package/dist/runtime/src/hooks/codex-loop.js +439 -0
  42. package/dist/runtime/src/hooks/stop-service.js +70 -0
  43. package/dist/runtime/src/hooks/stop-verify.js +4 -1
  44. package/dist/runtime/src/install/codex-loop.js +139 -0
  45. package/dist/runtime/src/install/doctor.js +182 -14
  46. package/dist/runtime/src/install/harness.js +294 -35
  47. package/dist/runtime/src/install/hooks.js +22 -17
  48. package/dist/runtime/src/install/ownership.js +146 -35
  49. package/dist/runtime/src/install/plan.js +276 -31
  50. package/dist/runtime/src/install/probes.js +9 -43
  51. package/dist/runtime/src/install/profiles.js +12 -3
  52. package/dist/runtime/src/install/surface-inspection.js +296 -0
  53. package/dist/runtime/src/install/surfaces.js +11 -0
  54. package/dist/runtime/src/install/uninstall.js +11 -4
  55. package/dist/runtime/src/install/update.js +13 -3
  56. package/dist/runtime/src/logging/local-log.js +25 -0
  57. package/dist/runtime/src/schema/validate.js +45 -19
  58. package/dist/runtime/src/task/service.js +3 -3
  59. package/dist/runtime/src/task/store.js +12 -3
  60. package/dist/runtime/src/verify/command-executor.js +113 -0
  61. package/dist/runtime/src/verify/evidence.js +4 -24
  62. package/dist/runtime/src/verify/service.js +20 -92
  63. package/dist/runtime/src/verify/spawn.js +6 -1
  64. package/docs/en/guides/configuration.md +159 -0
  65. package/docs/en/guides/quickstart.md +9 -0
  66. package/docs/en/guides/security.md +5 -0
  67. package/docs/en/spec/README.md +9 -0
  68. package/docs/en/spec/harness-adapters.md +104 -2
  69. package/docs/en/spec/maintenance.md +11 -0
  70. package/docs/en/spec/review.md +11 -0
  71. package/docs/zh-TW/guides/configuration.md +145 -0
  72. package/docs/zh-TW/guides/quickstart.md +9 -0
  73. package/docs/zh-TW/guides/security.md +5 -0
  74. package/docs/zh-TW/spec/README.md +9 -0
  75. package/docs/zh-TW/spec/harness-adapters.md +89 -3
  76. package/docs/zh-TW/spec/maintenance.md +11 -1
  77. package/docs/zh-TW/spec/review.md +10 -0
  78. package/package.json +4 -2
  79. package/schemas/config.schema.json +56 -2
  80. package/schemas/manifest.schema.json +19 -3
  81. package/templates/common/AGENTS.block.md +2 -1
  82. package/templates/common/CLAUDE.block.md +2 -1
  83. package/dist/runtime/src/review/claude-runner.js +0 -4
  84. package/dist/runtime/src/review/codex-runner.js +0 -4
@@ -1,6 +1,5 @@
1
- import { buildClaudeHookSettings, mergeClaudeSettings, stripClaudeManagedHooks } from "../adapters/claude/config.js";
2
- import { buildCodexHookConfig, mergeCodexHookConfig, stripCodexManagedHooks } from "../adapters/codex/config.js";
3
1
  import { AgentOpsError } from "../fs/paths.js";
2
+ import { harnessDescriptor, harnessHookPath } from "./harness.js";
4
3
  export const CLAUDE_HOOK_PATH = ".claude/settings.json";
5
4
  export const CODEX_HOOK_PATH = ".codex/hooks.json";
6
5
  function format(value) {
@@ -17,31 +16,34 @@ function parseSettings(path, source) {
17
16
  throw new AgentOpsError("HOOK_SETTINGS_INVALID_JSON", `Hook settings are not valid JSON: ${path}`, { cause: error });
18
17
  }
19
18
  }
20
- export function hookRegistrationPath(harness, scope) {
19
+ export function hookRegistrationPath(harness, scope, root) {
21
20
  // ponytail: user scope resolves against AGENT_OPS_HOME, so the same
22
- // relative path serves both scopes.
23
- void scope;
24
- return harness === "claude" ? CLAUDE_HOOK_PATH : CODEX_HOOK_PATH;
21
+ // root can resolve a scope-specific harness path.
22
+ return harnessHookPath(harness, scope, root);
25
23
  }
26
24
  /**
27
25
  * Builds the merged settings file for one harness, preserving foreign hooks.
28
26
  * Returns null when the selected capabilities register no events at all.
29
27
  */
30
28
  export function planHookRegistration(options) {
31
- const path = hookRegistrationPath(options.harness, options.scope);
32
- const managed = options.harness === "claude"
33
- ? buildClaudeHookSettings(options.capabilities, options.runtimePath)
34
- : buildCodexHookConfig(options.capabilities);
29
+ const descriptor = harnessDescriptor(options.harness);
30
+ if (descriptor.control.buildHooks === undefined ||
31
+ descriptor.control.mergeHooks === undefined) {
32
+ // File-backed adapters (currently opencode) track their plugin as a
33
+ // managed artifact rather than as a ManagedHookRecord.
34
+ return null;
35
+ }
36
+ const path = options.path ?? hookRegistrationPath(options.harness, options.scope);
37
+ const managed = descriptor.control.buildHooks(options.capabilities, options.runtimePath);
35
38
  const events = Object.keys(managed.hooks);
36
39
  if (events.length === 0) {
37
40
  return null;
38
41
  }
39
42
  const existing = parseSettings(path, options.currentSource);
40
- const merged = options.harness === "claude"
41
- ? mergeClaudeSettings(existing, managed)
42
- : mergeCodexHookConfig(existing, managed);
43
+ const merged = descriptor.control.mergeHooks(existing, managed);
43
44
  return {
44
45
  content: format(merged),
46
+ disclosure: "opaque",
45
47
  record: {
46
48
  id: `${options.harness}-hooks`,
47
49
  path,
@@ -52,7 +54,7 @@ export function planHookRegistration(options) {
52
54
  };
53
55
  }
54
56
  function onlyManagedRemains(harness, value) {
55
- const ownKeys = new Set(harness === "codex" ? ["hooks", "description"] : ["hooks"]);
57
+ const ownKeys = new Set(harnessDescriptor(harness).control.ownSettingsKeys ?? []);
56
58
  const hooks = value.hooks;
57
59
  return (Object.keys(value).every((key) => ownKeys.has(key)) &&
58
60
  typeof hooks === "object" &&
@@ -64,12 +66,15 @@ function onlyManagedRemains(harness, value) {
64
66
  * when nothing but agent-ops content is left, so foreign settings survive.
65
67
  */
66
68
  export function planHookRemoval(record, currentSource) {
69
+ const descriptor = harnessDescriptor(record.harness);
70
+ if (descriptor.control.stripHooks === undefined) {
71
+ throw new AgentOpsError("HOOK_REMOVAL_UNSUPPORTED", `Harness hook records are not supported for ${record.harness}.`);
72
+ }
67
73
  const existing = parseSettings(record.path, currentSource);
68
- const stripped = record.harness === "claude"
69
- ? stripClaudeManagedHooks(existing)
70
- : stripCodexManagedHooks(existing);
74
+ const stripped = descriptor.control.stripHooks(existing);
71
75
  return {
72
76
  path: record.path,
77
+ disclosure: "opaque",
73
78
  content: onlyManagedRemains(record.harness, stripped)
74
79
  ? null
75
80
  : format(stripped)
@@ -1,27 +1,38 @@
1
1
  import { applyManagedBlock, managedBlockMarkers } from "../fs/managed-block.js";
2
2
  import { AgentOpsError } from "../fs/paths.js";
3
- import { COMMON_AGENTS_BLOCK, COMMON_CLAUDE_BLOCK } from "./harness.js";
4
- import { hookRegistrationPath } from "./hooks.js";
5
- function selectedHarnesses(manifest) {
6
- return manifest.harness === "both"
7
- ? ["codex", "claude"]
8
- : [manifest.harness];
9
- }
10
- function expectedMarker(manifest, id) {
11
- const isCodex = id === "codex";
12
- const markerId = `${id}-routing`;
13
- const markers = managedBlockMarkers(markerId, 1);
14
- const instructionFile = isCodex ? "AGENTS.md" : "CLAUDE.md";
3
+ import { harnessDescriptor, harnessHookPath, routingBlockId, selectHarnessHookSurface, rulesArtifactId } from "./harness.js";
4
+ import { isOpencodePluginPath } from "../adapters/opencode/config.js";
5
+ import { LOOP_MARKER_ID, LOOP_MARKER_VERSION, loopIgnoreContent, loopLauncherArtifactId, loopLauncherPath, selectedLoopHarnesses } from "./codex-loop.js";
6
+ function expectedMarker(manifest, id, markerId) {
7
+ const descriptor = harnessDescriptor(id);
8
+ const markers = managedBlockMarkers(markerId, 1, "html");
15
9
  return {
16
10
  id: markerId,
17
11
  path: manifest.scope === "project"
18
- ? instructionFile
19
- : `.${id}/${instructionFile}`,
12
+ ? descriptor.control.instructionFile
13
+ : `.${id}/${descriptor.control.instructionFile}`,
14
+ startMarker: markers.start,
15
+ endMarker: markers.end,
16
+ markerStyle: "html",
17
+ content: descriptor.control.routing.desired,
18
+ legacyContent: descriptor.control.routing.legacy
19
+ };
20
+ }
21
+ function expectedLoopMarker(manifest) {
22
+ const markers = managedBlockMarkers(LOOP_MARKER_ID, LOOP_MARKER_VERSION, "hash");
23
+ return {
24
+ id: LOOP_MARKER_ID,
25
+ path: ".gitignore",
20
26
  startMarker: markers.start,
21
27
  endMarker: markers.end,
22
- content: isCodex ? COMMON_AGENTS_BLOCK : COMMON_CLAUDE_BLOCK
28
+ markerStyle: "hash",
29
+ content: loopIgnoreContent(manifest.harness),
30
+ legacyContent: []
23
31
  };
24
32
  }
33
+ function pathKey(path) {
34
+ return path.toLowerCase();
35
+ }
25
36
  function manifestOwnershipError() {
26
37
  return new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", "The manifest does not match a supported managed installation shape.");
27
38
  }
@@ -29,37 +40,129 @@ function manifestOwnershipError() {
29
40
  * Hook records are optional: installations without hook capabilities, and
30
41
  * manifests written before hook registration existed, carry none.
31
42
  */
32
- function assertSupportedHookRecords(manifest, harnesses) {
43
+ function assertSupportedHookRecords(manifest, harnesses, root) {
33
44
  const selected = new Set(harnesses);
34
45
  const seen = new Set();
35
46
  for (const hook of manifest.hooks ?? []) {
36
47
  if (!selected.has(hook.harness) ||
37
48
  seen.has(hook.harness) ||
49
+ harnessDescriptor(hook.harness).control.buildHooks === undefined ||
38
50
  hook.id !== `${hook.harness}-hooks` ||
39
- hook.path !== hookRegistrationPath(hook.harness, manifest.scope) ||
40
51
  hook.events.length === 0) {
41
52
  throw manifestOwnershipError();
42
53
  }
54
+ try {
55
+ selectHarnessHookSurface({
56
+ harness: hook.harness,
57
+ scope: manifest.scope,
58
+ root,
59
+ persistedPath: hook.path
60
+ });
61
+ }
62
+ catch {
63
+ throw manifestOwnershipError();
64
+ }
43
65
  seen.add(hook.harness);
44
66
  }
45
67
  }
46
- export function assertSupportedManifestOwnership(manifest) {
47
- const harnesses = selectedHarnesses(manifest);
48
- const expectedArtifacts = new Map([
49
- ["config", ".agent-ops/config.json"]
68
+ export function assertSupportedManifestOwnership(manifest, root) {
69
+ const harnesses = manifest.harness;
70
+ const expectedArtifactPaths = new Map([
71
+ [
72
+ pathKey(".agent-ops/config.json"),
73
+ { path: ".agent-ops/config.json", ids: new Set(["config"]) }
74
+ ]
75
+ ]);
76
+ const requiredArtifactPaths = new Set([
77
+ pathKey(".agent-ops/config.json")
50
78
  ]);
51
79
  const expectedMarkers = new Map();
80
+ const expectedMarkerPaths = new Set();
81
+ const loopHarnesses = selectedLoopHarnesses(harnesses);
82
+ const hasLoopArtifacts = manifest.artifacts.some(({ id }) => loopHarnesses.some((harness) => id === loopLauncherArtifactId(harness)));
83
+ const hasLoopMarker = manifest.markers.some(({ id }) => id === LOOP_MARKER_ID);
84
+ const hasLoop = hasLoopArtifacts || hasLoopMarker;
85
+ if (hasLoop &&
86
+ (manifest.scope !== "project" || loopHarnesses.length === 0)) {
87
+ throw manifestOwnershipError();
88
+ }
89
+ const recordedOpencodePluginPath = manifest.artifacts.find(({ id }) => id === "opencode-plugin")?.path;
90
+ if (recordedOpencodePluginPath !== undefined &&
91
+ (!harnesses.includes("opencode") ||
92
+ !isOpencodePluginPath(manifest.scope, recordedOpencodePluginPath))) {
93
+ throw manifestOwnershipError();
94
+ }
52
95
  for (const id of harnesses) {
53
- expectedArtifacts.set(`${id}-rules`, `.agent-ops/${id === "codex" ? "AGENTS.md" : "CLAUDE.md"}`);
54
- const marker = expectedMarker(manifest, id);
55
- expectedMarkers.set(marker.id, marker);
96
+ const descriptor = harnessDescriptor(id);
97
+ const artifactPath = `.agent-ops/${descriptor.control.instructionFile}`;
98
+ const artifactKey = pathKey(artifactPath);
99
+ const artifactEntry = expectedArtifactPaths.get(artifactKey);
100
+ const artifactIds = artifactEntry?.ids ?? new Set();
101
+ artifactIds.add(rulesArtifactId(descriptor));
102
+ // Accept the PR 2 per-harness spelling while update rewrites it to the
103
+ // path-derived ID. This keeps already-installed manifests removable.
104
+ artifactIds.add(`${id}-rules`);
105
+ expectedArtifactPaths.set(artifactKey, {
106
+ path: artifactEntry?.path ?? artifactPath,
107
+ ids: artifactIds
108
+ });
109
+ requiredArtifactPaths.add(artifactKey);
110
+ const markerPath = manifest.scope === "project"
111
+ ? descriptor.control.instructionFile
112
+ : `.${id}/${descriptor.control.instructionFile}`;
113
+ const markerKey = pathKey(markerPath);
114
+ expectedMarkerPaths.add(markerKey);
115
+ const currentId = routingBlockId(id, manifest.scope, descriptor);
116
+ const markerIds = new Set([currentId, `${id}-routing`]);
117
+ for (const markerId of markerIds) {
118
+ expectedMarkers.set(markerId, expectedMarker(manifest, id, markerId));
119
+ }
120
+ if (id === "opencode") {
121
+ const pluginPath = recordedOpencodePluginPath ?? harnessHookPath(id, manifest.scope, root);
122
+ const pluginKey = pathKey(pluginPath);
123
+ const pluginEntry = expectedArtifactPaths.get(pluginKey);
124
+ if (pluginEntry !== undefined && pluginEntry.path !== pluginPath) {
125
+ throw manifestOwnershipError();
126
+ }
127
+ const pluginIds = pluginEntry?.ids ?? new Set();
128
+ pluginIds.add("opencode-plugin");
129
+ expectedArtifactPaths.set(pluginKey, {
130
+ path: pluginPath,
131
+ ids: pluginIds
132
+ });
133
+ }
134
+ }
135
+ if (hasLoop) {
136
+ for (const harness of loopHarnesses) {
137
+ const path = loopLauncherPath(harness);
138
+ expectedArtifactPaths.set(pathKey(path), {
139
+ path,
140
+ ids: new Set([loopLauncherArtifactId(harness)])
141
+ });
142
+ requiredArtifactPaths.add(pathKey(path));
143
+ }
144
+ expectedMarkerPaths.add(pathKey(".gitignore"));
145
+ expectedMarkers.set(LOOP_MARKER_ID, expectedLoopMarker(manifest));
56
146
  }
57
- if (manifest.artifacts.length !== expectedArtifacts.size ||
58
- manifest.markers.length !== expectedMarkers.size) {
147
+ const opencodePluginPath = harnesses.includes("opencode")
148
+ ? recordedOpencodePluginPath ??
149
+ harnessHookPath("opencode", manifest.scope, root)
150
+ : null;
151
+ const optionalArtifactCount = opencodePluginPath !== null &&
152
+ expectedArtifactPaths.has(pathKey(opencodePluginPath))
153
+ ? 1
154
+ : 0;
155
+ if ((manifest.artifacts.length !== requiredArtifactPaths.size &&
156
+ manifest.artifacts.length !== requiredArtifactPaths.size +
157
+ optionalArtifactCount) ||
158
+ manifest.markers.length !== expectedMarkerPaths.size) {
59
159
  throw manifestOwnershipError();
60
160
  }
61
161
  for (const artifact of manifest.artifacts) {
62
- if (expectedArtifacts.get(artifact.id) !== artifact.path) {
162
+ const expected = expectedArtifactPaths.get(pathKey(artifact.path));
163
+ if (expected === undefined ||
164
+ expected.path !== artifact.path ||
165
+ !expected.ids.has(artifact.id)) {
63
166
  throw manifestOwnershipError();
64
167
  }
65
168
  }
@@ -72,7 +175,7 @@ export function assertSupportedManifestOwnership(manifest) {
72
175
  throw manifestOwnershipError();
73
176
  }
74
177
  }
75
- assertSupportedHookRecords(manifest, harnesses);
178
+ assertSupportedHookRecords(manifest, harnesses, root);
76
179
  return expectedMarkers;
77
180
  }
78
181
  function exactMarkerCount(source, marker) {
@@ -92,13 +195,21 @@ export function assertExpectedManagedBlock(source, marker, expected) {
92
195
  startIndex >= endIndex) {
93
196
  throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block boundaries changed after installation: ${marker.path}`);
94
197
  }
95
- const expectedBlock = applyManagedBlock("", {
96
- id: expected.id,
97
- version: 1,
98
- content: expected.content
99
- }).replace(/\n$/u, "");
100
198
  const currentBlock = source.slice(startIndex, endIndex + marker.endMarker.length);
101
- if (currentBlock !== expectedBlock) {
102
- throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block content changed after installation: ${marker.path}`);
199
+ const candidates = [
200
+ ["desired", expected.content],
201
+ ...expected.legacyContent.map((content) => ["legacy", content])
202
+ ];
203
+ for (const [kind, content] of candidates) {
204
+ const expectedBlock = applyManagedBlock("", {
205
+ id: expected.id,
206
+ version: 1,
207
+ content,
208
+ markerStyle: expected.markerStyle
209
+ }).replace(/\n$/u, "");
210
+ if (currentBlock === expectedBlock) {
211
+ return kind;
212
+ }
103
213
  }
214
+ throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block content changed after installation: ${marker.path}`);
104
215
  }