@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.
Files changed (80) hide show
  1. package/README.md +88 -13
  2. package/dist/packages/cli/src/args.js +39 -6
  3. package/dist/packages/cli/src/bin.js +33 -11
  4. package/dist/packages/cli/src/cli.js +8 -8
  5. package/dist/packages/cli/src/commands/doctor.js +31 -9
  6. package/dist/packages/cli/src/commands/hook.js +18 -16
  7. package/dist/packages/cli/src/commands/init.js +29 -7
  8. package/dist/packages/cli/src/commands/review.js +5 -1
  9. package/dist/packages/cli/src/commands/uninstall.js +6 -5
  10. package/dist/packages/cli/src/commands/update.js +13 -5
  11. package/dist/packages/cli/src/context.js +9 -3
  12. package/dist/packages/cli/src/hook-process.js +109 -7
  13. package/dist/packages/cli/src/plan-output.js +9 -4
  14. package/dist/packages/cli/src/public-plan.js +62 -0
  15. package/dist/packages/cli/src/ui.js +242 -1
  16. package/dist/packages/cli/src/version.js +1 -1
  17. package/dist/packages/cli/src/wizard.js +68 -2
  18. package/dist/runtime/src/adapters/claude/config.js +0 -8
  19. package/dist/runtime/src/adapters/claude/events.js +26 -0
  20. package/dist/runtime/src/adapters/claude/output.js +6 -6
  21. package/dist/runtime/src/adapters/claude/surfaces.js +70 -0
  22. package/dist/runtime/src/adapters/codex/config.js +29 -11
  23. package/dist/runtime/src/adapters/codex/events.js +26 -0
  24. package/dist/runtime/src/adapters/codex/output.js +10 -0
  25. package/dist/runtime/src/adapters/codex/surfaces.js +12 -0
  26. package/dist/runtime/src/adapters/opencode/config.js +170 -0
  27. package/dist/runtime/src/adapters/opencode/events.js +49 -0
  28. package/dist/runtime/src/adapters/opencode/input.js +32 -0
  29. package/dist/runtime/src/adapters/opencode/output.js +23 -0
  30. package/dist/runtime/src/adapters/opencode/surfaces.js +23 -0
  31. package/dist/runtime/src/config/explain.js +7 -0
  32. package/dist/runtime/src/config/hash.js +24 -0
  33. package/dist/runtime/src/config/merge.js +6 -2
  34. package/dist/runtime/src/config/migrate.js +19 -4
  35. package/dist/runtime/src/contracts.js +10 -1
  36. package/dist/runtime/src/fs/manifest.js +32 -1
  37. package/dist/runtime/src/fs/transaction.js +14 -2
  38. package/dist/runtime/src/hooks/advisory.js +16 -0
  39. package/dist/runtime/src/hooks/stop-service.js +70 -0
  40. package/dist/runtime/src/hooks/stop-verify.js +4 -1
  41. package/dist/runtime/src/install/doctor.js +119 -9
  42. package/dist/runtime/src/install/harness.js +296 -35
  43. package/dist/runtime/src/install/hooks.js +22 -17
  44. package/dist/runtime/src/install/ownership.js +110 -34
  45. package/dist/runtime/src/install/plan.js +207 -28
  46. package/dist/runtime/src/install/probes.js +9 -43
  47. package/dist/runtime/src/install/profiles.js +8 -1
  48. package/dist/runtime/src/install/surface-inspection.js +296 -0
  49. package/dist/runtime/src/install/surfaces.js +11 -0
  50. package/dist/runtime/src/install/uninstall.js +10 -3
  51. package/dist/runtime/src/install/update.js +8 -2
  52. package/dist/runtime/src/schema/validate.js +37 -18
  53. package/dist/runtime/src/task/service.js +3 -3
  54. package/dist/runtime/src/task/store.js +12 -3
  55. package/dist/runtime/src/verify/command-executor.js +113 -0
  56. package/dist/runtime/src/verify/evidence.js +4 -24
  57. package/dist/runtime/src/verify/service.js +20 -92
  58. package/dist/runtime/src/verify/spawn.js +6 -1
  59. package/docs/en/guides/configuration.md +83 -0
  60. package/docs/en/guides/quickstart.md +9 -0
  61. package/docs/en/guides/security.md +5 -0
  62. package/docs/en/spec/README.md +9 -0
  63. package/docs/en/spec/harness-adapters.md +66 -2
  64. package/docs/en/spec/maintenance.md +11 -0
  65. package/docs/en/spec/review.md +11 -0
  66. package/docs/zh-TW/guides/configuration.md +77 -0
  67. package/docs/zh-TW/guides/quickstart.md +9 -0
  68. package/docs/zh-TW/guides/security.md +5 -0
  69. package/docs/zh-TW/spec/README.md +9 -0
  70. package/docs/zh-TW/spec/harness-adapters.md +57 -3
  71. package/docs/zh-TW/spec/maintenance.md +11 -1
  72. package/docs/zh-TW/spec/review.md +10 -0
  73. package/package.json +8 -4
  74. package/postinstall.cjs +102 -0
  75. package/schemas/config.schema.json +55 -1
  76. package/schemas/manifest.schema.json +7 -2
  77. package/templates/common/AGENTS.block.md +2 -1
  78. package/templates/common/CLAUDE.block.md +2 -1
  79. package/dist/runtime/src/review/claude-runner.js +0 -4
  80. 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 { SCHEMA_VERSION } from "../contracts.js";
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, requestedHarnessIds } from "./harness.js";
9
- import { hookRegistrationPath, planHookRegistration } from "./hooks.js";
10
- import { resolveProfiles } from "./profiles.js";
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: SCHEMA_VERSION,
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 || existing.harness !== harness)) {
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 planBlocks(root, blocks) {
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 existing = grouped.get(block.path) ?? [];
175
- existing.push(block);
176
- grouped.set(block.path, existing);
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 [path, pathBlocks] of grouped) {
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
- operations.push({
188
- kind: "write",
189
- path,
190
- content,
191
- expectedHash: current?.hash ?? null
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 = resolveProfiles(options.profiles);
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 existing = await readExistingManifest(options.root);
224
- assertCompatibleManifest(existing?.manifest ?? null, options.scope, options.harness);
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
- const plannedBlocks = await planBlocks(options.root, contribution.blocks);
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 requestedHarnessIds(options.harness)) {
240
- const current = await readCurrentFile(options.root, hookRegistrationPath(harness, options.scope));
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: SCHEMA_VERSION,
440
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
262
441
  scope: options.scope,
263
442
  harness: options.harness,
264
443
  artifacts,
@@ -1,52 +1,18 @@
1
- import { buildClaudeHookSettings } from "../adapters/claude/config.js";
2
- import { buildCodexHookConfig } from "../adapters/codex/config.js";
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 { capabilities } = resolveProfiles(input.profiles);
39
- const claudeEvents = Object.keys(buildClaudeHookSettings(capabilities, "probe").hooks);
40
- const codexEvents = Object.keys(buildCodexHookConfig(capabilities).hooks);
41
- if (claudeEvents.length === 0 && codexEvents.length === 0) {
42
- return true;
43
- }
44
- if (input.harness !== "codex" &&
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", "optional-stop-verify"]
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
+ }