@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.
- package/README.md +164 -13
- package/dist/packages/cli/src/args.js +40 -7
- package/dist/packages/cli/src/bin.js +35 -11
- package/dist/packages/cli/src/cli.js +7 -2
- package/dist/packages/cli/src/codex-loop-process.js +70 -0
- package/dist/packages/cli/src/commands/doctor.js +31 -9
- package/dist/packages/cli/src/commands/hook.js +33 -16
- package/dist/packages/cli/src/commands/init.js +9 -5
- package/dist/packages/cli/src/commands/review.js +5 -1
- package/dist/packages/cli/src/commands/uninstall.js +6 -5
- package/dist/packages/cli/src/commands/update.js +16 -5
- package/dist/packages/cli/src/context.js +69 -3
- package/dist/packages/cli/src/hook-process.js +231 -16
- package/dist/packages/cli/src/loop-entry.js +8 -0
- package/dist/packages/cli/src/plan-output.js +9 -4
- package/dist/packages/cli/src/public-plan.js +62 -0
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/wizard.js +36 -15
- package/dist/runtime/src/adapters/claude/config.js +57 -19
- package/dist/runtime/src/adapters/claude/events.js +33 -0
- package/dist/runtime/src/adapters/claude/output.js +8 -7
- package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
- package/dist/runtime/src/adapters/codex/config.js +66 -13
- package/dist/runtime/src/adapters/codex/events.js +33 -0
- package/dist/runtime/src/adapters/codex/output.js +10 -0
- package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
- package/dist/runtime/src/adapters/opencode/config.js +170 -0
- package/dist/runtime/src/adapters/opencode/events.js +49 -0
- package/dist/runtime/src/adapters/opencode/input.js +32 -0
- package/dist/runtime/src/adapters/opencode/output.js +23 -0
- package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
- package/dist/runtime/src/config/explain.js +7 -0
- package/dist/runtime/src/config/hash.js +24 -0
- package/dist/runtime/src/config/merge.js +6 -2
- package/dist/runtime/src/config/migrate.js +19 -4
- package/dist/runtime/src/contracts.js +10 -1
- package/dist/runtime/src/fs/managed-block.js +35 -18
- package/dist/runtime/src/fs/manifest.js +32 -1
- package/dist/runtime/src/fs/transaction.js +14 -2
- package/dist/runtime/src/hooks/advisory.js +16 -0
- package/dist/runtime/src/hooks/codex-loop.js +439 -0
- package/dist/runtime/src/hooks/stop-service.js +70 -0
- package/dist/runtime/src/hooks/stop-verify.js +4 -1
- package/dist/runtime/src/install/codex-loop.js +139 -0
- package/dist/runtime/src/install/doctor.js +182 -14
- package/dist/runtime/src/install/harness.js +294 -35
- package/dist/runtime/src/install/hooks.js +22 -17
- package/dist/runtime/src/install/ownership.js +146 -35
- package/dist/runtime/src/install/plan.js +276 -31
- package/dist/runtime/src/install/probes.js +9 -43
- package/dist/runtime/src/install/profiles.js +12 -3
- package/dist/runtime/src/install/surface-inspection.js +296 -0
- package/dist/runtime/src/install/surfaces.js +11 -0
- package/dist/runtime/src/install/uninstall.js +11 -4
- package/dist/runtime/src/install/update.js +13 -3
- package/dist/runtime/src/logging/local-log.js +25 -0
- package/dist/runtime/src/schema/validate.js +45 -19
- package/dist/runtime/src/task/service.js +3 -3
- package/dist/runtime/src/task/store.js +12 -3
- package/dist/runtime/src/verify/command-executor.js +113 -0
- package/dist/runtime/src/verify/evidence.js +4 -24
- package/dist/runtime/src/verify/service.js +20 -92
- package/dist/runtime/src/verify/spawn.js +6 -1
- package/docs/en/guides/configuration.md +159 -0
- package/docs/en/guides/quickstart.md +9 -0
- package/docs/en/guides/security.md +5 -0
- package/docs/en/spec/README.md +9 -0
- package/docs/en/spec/harness-adapters.md +104 -2
- package/docs/en/spec/maintenance.md +11 -0
- package/docs/en/spec/review.md +11 -0
- package/docs/zh-TW/guides/configuration.md +145 -0
- package/docs/zh-TW/guides/quickstart.md +9 -0
- package/docs/zh-TW/guides/security.md +5 -0
- package/docs/zh-TW/spec/README.md +9 -0
- package/docs/zh-TW/spec/harness-adapters.md +89 -3
- package/docs/zh-TW/spec/maintenance.md +11 -1
- package/docs/zh-TW/spec/review.md +10 -0
- package/package.json +4 -2
- package/schemas/config.schema.json +56 -2
- package/schemas/manifest.schema.json +19 -3
- package/templates/common/AGENTS.block.md +2 -1
- package/templates/common/CLAUDE.block.md +2 -1
- package/dist/runtime/src/review/claude-runner.js +0 -4
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
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
|
|
32
|
-
const
|
|
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
|
-
|
|
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 =
|
|
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 {
|
|
342
|
+
return {
|
|
343
|
+
artifacts: dedupeByPath(artifacts),
|
|
344
|
+
blocks: dedupeByPath(blocks)
|
|
345
|
+
};
|
|
87
346
|
}
|