@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
@@ -5,11 +5,17 @@ import { parseInstallManifest, PROJECT_MANIFEST_PATH } from "../fs/manifest.js";
5
5
  import { resolveContainedPath } from "../fs/paths.js";
6
6
  import { validateConfig } from "../schema/validate.js";
7
7
  import { assertExpectedManagedBlock, assertSupportedManifestOwnership } from "./ownership.js";
8
+ import { isOpencodeManagedPlugin } from "../adapters/opencode/config.js";
9
+ import { harnessDescriptor, managedRules } from "./harness.js";
10
+ import { resolveCapabilities, resolveProfiles } from "./profiles.js";
11
+ import { inspectHarnessSurfaces, inspectHarnessRegistrations } from "./surface-inspection.js";
8
12
  const CONFIG_PATH = ".agent-ops/config.json";
9
13
  const MINIMUM_NODE_VERSION = [22, 14, 0];
10
14
  const MAX_DOCTOR_FILE_BYTES = 1024 * 1024;
11
- function check(id, status, message) {
12
- return { id, status, message };
15
+ function check(id, status, message, code) {
16
+ return code === undefined
17
+ ? { id, status, message }
18
+ : { id, status, message, code };
13
19
  }
14
20
  function parseNodeVersion(version) {
15
21
  const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
@@ -80,7 +86,7 @@ async function readContainedText(root, path) {
80
86
  async function checkManifest(root) {
81
87
  try {
82
88
  const manifest = parseInstallManifest(await readContainedText(root, PROJECT_MANIFEST_PATH));
83
- assertSupportedManifestOwnership(manifest);
89
+ assertSupportedManifestOwnership(manifest, root);
84
90
  return {
85
91
  check: check("manifest", "PASS", "Installation manifest is valid."),
86
92
  manifest
@@ -118,30 +124,89 @@ async function checkConfig(root) {
118
124
  }
119
125
  async function checkArtifacts(root, manifest) {
120
126
  if (manifest === undefined) {
121
- return check("artifacts", "FAIL", "Artifacts cannot be verified without a valid manifest.");
127
+ return {
128
+ check: check("artifacts", "FAIL", "Artifacts cannot be verified without a valid manifest."),
129
+ hashesByPath: new Map()
130
+ };
122
131
  }
123
132
  const failures = [];
133
+ const hashesByPath = new Map();
124
134
  for (const artifact of manifest.artifacts) {
125
135
  try {
126
136
  const content = await readContained(root, artifact.path);
127
- if (sha256(content) !== artifact.hash) {
137
+ const hash = sha256(content);
138
+ if (hash !== artifact.hash ||
139
+ (artifact.id === "opencode-plugin" &&
140
+ !isOpencodeManagedPlugin(content.toString("utf8")))) {
128
141
  failures.push(artifact.path);
129
142
  }
143
+ else {
144
+ hashesByPath.set(artifact.path, hash);
145
+ }
130
146
  }
131
147
  catch {
132
148
  failures.push(artifact.path);
133
149
  }
134
150
  }
135
- return failures.length === 0
136
- ? check("artifacts", "PASS", "All managed artifacts match their hashes.")
137
- : check("artifacts", "FAIL", `Managed artifacts failed verification: ${failures.join(", ")}.`);
151
+ return {
152
+ check: failures.length === 0
153
+ ? check("artifacts", "PASS", "All managed artifacts match their hashes.")
154
+ : check("artifacts", "FAIL", `Managed artifacts failed verification: ${failures.join(", ")}.`),
155
+ hashesByPath
156
+ };
157
+ }
158
+ function checkArtifactStaleness(manifest, config, artifacts, toolkitVersion) {
159
+ if (manifest === undefined || config === undefined) {
160
+ return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness cannot be assessed without a valid manifest and configuration.");
161
+ }
162
+ if (artifacts.check.status !== "PASS") {
163
+ return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness cannot be assessed until artifact integrity passes.");
164
+ }
165
+ if (toolkitVersion === undefined) {
166
+ return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness cannot be assessed without the running toolkit version.");
167
+ }
168
+ const expectedHashesByPath = new Map();
169
+ try {
170
+ const resolved = config.profiles.length === 0
171
+ ? { profiles: [], capabilities: [] }
172
+ : resolveCapabilities(config);
173
+ for (const id of manifest.harness) {
174
+ const descriptor = harnessDescriptor(id);
175
+ const path = `.agent-ops/${descriptor.control.instructionFile}`;
176
+ if (!expectedHashesByPath.has(path)) {
177
+ expectedHashesByPath.set(path, sha256(managedRules(descriptor, {
178
+ scope: manifest.scope,
179
+ profiles: resolved.profiles,
180
+ capabilities: resolved.capabilities,
181
+ toolkitVersion
182
+ })));
183
+ }
184
+ }
185
+ }
186
+ catch {
187
+ return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness could not be assessed safely.");
188
+ }
189
+ const stalePaths = [];
190
+ for (const [path, expectedHash] of expectedHashesByPath) {
191
+ const actualHash = artifacts.hashesByPath.get(path);
192
+ if (actualHash === undefined) {
193
+ return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness could not be assessed safely.");
194
+ }
195
+ if (actualHash !== expectedHash) {
196
+ stalePaths.push(path);
197
+ }
198
+ }
199
+ return stalePaths.length === 0
200
+ ? check("artifact-staleness", "PASS", "Managed artifacts match the current toolkit and configuration.")
201
+ : check("artifact-staleness", "DEGRADED", `Managed artifacts need update: ${stalePaths.join(", ")}; run agent-ops update.`, "UPDATE_REQUIRED");
138
202
  }
139
203
  async function checkMarkers(root, manifest) {
140
204
  if (manifest === undefined) {
141
205
  return check("markers", "FAIL", "Managed blocks cannot be verified without a valid manifest.");
142
206
  }
143
207
  const failures = [];
144
- const expectedMarkers = assertSupportedManifestOwnership(manifest);
208
+ const legacyPaths = [];
209
+ const expectedMarkers = assertSupportedManifestOwnership(manifest, root);
145
210
  for (const marker of manifest.markers) {
146
211
  try {
147
212
  const source = await readContainedText(root, marker.path);
@@ -150,15 +215,22 @@ async function checkMarkers(root, manifest) {
150
215
  failures.push(marker.path);
151
216
  continue;
152
217
  }
153
- assertExpectedManagedBlock(source, marker, expected);
218
+ const match = assertExpectedManagedBlock(source, marker, expected);
219
+ if (match === "legacy") {
220
+ legacyPaths.push(marker.path);
221
+ }
154
222
  }
155
223
  catch {
156
224
  failures.push(marker.path);
157
225
  }
158
226
  }
159
- return failures.length === 0
160
- ? check("markers", "PASS", "All managed block markers are intact.")
161
- : check("markers", "FAIL", `Managed block markers failed verification: ${failures.join(", ")}.`);
227
+ if (failures.length > 0) {
228
+ return check("markers", "FAIL", `Managed block markers failed verification: ${failures.join(", ")}.`);
229
+ }
230
+ if (legacyPaths.length > 0) {
231
+ return check("markers", "DEGRADED", `Legacy managed routing blocks need migration: ${legacyPaths.join(", ")}.`);
232
+ }
233
+ return check("markers", "PASS", "All managed block markers are intact.");
162
234
  }
163
235
  async function checkProbe(id, probe) {
164
236
  if (probe === undefined) {
@@ -177,21 +249,117 @@ async function checkProbe(id, probe) {
177
249
  return check(id, "FAIL", "Probe failed.");
178
250
  }
179
251
  }
252
+ function checkLifecycleSummary(manifest, config) {
253
+ if (manifest === undefined || config === undefined) {
254
+ return check("lifecycle-summary", "UNKNOWN", "Lifecycle summary cannot be assessed without a valid manifest and configuration.");
255
+ }
256
+ const hasLifecycleSummary = config.profiles.length > 0 &&
257
+ resolveProfiles(config.profiles).capabilities.includes("lifecycle-summary");
258
+ if (!hasLifecycleSummary) {
259
+ return check("lifecycle-summary", "PASS", "Lifecycle summary has no harness-specific degradation.");
260
+ }
261
+ const registrations = manifest.harness.map((id) => ({
262
+ id,
263
+ registration: harnessDescriptor(id).control.registrations.find((candidate) => candidate.capability === "lifecycle-summary")
264
+ }));
265
+ const missing = registrations
266
+ .filter(({ registration }) => registration === undefined)
267
+ .map(({ id }) => id);
268
+ if (missing.length > 0) {
269
+ return check("lifecycle-summary", "UNKNOWN", `Lifecycle summary registration is missing for ${missing.join(", ")}.`);
270
+ }
271
+ const unsupported = registrations
272
+ .filter(({ registration }) => registration?.support === "unsupported")
273
+ .map(({ id }) => id);
274
+ if (unsupported.length > 0) {
275
+ return check("lifecycle-summary", "UNSUPPORTED", `Lifecycle summary is not dispatched for ${unsupported.join(", ")}; advisory runtime wiring is unavailable.`);
276
+ }
277
+ const degraded = registrations
278
+ .filter(({ registration }) => registration?.support === "degraded")
279
+ .map(({ id }) => id);
280
+ if (degraded.length > 0) {
281
+ return check("lifecycle-summary", "DEGRADED", `Lifecycle summary is degraded for ${degraded.join(", ")}.`);
282
+ }
283
+ const unknown = registrations
284
+ .filter(({ registration }) => registration?.support === "unknown")
285
+ .map(({ id }) => id);
286
+ if (unknown.length > 0) {
287
+ return check("lifecycle-summary", "UNKNOWN", `Lifecycle summary support is unknown for ${unknown.join(", ")}.`);
288
+ }
289
+ return check("lifecycle-summary", "PASS", "Lifecycle summary is reachable for every selected harness.");
290
+ }
291
+ async function checkSurfaceInventory(root, manifest, config) {
292
+ if (manifest === undefined || config === undefined) {
293
+ return {
294
+ check: check("surface-inventory", "UNKNOWN", "Harness surfaces cannot be inventoried without a valid manifest and configuration."),
295
+ surfaces: []
296
+ };
297
+ }
298
+ try {
299
+ const surfaces = await inspectHarnessSurfaces({
300
+ root,
301
+ scope: manifest.scope,
302
+ harness: manifest.harness,
303
+ profiles: config.profiles
304
+ });
305
+ const unknown = surfaces.filter(({ status }) => status === "unknown");
306
+ return {
307
+ check: check("surface-inventory", unknown.length > 0 ? "UNKNOWN" : "PASS", unknown.length > 0
308
+ ? `${unknown.length} harness surface(s) could not be inspected.`
309
+ : "Harness surfaces were inventoried without exposing settings values."),
310
+ surfaces
311
+ };
312
+ }
313
+ catch {
314
+ return {
315
+ check: check("surface-inventory", "UNKNOWN", "Harness surfaces could not be inspected safely."),
316
+ surfaces: []
317
+ };
318
+ }
319
+ }
320
+ async function checkRegistrationDrift(root, manifest, config) {
321
+ if (manifest === undefined || config === undefined) {
322
+ return check("registration-drift", "UNKNOWN", "Hook registration drift cannot be assessed without a valid manifest and configuration.");
323
+ }
324
+ try {
325
+ const statuses = await inspectHarnessRegistrations({
326
+ root,
327
+ manifest,
328
+ config
329
+ });
330
+ const drifted = statuses
331
+ .filter(({ registered }) => !registered)
332
+ .map(({ harness }) => harness);
333
+ return drifted.length === 0
334
+ ? check("registration-drift", "PASS", "Managed hook registrations match the desired capabilities.")
335
+ : check("registration-drift", "FAIL", `Hook registration drift detected for ${drifted.join(", ")}; run agent-ops update.`, "UPDATE_REQUIRED");
336
+ }
337
+ catch {
338
+ return check("registration-drift", "UNKNOWN", "Hook registration drift could not be assessed safely.");
339
+ }
340
+ }
180
341
  export async function doctorInstallation(options) {
181
342
  const manifest = await checkManifest(options.root);
182
343
  const config = await checkConfig(options.root);
344
+ const artifacts = await checkArtifacts(options.root, manifest.manifest);
345
+ const surfaceInventory = await checkSurfaceInventory(options.root, manifest.manifest, config.config);
183
346
  const checks = [
184
347
  checkNodeVersion(options.nodeVersion ?? process.versions.node),
185
348
  manifest.check,
186
349
  config.check,
187
- await checkArtifacts(options.root, manifest.manifest),
350
+ artifacts.check,
351
+ checkArtifactStaleness(manifest.manifest, config.config, artifacts, options.toolkitVersion),
188
352
  await checkMarkers(options.root, manifest.manifest),
353
+ surfaceInventory.check,
354
+ await checkRegistrationDrift(options.root, manifest.manifest, config.config),
189
355
  await checkProbe("hook-registration", options.probes?.hookRegistration),
356
+ checkLifecycleSummary(manifest.manifest, config.config),
190
357
  await checkProbe("repository-trust", options.probes?.repositoryTrust),
191
358
  await checkProbe("smoke-availability", options.probes?.smokeAvailability)
192
359
  ];
193
360
  return {
194
361
  checks,
362
+ surfaces: surfaceInventory.surfaces,
195
363
  ...(manifest.manifest === undefined
196
364
  ? {}
197
365
  : { manifest: manifest.manifest }),
@@ -1,8 +1,233 @@
1
+ import { buildClaudeHookSettings, isClaudeManagedHandler, mergeClaudeSettings, stripClaudeManagedHooks } from "../adapters/claude/config.js";
2
+ import { CLAUDE_CAPABILITY_REGISTRATIONS } from "../adapters/claude/events.js";
3
+ import { normalizeClaudeHookInput } from "../adapters/claude/input.js";
4
+ import { claudeHookOutput } from "../adapters/claude/output.js";
5
+ import { claudeSurfaces } from "../adapters/claude/surfaces.js";
6
+ import { buildCodexHookConfig, CODEX_MANAGED_MARKER, mergeCodexHookConfig, stripCodexManagedHooks } from "../adapters/codex/config.js";
7
+ import { CODEX_CAPABILITY_REGISTRATIONS } from "../adapters/codex/events.js";
8
+ import { normalizeCodexHookInput } from "../adapters/codex/input.js";
9
+ import { codexHookOutput } from "../adapters/codex/output.js";
10
+ import { codexSurfaces } from "../adapters/codex/surfaces.js";
11
+ import { buildOpencodePlugin, isOpencodePluginRegistered, opencodePluginTarget } from "../adapters/opencode/config.js";
12
+ import { OPENCODE_CAPABILITY_REGISTRATIONS } from "../adapters/opencode/events.js";
13
+ import { normalizeOpencodeHookInput } from "../adapters/opencode/input.js";
14
+ import { opencodeHookOutput } from "../adapters/opencode/output.js";
15
+ import { opencodeSurfaces } from "../adapters/opencode/surfaces.js";
1
16
  import { AgentOpsError } from "../fs/paths.js";
2
- export const COMMON_AGENTS_BLOCK = "## Loop Engineering\n\nUse `.agent-ops/AGENTS.md` as the canonical Loop Engineering specification for this project.\n";
3
- export const COMMON_CLAUDE_BLOCK = "## Loop Engineering\n\nUse `.agent-ops/CLAUDE.md` as the canonical Loop Engineering specification for this project.\n";
4
- function managedRules(id, context) {
5
- const instructionFile = id === "codex" ? "AGENTS.md" : "CLAUDE.md";
17
+ import { findSurfaceById, findSurfaceByPath, isWritableSurface } from "./surfaces.js";
18
+ export const HARNESS_IDS = [
19
+ "codex",
20
+ "claude",
21
+ "opencode"
22
+ ];
23
+ function isRecord(value) {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+ function parseJsonSource(source) {
27
+ if (typeof source !== "string") {
28
+ return source;
29
+ }
30
+ if (source.trim().length === 0) {
31
+ return {};
32
+ }
33
+ try {
34
+ return JSON.parse(source);
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ function jsonHookRegistered(control, source, capabilities) {
41
+ if (control.buildHooks === undefined ||
42
+ control.isManagedHandler === undefined) {
43
+ return false;
44
+ }
45
+ const events = Object.keys(control.buildHooks(capabilities, "probe").hooks);
46
+ if (events.length === 0) {
47
+ return true;
48
+ }
49
+ const parsed = parseJsonSource(source);
50
+ if (!isRecord(parsed) || !isRecord(parsed.hooks)) {
51
+ return false;
52
+ }
53
+ const hooks = parsed.hooks;
54
+ return events.every((event) => {
55
+ const groups = hooks[event];
56
+ return (Array.isArray(groups) &&
57
+ groups.some((group) => isRecord(group) &&
58
+ Array.isArray(group.hooks) &&
59
+ group.hooks.some(control.isManagedHandler)));
60
+ });
61
+ }
62
+ function runtimeFailureResult(capability, registrations, remedy) {
63
+ const runtimeFailure = registrations.find((registration) => registration.capability === capability)?.runtimeFailure;
64
+ return {
65
+ action: runtimeFailure === "fail-closed" ? "block" : "continue",
66
+ status: "UNKNOWN",
67
+ code: `${capability.replaceAll("-", "_").toUpperCase()}_UNAVAILABLE`,
68
+ ...(remedy === undefined ? {} : { remedy })
69
+ };
70
+ }
71
+ function createJsonDescriptor(options) {
72
+ const control = {
73
+ instructionFile: options.instructionFile,
74
+ routing: options.routing,
75
+ hookPath: options.hookPath,
76
+ surfaces: options.surfaces,
77
+ ownSettingsKeys: options.ownSettingsKeys,
78
+ buildHooks: options.buildHooks,
79
+ mergeHooks: options.mergeHooks,
80
+ stripHooks: options.stripHooks,
81
+ isManagedHandler: options.isManagedHandler,
82
+ registrations: options.registrations,
83
+ plan: (context) => planCommonHarnessContribution(options.id, context),
84
+ hookRegistered: (source, capabilities) => jsonHookRegistered(control, source, capabilities)
85
+ };
86
+ return {
87
+ id: options.id,
88
+ control,
89
+ runtime: {
90
+ normalizeInput: options.normalizeInput,
91
+ formatOutput: options.formatOutput,
92
+ formatRuntimeFailure: (event, capability, remedy) => options.formatOutput(event, runtimeFailureResult(capability, options.registrations, remedy))
93
+ }
94
+ };
95
+ }
96
+ const AGENTS_ROUTING = {
97
+ desired: "## Loop Engineering\n\nLoad `.agent-ops/AGENTS.md` as the agent-ops managed baseline.\nProject-specific instructions in this file remain authoritative.\n",
98
+ legacy: [
99
+ "## Loop Engineering\n\nUse `.agent-ops/AGENTS.md` as the canonical Loop Engineering specification for this project.\n"
100
+ ]
101
+ };
102
+ const CLAUDE_ROUTING = {
103
+ desired: "## Loop Engineering\n\nLoad `.agent-ops/CLAUDE.md` as the agent-ops managed baseline.\nProject-specific instructions in this file remain authoritative.\n",
104
+ legacy: [
105
+ "## Loop Engineering\n\nUse `.agent-ops/CLAUDE.md` as the canonical Loop Engineering specification for this project.\n"
106
+ ]
107
+ };
108
+ const DESCRIPTORS = {
109
+ codex: createJsonDescriptor({
110
+ id: "codex",
111
+ instructionFile: "AGENTS.md",
112
+ routing: AGENTS_ROUTING,
113
+ hookPath: ".codex/hooks.json",
114
+ surfaces: codexSurfaces,
115
+ ownSettingsKeys: ["hooks", "description"],
116
+ buildHooks: (capabilities, runtimePath) => buildCodexHookConfig(capabilities, runtimePath),
117
+ mergeHooks: (existing, managed) => mergeCodexHookConfig(existing, managed),
118
+ stripHooks: (existing) => stripCodexManagedHooks(existing),
119
+ isManagedHandler: (handler) => isRecord(handler) &&
120
+ typeof handler.command === "string" &&
121
+ handler.command.includes(CODEX_MANAGED_MARKER),
122
+ registrations: CODEX_CAPABILITY_REGISTRATIONS,
123
+ normalizeInput: normalizeCodexHookInput,
124
+ formatOutput: (event, result) => {
125
+ const output = codexHookOutput(event, result);
126
+ return { exitCode: 0, stdout: output.stdout, stderr: "" };
127
+ }
128
+ }),
129
+ claude: createJsonDescriptor({
130
+ id: "claude",
131
+ instructionFile: "CLAUDE.md",
132
+ routing: CLAUDE_ROUTING,
133
+ hookPath: ".claude/settings.json",
134
+ surfaces: claudeSurfaces,
135
+ ownSettingsKeys: ["hooks"],
136
+ buildHooks: (capabilities, runtimePath) => buildClaudeHookSettings(capabilities, runtimePath),
137
+ mergeHooks: (existing, managed) => mergeClaudeSettings(existing, managed),
138
+ stripHooks: (existing) => stripClaudeManagedHooks(existing),
139
+ isManagedHandler: isClaudeManagedHandler,
140
+ registrations: CLAUDE_CAPABILITY_REGISTRATIONS,
141
+ normalizeInput: normalizeClaudeHookInput,
142
+ formatOutput: (event, result) => claudeHookOutput(event, result)
143
+ }),
144
+ opencode: {
145
+ id: "opencode",
146
+ control: {
147
+ instructionFile: "AGENTS.md",
148
+ routing: AGENTS_ROUTING,
149
+ hookPath: opencodePluginTarget("project").path,
150
+ hookPathForScope: (scope, root) => opencodePluginTarget(scope, root).path,
151
+ surfaces: opencodeSurfaces,
152
+ registrations: OPENCODE_CAPABILITY_REGISTRATIONS,
153
+ plan: (context) => planCommonHarnessContribution("opencode", context),
154
+ hookRegistered: (source, capabilities) => isOpencodePluginRegistered(source, capabilities)
155
+ },
156
+ runtime: {
157
+ normalizeInput: normalizeOpencodeHookInput,
158
+ formatOutput: (event, result) => opencodeHookOutput(event, result),
159
+ formatRuntimeFailure: (event, capability, remedy) => opencodeHookOutput(event, runtimeFailureResult(capability, OPENCODE_CAPABILITY_REGISTRATIONS, remedy))
160
+ }
161
+ }
162
+ };
163
+ export function harnessDescriptor(id) {
164
+ const descriptor = DESCRIPTORS[id];
165
+ if (descriptor === undefined) {
166
+ throw new AgentOpsError("HARNESS_ADAPTER_MISSING", `Missing harness adapter: ${id}`);
167
+ }
168
+ return descriptor;
169
+ }
170
+ export function harnessHookPath(id, scope, root) {
171
+ const descriptor = harnessDescriptor(id);
172
+ return descriptor.control.hookPathForScope?.(scope, root) ??
173
+ descriptor.control.hookPath;
174
+ }
175
+ export function harnessSurfaces(id, scope, root) {
176
+ return harnessDescriptor(id).control.surfaces(scope, root);
177
+ }
178
+ export function harnessSurfaceById(id, scope, surfaceId, root) {
179
+ return harnessSurfaces(id, scope, root).find((surface) => surface.id === surfaceId);
180
+ }
181
+ export function harnessSurfaceByPath(id, scope, path, root) {
182
+ return harnessSurfaces(id, scope, root).find((surface) => surface.path === path);
183
+ }
184
+ export function selectHarnessHookSurface(options) {
185
+ const descriptor = harnessDescriptor(options.harness);
186
+ if (descriptor.control.buildHooks === undefined) {
187
+ throw new AgentOpsError("HOOK_TARGET_UNSUPPORTED", `Harness hook targets are not supported for ${options.harness}.`);
188
+ }
189
+ const surfaces = harnessSurfaces(options.harness, options.scope, options.root);
190
+ const selected = options.surfaceId === undefined
191
+ ? options.persistedPath === undefined
192
+ ? surfaces.find((surface) => surface.access === "managed-default" &&
193
+ surface.representation === "json")
194
+ : findSurfaceByPath(surfaces, options.persistedPath)
195
+ : findSurfaceById(surfaces, options.surfaceId);
196
+ if (selected === undefined ||
197
+ selected.scope !== options.scope ||
198
+ !isWritableSurface(selected) ||
199
+ selected.representation !== "json") {
200
+ const requested = options.surfaceId ?? options.persistedPath ?? "default surface";
201
+ throw new AgentOpsError("HOOK_TARGET_INVALID", `Harness hook target is not a writable ${options.scope} surface: ${options.harness}=${requested}.`);
202
+ }
203
+ return selected;
204
+ }
205
+ export function isHarnessId(value) {
206
+ return Object.hasOwn(DESCRIPTORS, value);
207
+ }
208
+ /**
209
+ * `both` predates opencode and remains an input alias. `all` follows the
210
+ * registry and selects every registered harness.
211
+ */
212
+ const HARNESS_ALIASES = {
213
+ all: HARNESS_IDS,
214
+ both: ["codex", "claude"]
215
+ };
216
+ export function resolveHarnessSelection(value) {
217
+ const alias = HARNESS_ALIASES[value.trim()];
218
+ if (alias !== undefined) {
219
+ return [...alias];
220
+ }
221
+ const ids = value.split(",").map((entry) => entry.trim());
222
+ return ids.length > 0 &&
223
+ ids.every(isHarnessId) &&
224
+ new Set(ids).size === ids.length
225
+ ? ids
226
+ : null;
227
+ }
228
+ export const COMMON_AGENTS_BLOCK = DESCRIPTORS.codex.control.routing.desired;
229
+ export const COMMON_CLAUDE_BLOCK = DESCRIPTORS.claude.control.routing.desired;
230
+ export function managedRules(descriptor, context) {
6
231
  const lines = [
7
232
  "# Loop Engineering",
8
233
  "",
@@ -22,44 +247,75 @@ function managedRules(id, context) {
22
247
  lines.push("Advisory lifecycle summaries and local logs are informational. Advisory", "failures must remain fail-open and cannot become verification evidence.", "");
23
248
  }
24
249
  if (context.capabilities.includes("command-policy")) {
25
- lines.push("Command policy guards high-confidence unsafe actions. Optional Stop", "verification never marks a task complete by itself.", "");
250
+ lines.push("Command policy guards high-confidence unsafe actions. Explicitly enabled", "Stop verification is report-only and never marks a task complete by itself.", "");
26
251
  }
27
- lines.push(`This file is routed from the active ${instructionFile}.`, "");
252
+ lines.push(`This file is routed from the active ${descriptor.control.instructionFile}.`, "");
28
253
  return lines.join("\n");
29
254
  }
255
+ function instructionStem(instructionFile) {
256
+ return instructionFile.replace(/\.md$/iu, "").toLowerCase();
257
+ }
258
+ export function rulesArtifactId(descriptor) {
259
+ return `${instructionStem(descriptor.control.instructionFile)}-rules`;
260
+ }
261
+ export function routingBlockId(id, scope, descriptor = harnessDescriptor(id)) {
262
+ return scope === "project" && descriptor.control.instructionFile === "AGENTS.md"
263
+ ? "agents-routing"
264
+ : `${id}-routing`;
265
+ }
266
+ function dedupeByPath(entries) {
267
+ const kept = [];
268
+ for (const entry of entries) {
269
+ const duplicate = kept.some((existing) => existing.path === entry.path && existing.content === entry.content);
270
+ if (!duplicate) {
271
+ kept.push(entry);
272
+ }
273
+ }
274
+ return kept;
275
+ }
276
+ async function planCommonHarnessContribution(id, context) {
277
+ const descriptor = harnessDescriptor(id);
278
+ const artifacts = [
279
+ {
280
+ id: rulesArtifactId(descriptor),
281
+ path: `.agent-ops/${descriptor.control.instructionFile}`,
282
+ content: managedRules(descriptor, context)
283
+ }
284
+ ];
285
+ if (id === "opencode" && context.runtimePath !== undefined) {
286
+ const plugin = buildOpencodePlugin(context.capabilities, context.runtimePath);
287
+ if (plugin !== null) {
288
+ artifacts.push({
289
+ id: "opencode-plugin",
290
+ path: context.opencodePluginPath ??
291
+ opencodePluginTarget(context.scope, context.root).path,
292
+ content: plugin
293
+ });
294
+ }
295
+ }
296
+ return {
297
+ artifacts,
298
+ blocks: [
299
+ {
300
+ id: routingBlockId(id, context.scope, descriptor),
301
+ path: context.scope === "project"
302
+ ? descriptor.control.instructionFile
303
+ : `.${id}/${descriptor.control.instructionFile}`,
304
+ version: 1,
305
+ content: descriptor.control.routing.desired
306
+ }
307
+ ]
308
+ };
309
+ }
30
310
  export function commonHarnessAdapters() {
31
- return ["codex", "claude"].map((id) => {
32
- const instructionFile = id === "codex" ? "AGENTS.md" : "CLAUDE.md";
33
- const blockContent = id === "codex" ? COMMON_AGENTS_BLOCK : COMMON_CLAUDE_BLOCK;
311
+ return HARNESS_IDS.map((id) => {
312
+ const descriptor = harnessDescriptor(id);
34
313
  return {
35
314
  id,
36
- async plan(context) {
37
- return {
38
- artifacts: [
39
- {
40
- id: `${id}-rules`,
41
- path: `.agent-ops/${instructionFile}`,
42
- content: managedRules(id, context)
43
- }
44
- ],
45
- blocks: [
46
- {
47
- id: `${id}-routing`,
48
- path: context.scope === "project"
49
- ? instructionFile
50
- : `.${id}/${instructionFile}`,
51
- version: 1,
52
- content: blockContent
53
- }
54
- ]
55
- };
56
- }
315
+ plan: (context) => descriptor.control.plan(context)
57
316
  };
58
317
  });
59
318
  }
60
- export function requestedHarnessIds(harness) {
61
- return harness === "both" ? ["codex", "claude"] : [harness];
62
- }
63
319
  function selectAdapter(id, adapters) {
64
320
  const matches = adapters.filter((adapter) => adapter.id === id);
65
321
  if (matches.length === 0) {
@@ -75,7 +331,7 @@ function selectAdapter(id, adapters) {
75
331
  return adapter;
76
332
  }
77
333
  export async function planHarnessContributions(harness, context, adapters) {
78
- const selectedAdapters = requestedHarnessIds(harness).map((id) => selectAdapter(id, adapters));
334
+ const selectedAdapters = harness.map((id) => selectAdapter(id, adapters));
79
335
  const artifacts = [];
80
336
  const blocks = [];
81
337
  for (const adapter of selectedAdapters) {
@@ -83,5 +339,8 @@ export async function planHarnessContributions(harness, context, adapters) {
83
339
  artifacts.push(...contribution.artifacts);
84
340
  blocks.push(...contribution.blocks);
85
341
  }
86
- return { artifacts, blocks };
342
+ return {
343
+ artifacts: dedupeByPath(artifacts),
344
+ blocks: dedupeByPath(blocks)
345
+ };
87
346
  }