@kylecheng3146/agent-ops 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -13
- package/dist/packages/cli/src/args.js +39 -6
- package/dist/packages/cli/src/bin.js +33 -11
- package/dist/packages/cli/src/cli.js +8 -8
- package/dist/packages/cli/src/commands/doctor.js +31 -9
- package/dist/packages/cli/src/commands/hook.js +18 -16
- package/dist/packages/cli/src/commands/init.js +29 -7
- 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 +13 -5
- package/dist/packages/cli/src/context.js +9 -3
- package/dist/packages/cli/src/hook-process.js +109 -7
- 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/ui.js +242 -1
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/wizard.js +68 -2
- package/dist/runtime/src/adapters/claude/config.js +0 -8
- package/dist/runtime/src/adapters/claude/events.js +26 -0
- package/dist/runtime/src/adapters/claude/output.js +6 -6
- package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
- package/dist/runtime/src/adapters/codex/config.js +29 -11
- package/dist/runtime/src/adapters/codex/events.js +26 -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/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/stop-service.js +70 -0
- package/dist/runtime/src/hooks/stop-verify.js +4 -1
- package/dist/runtime/src/install/doctor.js +119 -9
- package/dist/runtime/src/install/harness.js +296 -35
- package/dist/runtime/src/install/hooks.js +22 -17
- package/dist/runtime/src/install/ownership.js +110 -34
- package/dist/runtime/src/install/plan.js +207 -28
- package/dist/runtime/src/install/probes.js +9 -43
- package/dist/runtime/src/install/profiles.js +8 -1
- 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 +10 -3
- package/dist/runtime/src/install/update.js +8 -2
- package/dist/runtime/src/schema/validate.js +37 -18
- 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 +83 -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 +66 -2
- package/docs/en/spec/maintenance.md +11 -0
- package/docs/en/spec/review.md +11 -0
- package/docs/zh-TW/guides/configuration.md +77 -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 +57 -3
- package/docs/zh-TW/spec/maintenance.md +11 -1
- package/docs/zh-TW/spec/review.md +10 -0
- package/package.json +8 -4
- package/postinstall.cjs +102 -0
- package/schemas/config.schema.json +55 -1
- package/schemas/manifest.schema.json +7 -2
- 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
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { CONFIG_SCHEMA_VERSION, MANIFEST_SCHEMA_VERSION } from "../contracts.js";
|
|
3
3
|
import { sha256 } from "../fs/hash.js";
|
|
4
|
-
import { applyManagedBlock, managedBlockMarkers } from "../fs/managed-block.js";
|
|
4
|
+
import { applyManagedBlock, managedBlockMarkers, removeManagedBlock } from "../fs/managed-block.js";
|
|
5
5
|
import { formatInstallManifest, parseInstallManifest } from "../fs/manifest.js";
|
|
6
6
|
import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
|
|
7
7
|
import { validateConfig } from "../schema/validate.js";
|
|
8
|
-
import { planHarnessContributions,
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { planHarnessContributions, harnessDescriptor, selectHarnessHookSurface } from "./harness.js";
|
|
9
|
+
import { assertExpectedManagedBlock, assertSupportedManifestOwnership } from "./ownership.js";
|
|
10
|
+
import { isOpencodeManagedPlugin } from "../adapters/opencode/config.js";
|
|
11
|
+
import { planHookRegistration, planHookRemoval } from "./hooks.js";
|
|
12
|
+
import { resolveCapabilities, resolveProfiles } from "./profiles.js";
|
|
11
13
|
const CONFIG_PATH = ".agent-ops/config.json";
|
|
12
14
|
const MANIFEST_PATH = ".agent-ops/manifest.json";
|
|
13
15
|
const MAX_PLANNED_FILE_BYTES = 1024 * 1024;
|
|
@@ -39,9 +41,14 @@ async function readCurrentFile(root, path) {
|
|
|
39
41
|
}
|
|
40
42
|
function formatConfig(profiles, existing) {
|
|
41
43
|
return `${JSON.stringify({
|
|
42
|
-
schemaVersion:
|
|
44
|
+
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
43
45
|
profiles,
|
|
44
46
|
verification: existing?.verification ?? { commands: [] },
|
|
47
|
+
features: existing?.features ?? {
|
|
48
|
+
stopVerification: {
|
|
49
|
+
enabled: false
|
|
50
|
+
}
|
|
51
|
+
},
|
|
45
52
|
pathMappings: existing?.pathMappings ?? [],
|
|
46
53
|
securityExceptions: existing?.securityExceptions ?? []
|
|
47
54
|
}, null, 2)}\n`;
|
|
@@ -131,9 +138,12 @@ async function readExistingManifest(root) {
|
|
|
131
138
|
hash: current.hash
|
|
132
139
|
};
|
|
133
140
|
}
|
|
134
|
-
function assertCompatibleManifest(existing, scope, harness) {
|
|
141
|
+
function assertCompatibleManifest(existing, scope, harness, allowHarnessChange) {
|
|
142
|
+
const harnessChanged = existing !== null &&
|
|
143
|
+
(existing.harness.length !== harness.length ||
|
|
144
|
+
!existing.harness.every((id) => harness.includes(id)));
|
|
135
145
|
if (existing !== null &&
|
|
136
|
-
(existing.scope !== scope ||
|
|
146
|
+
(existing.scope !== scope || (harnessChanged && !allowHarnessChange))) {
|
|
137
147
|
throw new AgentOpsError("INSTALL_ALREADY_CONFIGURED", "Use update to change the scope or harness of an existing installation.");
|
|
138
148
|
}
|
|
139
149
|
}
|
|
@@ -144,6 +154,11 @@ function findOwnedArtifact(manifest, path) {
|
|
|
144
154
|
async function planArtifact(root, artifact, existingManifest) {
|
|
145
155
|
const current = await readCurrentFile(root, artifact.path);
|
|
146
156
|
const owned = findOwnedArtifact(existingManifest, artifact.path);
|
|
157
|
+
if (artifact.id === "opencode-plugin" &&
|
|
158
|
+
current !== null &&
|
|
159
|
+
!isOpencodeManagedPlugin(current.content)) {
|
|
160
|
+
throw new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", `The recorded opencode plugin is not an agent-ops managed plugin: ${artifact.path}`);
|
|
161
|
+
}
|
|
147
162
|
if (current !== null && owned === undefined) {
|
|
148
163
|
throw new AgentOpsError("UNMANAGED_INSTALL_PATH", `Refusing to replace an unmanaged install artifact: ${artifact.path}`);
|
|
149
164
|
}
|
|
@@ -168,28 +183,82 @@ async function planArtifact(root, artifact, existingManifest) {
|
|
|
168
183
|
}
|
|
169
184
|
};
|
|
170
185
|
}
|
|
171
|
-
async function
|
|
186
|
+
async function planArtifactRemoval(root, artifact) {
|
|
187
|
+
const current = await readCurrentFile(root, artifact.path);
|
|
188
|
+
if (current === null || current.hash !== artifact.hash) {
|
|
189
|
+
throw new AgentOpsError("MANAGED_ARTIFACT_CHANGED", `Managed artifact changed after installation: ${artifact.path}`);
|
|
190
|
+
}
|
|
191
|
+
if (artifact.id === "opencode-plugin" &&
|
|
192
|
+
!isOpencodeManagedPlugin(current.content)) {
|
|
193
|
+
throw new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", `The recorded opencode plugin is not an agent-ops managed plugin: ${artifact.path}`);
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
kind: "remove",
|
|
197
|
+
path: artifact.path,
|
|
198
|
+
expectedHash: current.hash
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function markerKey(path, id) {
|
|
202
|
+
return `${pathKey(path)}\0${id}`;
|
|
203
|
+
}
|
|
204
|
+
async function planBlocks(root, blocks, removals = [], expectedMarkers = new Map()) {
|
|
172
205
|
const grouped = new Map();
|
|
173
206
|
for (const block of blocks) {
|
|
174
|
-
const
|
|
175
|
-
existing.
|
|
176
|
-
|
|
207
|
+
const key = pathKey(block.path);
|
|
208
|
+
const existing = grouped.get(key) ?? {
|
|
209
|
+
path: block.path,
|
|
210
|
+
blocks: [],
|
|
211
|
+
removals: []
|
|
212
|
+
};
|
|
213
|
+
existing.blocks.push(block);
|
|
214
|
+
existing.path = block.path;
|
|
215
|
+
grouped.set(key, existing);
|
|
216
|
+
}
|
|
217
|
+
for (const marker of removals) {
|
|
218
|
+
const key = pathKey(marker.path);
|
|
219
|
+
const existing = grouped.get(key) ?? {
|
|
220
|
+
path: marker.path,
|
|
221
|
+
blocks: [],
|
|
222
|
+
removals: []
|
|
223
|
+
};
|
|
224
|
+
existing.removals.push(marker);
|
|
225
|
+
grouped.set(key, existing);
|
|
177
226
|
}
|
|
178
227
|
const operations = [];
|
|
179
228
|
const records = [];
|
|
180
|
-
for (const
|
|
229
|
+
for (const { path, blocks: pathBlocks, removals: pathRemovals } of grouped.values()) {
|
|
181
230
|
const current = await readCurrentFile(root, path);
|
|
231
|
+
if (pathRemovals.length > 0 && current === null) {
|
|
232
|
+
throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block file is missing: ${path}`);
|
|
233
|
+
}
|
|
182
234
|
let content = current?.content ?? "";
|
|
235
|
+
for (const marker of pathRemovals) {
|
|
236
|
+
const expected = expectedMarkers.get(marker.id);
|
|
237
|
+
if (expected === undefined) {
|
|
238
|
+
throw new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", "The manifest contains an unsupported managed block.");
|
|
239
|
+
}
|
|
240
|
+
assertExpectedManagedBlock(content, marker, expected);
|
|
241
|
+
content = removeManagedBlock(content, marker.id);
|
|
242
|
+
}
|
|
183
243
|
for (const block of pathBlocks) {
|
|
184
244
|
content = applyManagedBlock(content, block);
|
|
185
245
|
}
|
|
186
246
|
const hash = sha256(content);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
247
|
+
if (content.length === 0 && current !== null) {
|
|
248
|
+
operations.push({
|
|
249
|
+
kind: "remove",
|
|
250
|
+
path,
|
|
251
|
+
expectedHash: current.hash
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
operations.push({
|
|
256
|
+
kind: "write",
|
|
257
|
+
path,
|
|
258
|
+
content,
|
|
259
|
+
expectedHash: current?.hash ?? null
|
|
260
|
+
});
|
|
261
|
+
}
|
|
193
262
|
for (const block of pathBlocks) {
|
|
194
263
|
const markers = managedBlockMarkers(block.id, block.version);
|
|
195
264
|
records.push({
|
|
@@ -210,18 +279,76 @@ export async function createInstallPlan(options) {
|
|
|
210
279
|
.test(options.toolkitVersion)) {
|
|
211
280
|
throw new AgentOpsError("INVALID_TOOLKIT_VERSION", "Toolkit version must be a valid semantic version.");
|
|
212
281
|
}
|
|
213
|
-
const resolved =
|
|
282
|
+
const resolved = options.existingConfig === undefined
|
|
283
|
+
? resolveProfiles(options.profiles)
|
|
284
|
+
: resolveCapabilities(options.existingConfig.value);
|
|
285
|
+
const existing = await readExistingManifest(options.root);
|
|
286
|
+
assertCompatibleManifest(existing?.manifest ?? null, options.scope, options.harness, options.allowHarnessChange === true);
|
|
287
|
+
const existingOpencodePluginPath = existing?.manifest.artifacts.find(({ id }) => id === "opencode-plugin")?.path;
|
|
288
|
+
const explicitHookTargets = new Map();
|
|
289
|
+
for (const target of options.hookTargets ?? []) {
|
|
290
|
+
if (explicitHookTargets.has(target.harness) ||
|
|
291
|
+
!options.harness.includes(target.harness)) {
|
|
292
|
+
throw new AgentOpsError("HOOK_TARGET_INVALID", `Hook target must name one selected harness exactly once: ${target.harness}.`);
|
|
293
|
+
}
|
|
294
|
+
if (harnessDescriptor(target.harness).control.buildHooks === undefined) {
|
|
295
|
+
throw new AgentOpsError("HOOK_TARGET_UNSUPPORTED", `Harness hook targets are not supported for ${target.harness}.`);
|
|
296
|
+
}
|
|
297
|
+
explicitHookTargets.set(target.harness, target.surfaceId);
|
|
298
|
+
}
|
|
299
|
+
if (options.hookRuntimePath === undefined &&
|
|
300
|
+
explicitHookTargets.size > 0) {
|
|
301
|
+
throw new AgentOpsError("HOOK_TARGET_REQUIRES_RUNTIME", "An explicit hook target requires a hook runtime path.");
|
|
302
|
+
}
|
|
214
303
|
const contribution = await planHarnessContributions(options.harness, {
|
|
304
|
+
root: options.root,
|
|
215
305
|
scope: options.scope,
|
|
216
306
|
profiles: resolved.profiles,
|
|
217
307
|
capabilities: resolved.capabilities,
|
|
308
|
+
...(options.hookRuntimePath === undefined
|
|
309
|
+
? {}
|
|
310
|
+
: { runtimePath: options.hookRuntimePath }),
|
|
218
311
|
...(options.toolkitVersion === undefined
|
|
219
312
|
? {}
|
|
220
|
-
: { toolkitVersion: options.toolkitVersion })
|
|
313
|
+
: { toolkitVersion: options.toolkitVersion }),
|
|
314
|
+
...(existingOpencodePluginPath === undefined
|
|
315
|
+
? {}
|
|
316
|
+
: { opencodePluginPath: existingOpencodePluginPath })
|
|
221
317
|
}, options.adapters);
|
|
222
318
|
assertUniqueContributions(contribution.artifacts, contribution.blocks);
|
|
223
|
-
const
|
|
224
|
-
|
|
319
|
+
const reconcileExisting = existing !== null;
|
|
320
|
+
const expectedExistingMarkers = reconcileExisting
|
|
321
|
+
? assertSupportedManifestOwnership(existing.manifest, options.root)
|
|
322
|
+
: new Map();
|
|
323
|
+
const desiredArtifactPaths = new Set([
|
|
324
|
+
pathKey(CONFIG_PATH),
|
|
325
|
+
...contribution.artifacts.map(({ path }) => pathKey(path))
|
|
326
|
+
]);
|
|
327
|
+
const preservedArtifacts = options.hookRuntimePath === undefined &&
|
|
328
|
+
options.allowHarnessChange === true &&
|
|
329
|
+
options.harness.includes("opencode")
|
|
330
|
+
? (existing?.manifest.artifacts.filter(({ id }) => id === "opencode-plugin") ?? [])
|
|
331
|
+
: [];
|
|
332
|
+
for (const artifact of preservedArtifacts) {
|
|
333
|
+
const current = await readCurrentFile(options.root, artifact.path);
|
|
334
|
+
if (current === null || current.hash !== artifact.hash) {
|
|
335
|
+
throw new AgentOpsError("MANAGED_ARTIFACT_CHANGED", `Managed artifact changed after installation: ${artifact.path}`);
|
|
336
|
+
}
|
|
337
|
+
if (artifact.id === "opencode-plugin" &&
|
|
338
|
+
!isOpencodeManagedPlugin(current.content)) {
|
|
339
|
+
throw new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", `The recorded opencode plugin is not an agent-ops managed plugin: ${artifact.path}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
for (const artifact of preservedArtifacts) {
|
|
343
|
+
desiredArtifactPaths.add(pathKey(artifact.path));
|
|
344
|
+
}
|
|
345
|
+
const artifactsToRemove = reconcileExisting
|
|
346
|
+
? existing.manifest.artifacts.filter(({ path }) => !desiredArtifactPaths.has(pathKey(path)))
|
|
347
|
+
: [];
|
|
348
|
+
const desiredMarkerKeys = new Set(contribution.blocks.map(({ path, id }) => markerKey(path, id)));
|
|
349
|
+
const markersToRemove = reconcileExisting
|
|
350
|
+
? existing.manifest.markers.filter(({ path, id }) => !desiredMarkerKeys.has(markerKey(path, id)))
|
|
351
|
+
: [];
|
|
225
352
|
const operations = [];
|
|
226
353
|
const artifacts = [];
|
|
227
354
|
const config = await planConfig(options.root, resolved.profiles, existing?.manifest ?? null, options.existingConfig);
|
|
@@ -232,15 +359,35 @@ export async function createInstallPlan(options) {
|
|
|
232
359
|
operations.push(planned.operation);
|
|
233
360
|
artifacts.push(planned.record);
|
|
234
361
|
}
|
|
235
|
-
|
|
362
|
+
artifacts.push(...preservedArtifacts);
|
|
363
|
+
for (const artifact of artifactsToRemove) {
|
|
364
|
+
operations.push(await planArtifactRemoval(options.root, artifact));
|
|
365
|
+
}
|
|
366
|
+
const plannedBlocks = await planBlocks(options.root, contribution.blocks, markersToRemove, expectedExistingMarkers);
|
|
236
367
|
operations.push(...plannedBlocks.operations);
|
|
237
368
|
const hooks = [];
|
|
238
369
|
if (options.hookRuntimePath !== undefined) {
|
|
239
|
-
for (const harness of
|
|
240
|
-
|
|
370
|
+
for (const harness of options.harness) {
|
|
371
|
+
if (harnessDescriptor(harness).control.buildHooks === undefined) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const existingHook = existing?.manifest.hooks?.find(({ harness: recordHarness }) => recordHarness === harness);
|
|
375
|
+
const selectedSurface = selectHarnessHookSurface({
|
|
376
|
+
harness,
|
|
377
|
+
scope: options.scope,
|
|
378
|
+
root: options.root,
|
|
379
|
+
...(explicitHookTargets.has(harness)
|
|
380
|
+
? { surfaceId: explicitHookTargets.get(harness) }
|
|
381
|
+
: existingHook === undefined
|
|
382
|
+
? {}
|
|
383
|
+
: { persistedPath: existingHook.path })
|
|
384
|
+
});
|
|
385
|
+
const hookPath = selectedSurface.path;
|
|
386
|
+
const current = await readCurrentFile(options.root, hookPath);
|
|
241
387
|
const planned = planHookRegistration({
|
|
242
388
|
harness,
|
|
243
389
|
scope: options.scope,
|
|
390
|
+
path: hookPath,
|
|
244
391
|
capabilities: resolved.capabilities,
|
|
245
392
|
runtimePath: options.hookRuntimePath,
|
|
246
393
|
currentSource: current?.content ?? null
|
|
@@ -252,13 +399,45 @@ export async function createInstallPlan(options) {
|
|
|
252
399
|
kind: "write",
|
|
253
400
|
path: planned.record.path,
|
|
254
401
|
content: planned.content,
|
|
255
|
-
expectedHash: current?.hash ?? null
|
|
402
|
+
expectedHash: current?.hash ?? null,
|
|
403
|
+
disclosure: planned.disclosure
|
|
256
404
|
});
|
|
257
405
|
hooks.push(planned.record);
|
|
258
406
|
}
|
|
259
407
|
}
|
|
408
|
+
else if (existing !== null) {
|
|
409
|
+
const selectedHarnesses = new Set(options.harness);
|
|
410
|
+
hooks.push(...(existing.manifest.hooks ?? []).filter(({ harness }) => selectedHarnesses.has(harness)));
|
|
411
|
+
}
|
|
412
|
+
if (reconcileExisting) {
|
|
413
|
+
const desiredHookPaths = new Set(hooks.map(({ path }) => pathKey(path)));
|
|
414
|
+
for (const hook of existing.manifest.hooks ?? []) {
|
|
415
|
+
if (desiredHookPaths.has(pathKey(hook.path))) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const current = await readCurrentFile(options.root, hook.path);
|
|
419
|
+
if (current === null) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const removal = planHookRemoval(hook, current.content);
|
|
423
|
+
operations.push(removal.content === null
|
|
424
|
+
? {
|
|
425
|
+
kind: "remove",
|
|
426
|
+
path: hook.path,
|
|
427
|
+
expectedHash: current.hash,
|
|
428
|
+
disclosure: removal.disclosure
|
|
429
|
+
}
|
|
430
|
+
: {
|
|
431
|
+
kind: "write",
|
|
432
|
+
path: hook.path,
|
|
433
|
+
content: removal.content,
|
|
434
|
+
expectedHash: current.hash,
|
|
435
|
+
disclosure: removal.disclosure
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
260
439
|
const manifest = {
|
|
261
|
-
schemaVersion:
|
|
440
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
262
441
|
scope: options.scope,
|
|
263
442
|
harness: options.harness,
|
|
264
443
|
artifacts,
|
|
@@ -1,52 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { resolveProfiles } from "./profiles.js";
|
|
4
|
-
const CLAUDE_HOOK_MARKER = "--managed-by=agent-ops";
|
|
5
|
-
const CODEX_COMMAND_PREFIX = "agent-ops hook codex ";
|
|
6
|
-
function isRecord(value) {
|
|
7
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
-
}
|
|
9
|
-
function hasManagedHandler(source, events, isManaged) {
|
|
10
|
-
if (!isRecord(source) || !isRecord(source.hooks)) {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
const registered = source.hooks;
|
|
14
|
-
return events.every((event) => {
|
|
15
|
-
const groups = registered[event];
|
|
16
|
-
return (Array.isArray(groups) &&
|
|
17
|
-
groups.some((group) => isRecord(group) &&
|
|
18
|
-
Array.isArray(group.hooks) &&
|
|
19
|
-
group.hooks.some(isManaged)));
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
function isManagedClaudeHandler(handler) {
|
|
23
|
-
return (isRecord(handler) &&
|
|
24
|
-
Array.isArray(handler.args) &&
|
|
25
|
-
handler.args.includes(CLAUDE_HOOK_MARKER));
|
|
26
|
-
}
|
|
27
|
-
function isManagedCodexHandler(handler) {
|
|
28
|
-
return (isRecord(handler) &&
|
|
29
|
-
typeof handler.command === "string" &&
|
|
30
|
-
handler.command.startsWith(CODEX_COMMAND_PREFIX));
|
|
31
|
-
}
|
|
1
|
+
import { harnessDescriptor } from "./harness.js";
|
|
2
|
+
import { resolveCapabilities } from "./profiles.js";
|
|
32
3
|
/**
|
|
33
4
|
* Hook registration is satisfied when every hook event implied by the
|
|
34
5
|
* installed profiles carries an agent-ops owned handler for every installed
|
|
35
6
|
* harness. Installations without hook capabilities have nothing to register.
|
|
36
7
|
*/
|
|
37
8
|
export function hookRegistrationSatisfied(input) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
!hasManagedHandler(input.claudeSettings, claudeEvents, isManagedClaudeHandler)) {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
return (input.harness === "claude" ||
|
|
49
|
-
hasManagedHandler(input.codexHooks, codexEvents, isManagedCodexHandler));
|
|
9
|
+
const capabilities = input.config.profiles.length === 0
|
|
10
|
+
? []
|
|
11
|
+
: resolveCapabilities(input.config).capabilities;
|
|
12
|
+
return input.harness.every((id) => {
|
|
13
|
+
const descriptor = harnessDescriptor(id);
|
|
14
|
+
return descriptor.control.hookRegistered(input.sources[id], capabilities);
|
|
15
|
+
});
|
|
50
16
|
}
|
|
51
17
|
/**
|
|
52
18
|
* Smoke availability stays UNKNOWN until the repository declares a
|
|
@@ -3,7 +3,7 @@ const PROFILE_ORDER = ["core", "advisory", "guardrails"];
|
|
|
3
3
|
export const PROFILE_CAPABILITIES = {
|
|
4
4
|
core: ["rules", "task", "verify", "review"],
|
|
5
5
|
advisory: ["lifecycle-summary", "local-log"],
|
|
6
|
-
guardrails: ["command-policy"
|
|
6
|
+
guardrails: ["command-policy"]
|
|
7
7
|
};
|
|
8
8
|
export function resolveProfiles(inputProfiles) {
|
|
9
9
|
if (inputProfiles.length === 0) {
|
|
@@ -26,3 +26,10 @@ export function resolveProfiles(inputProfiles) {
|
|
|
26
26
|
}
|
|
27
27
|
return { profiles: [...profiles], capabilities };
|
|
28
28
|
}
|
|
29
|
+
export function resolveCapabilities(config) {
|
|
30
|
+
const resolved = resolveProfiles(config.profiles);
|
|
31
|
+
if (config.features.stopVerification.enabled) {
|
|
32
|
+
resolved.capabilities.push("optional-stop-verify");
|
|
33
|
+
}
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|