@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
|
@@ -1,13 +1,16 @@
|
|
|
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 { codexHooksExplicitlyDisabled, loopLauncherArtifactId, loopSeeds, planLoopContribution, selectedLoopHarnesses } from "./codex-loop.js";
|
|
11
|
+
import { isOpencodeManagedPlugin } from "../adapters/opencode/config.js";
|
|
12
|
+
import { planHookRegistration, planHookRemoval } from "./hooks.js";
|
|
13
|
+
import { resolveCapabilities, resolveProfiles } from "./profiles.js";
|
|
11
14
|
const CONFIG_PATH = ".agent-ops/config.json";
|
|
12
15
|
const MANIFEST_PATH = ".agent-ops/manifest.json";
|
|
13
16
|
const MAX_PLANNED_FILE_BYTES = 1024 * 1024;
|
|
@@ -39,9 +42,14 @@ async function readCurrentFile(root, path) {
|
|
|
39
42
|
}
|
|
40
43
|
function formatConfig(profiles, existing) {
|
|
41
44
|
return `${JSON.stringify({
|
|
42
|
-
schemaVersion:
|
|
45
|
+
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
43
46
|
profiles,
|
|
44
47
|
verification: existing?.verification ?? { commands: [] },
|
|
48
|
+
features: existing?.features ?? {
|
|
49
|
+
stopVerification: {
|
|
50
|
+
enabled: false
|
|
51
|
+
}
|
|
52
|
+
},
|
|
45
53
|
pathMappings: existing?.pathMappings ?? [],
|
|
46
54
|
securityExceptions: existing?.securityExceptions ?? []
|
|
47
55
|
}, null, 2)}\n`;
|
|
@@ -109,7 +117,7 @@ function assertUniqueContributions(artifacts, blocks) {
|
|
|
109
117
|
}
|
|
110
118
|
const markerBoundaries = new Set();
|
|
111
119
|
for (const block of blocks) {
|
|
112
|
-
const markers = managedBlockMarkers(block.id, block.version);
|
|
120
|
+
const markers = managedBlockMarkers(block.id, block.version, block.markerStyle);
|
|
113
121
|
const key = pathKey(block.path);
|
|
114
122
|
if (ids.has(block.id) ||
|
|
115
123
|
artifactPaths.has(key) ||
|
|
@@ -131,9 +139,12 @@ async function readExistingManifest(root) {
|
|
|
131
139
|
hash: current.hash
|
|
132
140
|
};
|
|
133
141
|
}
|
|
134
|
-
function assertCompatibleManifest(existing, scope, harness) {
|
|
142
|
+
function assertCompatibleManifest(existing, scope, harness, allowHarnessChange) {
|
|
143
|
+
const harnessChanged = existing !== null &&
|
|
144
|
+
(existing.harness.length !== harness.length ||
|
|
145
|
+
!existing.harness.every((id) => harness.includes(id)));
|
|
135
146
|
if (existing !== null &&
|
|
136
|
-
(existing.scope !== scope ||
|
|
147
|
+
(existing.scope !== scope || (harnessChanged && !allowHarnessChange))) {
|
|
137
148
|
throw new AgentOpsError("INSTALL_ALREADY_CONFIGURED", "Use update to change the scope or harness of an existing installation.");
|
|
138
149
|
}
|
|
139
150
|
}
|
|
@@ -144,6 +155,11 @@ function findOwnedArtifact(manifest, path) {
|
|
|
144
155
|
async function planArtifact(root, artifact, existingManifest) {
|
|
145
156
|
const current = await readCurrentFile(root, artifact.path);
|
|
146
157
|
const owned = findOwnedArtifact(existingManifest, artifact.path);
|
|
158
|
+
if (artifact.id === "opencode-plugin" &&
|
|
159
|
+
current !== null &&
|
|
160
|
+
!isOpencodeManagedPlugin(current.content)) {
|
|
161
|
+
throw new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", `The recorded opencode plugin is not an agent-ops managed plugin: ${artifact.path}`);
|
|
162
|
+
}
|
|
147
163
|
if (current !== null && owned === undefined) {
|
|
148
164
|
throw new AgentOpsError("UNMANAGED_INSTALL_PATH", `Refusing to replace an unmanaged install artifact: ${artifact.path}`);
|
|
149
165
|
}
|
|
@@ -168,30 +184,128 @@ async function planArtifact(root, artifact, existingManifest) {
|
|
|
168
184
|
}
|
|
169
185
|
};
|
|
170
186
|
}
|
|
171
|
-
async function
|
|
187
|
+
async function planArtifactRemoval(root, artifact) {
|
|
188
|
+
const current = await readCurrentFile(root, artifact.path);
|
|
189
|
+
if (current === null || current.hash !== artifact.hash) {
|
|
190
|
+
throw new AgentOpsError("MANAGED_ARTIFACT_CHANGED", `Managed artifact changed after installation: ${artifact.path}`);
|
|
191
|
+
}
|
|
192
|
+
if (artifact.id === "opencode-plugin" &&
|
|
193
|
+
!isOpencodeManagedPlugin(current.content)) {
|
|
194
|
+
throw new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", `The recorded opencode plugin is not an agent-ops managed plugin: ${artifact.path}`);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
kind: "remove",
|
|
198
|
+
path: artifact.path,
|
|
199
|
+
expectedHash: current.hash
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async function planCreateOnceSeeds(root, seeds) {
|
|
203
|
+
const operations = [];
|
|
204
|
+
for (const seed of seeds) {
|
|
205
|
+
const current = await readCurrentFile(root, seed.path);
|
|
206
|
+
if (current === null) {
|
|
207
|
+
operations.push({
|
|
208
|
+
kind: "write",
|
|
209
|
+
path: seed.path,
|
|
210
|
+
content: seed.content,
|
|
211
|
+
expectedHash: null
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return operations;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* User-owned seed files belong to the first loop install for each harness.
|
|
219
|
+
* An existing loop launcher is the durable installation record: it prevents an
|
|
220
|
+
* update from recreating a file a user intentionally removed, while still
|
|
221
|
+
* allowing loop to be enabled later or for a newly added harness.
|
|
222
|
+
*/
|
|
223
|
+
function loopHarnessesNeedingSeeds(harnesses, existingManifest) {
|
|
224
|
+
const existingArtifactIds = new Set(existingManifest?.artifacts.map(({ id }) => id) ?? []);
|
|
225
|
+
return selectedLoopHarnesses(harnesses).filter((harness) => !existingArtifactIds.has(loopLauncherArtifactId(harness)));
|
|
226
|
+
}
|
|
227
|
+
function markerKey(path, id) {
|
|
228
|
+
return `${pathKey(path)}\0${id}`;
|
|
229
|
+
}
|
|
230
|
+
function assertLoopProfileSupport(scope, harness, capabilities) {
|
|
231
|
+
if (!capabilities.includes("project-loop")) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (scope !== "project" ||
|
|
235
|
+
!harness.some((id) => id === "codex" || id === "claude")) {
|
|
236
|
+
throw new AgentOpsError("LOOP_PROFILE_UNSUPPORTED", "The loop profile requires project scope and the Codex or Claude harness.");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function assertCodexLoopConfiguration(root, harness, capabilities) {
|
|
240
|
+
if (!capabilities.includes("project-loop") ||
|
|
241
|
+
!harness.includes("codex")) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const config = await readCurrentFile(root, ".codex/config.toml");
|
|
245
|
+
if (config !== null && codexHooksExplicitlyDisabled(config.content)) {
|
|
246
|
+
throw new AgentOpsError("CODEX_LOOP_HOOKS_DISABLED", "Codex loop installation requires [features] hooks = true; the existing .codex/config.toml explicitly disables hooks.");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function planBlocks(root, blocks, removals = [], expectedMarkers = new Map()) {
|
|
172
250
|
const grouped = new Map();
|
|
173
251
|
for (const block of blocks) {
|
|
174
|
-
const
|
|
175
|
-
existing.
|
|
176
|
-
|
|
252
|
+
const key = pathKey(block.path);
|
|
253
|
+
const existing = grouped.get(key) ?? {
|
|
254
|
+
path: block.path,
|
|
255
|
+
blocks: [],
|
|
256
|
+
removals: []
|
|
257
|
+
};
|
|
258
|
+
existing.blocks.push(block);
|
|
259
|
+
existing.path = block.path;
|
|
260
|
+
grouped.set(key, existing);
|
|
261
|
+
}
|
|
262
|
+
for (const marker of removals) {
|
|
263
|
+
const key = pathKey(marker.path);
|
|
264
|
+
const existing = grouped.get(key) ?? {
|
|
265
|
+
path: marker.path,
|
|
266
|
+
blocks: [],
|
|
267
|
+
removals: []
|
|
268
|
+
};
|
|
269
|
+
existing.removals.push(marker);
|
|
270
|
+
grouped.set(key, existing);
|
|
177
271
|
}
|
|
178
272
|
const operations = [];
|
|
179
273
|
const records = [];
|
|
180
|
-
for (const
|
|
274
|
+
for (const { path, blocks: pathBlocks, removals: pathRemovals } of grouped.values()) {
|
|
181
275
|
const current = await readCurrentFile(root, path);
|
|
276
|
+
if (pathRemovals.length > 0 && current === null) {
|
|
277
|
+
throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block file is missing: ${path}`);
|
|
278
|
+
}
|
|
182
279
|
let content = current?.content ?? "";
|
|
280
|
+
for (const marker of pathRemovals) {
|
|
281
|
+
const expected = expectedMarkers.get(marker.id);
|
|
282
|
+
if (expected === undefined) {
|
|
283
|
+
throw new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", "The manifest contains an unsupported managed block.");
|
|
284
|
+
}
|
|
285
|
+
assertExpectedManagedBlock(content, marker, expected);
|
|
286
|
+
content = removeManagedBlock(content, marker.id, expected.markerStyle);
|
|
287
|
+
}
|
|
183
288
|
for (const block of pathBlocks) {
|
|
184
289
|
content = applyManagedBlock(content, block);
|
|
185
290
|
}
|
|
186
291
|
const hash = sha256(content);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
292
|
+
if (content.length === 0 && current !== null) {
|
|
293
|
+
operations.push({
|
|
294
|
+
kind: "remove",
|
|
295
|
+
path,
|
|
296
|
+
expectedHash: current.hash
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
operations.push({
|
|
301
|
+
kind: "write",
|
|
302
|
+
path,
|
|
303
|
+
content,
|
|
304
|
+
expectedHash: current?.hash ?? null
|
|
305
|
+
});
|
|
306
|
+
}
|
|
193
307
|
for (const block of pathBlocks) {
|
|
194
|
-
const markers = managedBlockMarkers(block.id, block.version);
|
|
308
|
+
const markers = managedBlockMarkers(block.id, block.version, block.markerStyle);
|
|
195
309
|
records.push({
|
|
196
310
|
id: block.id,
|
|
197
311
|
path,
|
|
@@ -210,18 +324,93 @@ export async function createInstallPlan(options) {
|
|
|
210
324
|
.test(options.toolkitVersion)) {
|
|
211
325
|
throw new AgentOpsError("INVALID_TOOLKIT_VERSION", "Toolkit version must be a valid semantic version.");
|
|
212
326
|
}
|
|
213
|
-
const resolved =
|
|
214
|
-
|
|
327
|
+
const resolved = options.existingConfig === undefined
|
|
328
|
+
? resolveProfiles(options.profiles)
|
|
329
|
+
: resolveCapabilities(options.existingConfig.value);
|
|
330
|
+
assertLoopProfileSupport(options.scope, options.harness, resolved.capabilities);
|
|
331
|
+
await assertCodexLoopConfiguration(options.root, options.harness, resolved.capabilities);
|
|
332
|
+
const existing = await readExistingManifest(options.root);
|
|
333
|
+
assertCompatibleManifest(existing?.manifest ?? null, options.scope, options.harness, options.allowHarnessChange === true);
|
|
334
|
+
const existingOpencodePluginPath = existing?.manifest.artifacts.find(({ id }) => id === "opencode-plugin")?.path;
|
|
335
|
+
const explicitHookTargets = new Map();
|
|
336
|
+
for (const target of options.hookTargets ?? []) {
|
|
337
|
+
if (explicitHookTargets.has(target.harness) ||
|
|
338
|
+
!options.harness.includes(target.harness)) {
|
|
339
|
+
throw new AgentOpsError("HOOK_TARGET_INVALID", `Hook target must name one selected harness exactly once: ${target.harness}.`);
|
|
340
|
+
}
|
|
341
|
+
if (harnessDescriptor(target.harness).control.buildHooks === undefined) {
|
|
342
|
+
throw new AgentOpsError("HOOK_TARGET_UNSUPPORTED", `Harness hook targets are not supported for ${target.harness}.`);
|
|
343
|
+
}
|
|
344
|
+
explicitHookTargets.set(target.harness, target.surfaceId);
|
|
345
|
+
}
|
|
346
|
+
if (options.hookRuntimePath === undefined &&
|
|
347
|
+
explicitHookTargets.size > 0) {
|
|
348
|
+
throw new AgentOpsError("HOOK_TARGET_REQUIRES_RUNTIME", "An explicit hook target requires a hook runtime path.");
|
|
349
|
+
}
|
|
350
|
+
const baseContribution = await planHarnessContributions(options.harness, {
|
|
351
|
+
root: options.root,
|
|
215
352
|
scope: options.scope,
|
|
216
353
|
profiles: resolved.profiles,
|
|
217
354
|
capabilities: resolved.capabilities,
|
|
355
|
+
...(options.hookRuntimePath === undefined
|
|
356
|
+
? {}
|
|
357
|
+
: { runtimePath: options.hookRuntimePath }),
|
|
218
358
|
...(options.toolkitVersion === undefined
|
|
219
359
|
? {}
|
|
220
|
-
: { toolkitVersion: options.toolkitVersion })
|
|
360
|
+
: { toolkitVersion: options.toolkitVersion }),
|
|
361
|
+
...(existingOpencodePluginPath === undefined
|
|
362
|
+
? {}
|
|
363
|
+
: { opencodePluginPath: existingOpencodePluginPath })
|
|
221
364
|
}, options.adapters);
|
|
365
|
+
const loopContribution = planLoopContribution({
|
|
366
|
+
scope: options.scope,
|
|
367
|
+
harnesses: options.harness,
|
|
368
|
+
capabilities: resolved.capabilities,
|
|
369
|
+
...(options.hookRuntimePath === undefined
|
|
370
|
+
? {}
|
|
371
|
+
: { hookRuntimePath: options.hookRuntimePath })
|
|
372
|
+
});
|
|
373
|
+
const contribution = {
|
|
374
|
+
artifacts: [
|
|
375
|
+
...baseContribution.artifacts,
|
|
376
|
+
...loopContribution.artifacts
|
|
377
|
+
],
|
|
378
|
+
blocks: [...baseContribution.blocks, ...loopContribution.blocks]
|
|
379
|
+
};
|
|
222
380
|
assertUniqueContributions(contribution.artifacts, contribution.blocks);
|
|
223
|
-
const
|
|
224
|
-
|
|
381
|
+
const reconcileExisting = existing !== null;
|
|
382
|
+
const expectedExistingMarkers = reconcileExisting
|
|
383
|
+
? assertSupportedManifestOwnership(existing.manifest, options.root)
|
|
384
|
+
: new Map();
|
|
385
|
+
const desiredArtifactPaths = new Set([
|
|
386
|
+
pathKey(CONFIG_PATH),
|
|
387
|
+
...contribution.artifacts.map(({ path }) => pathKey(path))
|
|
388
|
+
]);
|
|
389
|
+
const preservedArtifacts = options.hookRuntimePath === undefined &&
|
|
390
|
+
options.allowHarnessChange === true &&
|
|
391
|
+
options.harness.includes("opencode")
|
|
392
|
+
? (existing?.manifest.artifacts.filter(({ id }) => id === "opencode-plugin") ?? [])
|
|
393
|
+
: [];
|
|
394
|
+
for (const artifact of preservedArtifacts) {
|
|
395
|
+
const current = await readCurrentFile(options.root, artifact.path);
|
|
396
|
+
if (current === null || current.hash !== artifact.hash) {
|
|
397
|
+
throw new AgentOpsError("MANAGED_ARTIFACT_CHANGED", `Managed artifact changed after installation: ${artifact.path}`);
|
|
398
|
+
}
|
|
399
|
+
if (artifact.id === "opencode-plugin" &&
|
|
400
|
+
!isOpencodeManagedPlugin(current.content)) {
|
|
401
|
+
throw new AgentOpsError("MANIFEST_OWNERSHIP_INVALID", `The recorded opencode plugin is not an agent-ops managed plugin: ${artifact.path}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
for (const artifact of preservedArtifacts) {
|
|
405
|
+
desiredArtifactPaths.add(pathKey(artifact.path));
|
|
406
|
+
}
|
|
407
|
+
const artifactsToRemove = reconcileExisting
|
|
408
|
+
? existing.manifest.artifacts.filter(({ path }) => !desiredArtifactPaths.has(pathKey(path)))
|
|
409
|
+
: [];
|
|
410
|
+
const desiredMarkerKeys = new Set(contribution.blocks.map(({ path, id }) => markerKey(path, id)));
|
|
411
|
+
const markersToRemove = reconcileExisting
|
|
412
|
+
? existing.manifest.markers.filter(({ path, id }) => !desiredMarkerKeys.has(markerKey(path, id)))
|
|
413
|
+
: [];
|
|
225
414
|
const operations = [];
|
|
226
415
|
const artifacts = [];
|
|
227
416
|
const config = await planConfig(options.root, resolved.profiles, existing?.manifest ?? null, options.existingConfig);
|
|
@@ -232,15 +421,39 @@ export async function createInstallPlan(options) {
|
|
|
232
421
|
operations.push(planned.operation);
|
|
233
422
|
artifacts.push(planned.record);
|
|
234
423
|
}
|
|
235
|
-
|
|
424
|
+
if (resolved.capabilities.includes("project-loop")) {
|
|
425
|
+
const seedHarnesses = loopHarnessesNeedingSeeds(options.harness, existing?.manifest ?? null);
|
|
426
|
+
operations.push(...await planCreateOnceSeeds(options.root, loopSeeds(seedHarnesses)));
|
|
427
|
+
}
|
|
428
|
+
artifacts.push(...preservedArtifacts);
|
|
429
|
+
for (const artifact of artifactsToRemove) {
|
|
430
|
+
operations.push(await planArtifactRemoval(options.root, artifact));
|
|
431
|
+
}
|
|
432
|
+
const plannedBlocks = await planBlocks(options.root, contribution.blocks, markersToRemove, expectedExistingMarkers);
|
|
236
433
|
operations.push(...plannedBlocks.operations);
|
|
237
434
|
const hooks = [];
|
|
238
435
|
if (options.hookRuntimePath !== undefined) {
|
|
239
|
-
for (const harness of
|
|
240
|
-
|
|
436
|
+
for (const harness of options.harness) {
|
|
437
|
+
if (harnessDescriptor(harness).control.buildHooks === undefined) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const existingHook = existing?.manifest.hooks?.find(({ harness: recordHarness }) => recordHarness === harness);
|
|
441
|
+
const selectedSurface = selectHarnessHookSurface({
|
|
442
|
+
harness,
|
|
443
|
+
scope: options.scope,
|
|
444
|
+
root: options.root,
|
|
445
|
+
...(explicitHookTargets.has(harness)
|
|
446
|
+
? { surfaceId: explicitHookTargets.get(harness) }
|
|
447
|
+
: existingHook === undefined
|
|
448
|
+
? {}
|
|
449
|
+
: { persistedPath: existingHook.path })
|
|
450
|
+
});
|
|
451
|
+
const hookPath = selectedSurface.path;
|
|
452
|
+
const current = await readCurrentFile(options.root, hookPath);
|
|
241
453
|
const planned = planHookRegistration({
|
|
242
454
|
harness,
|
|
243
455
|
scope: options.scope,
|
|
456
|
+
path: hookPath,
|
|
244
457
|
capabilities: resolved.capabilities,
|
|
245
458
|
runtimePath: options.hookRuntimePath,
|
|
246
459
|
currentSource: current?.content ?? null
|
|
@@ -252,13 +465,45 @@ export async function createInstallPlan(options) {
|
|
|
252
465
|
kind: "write",
|
|
253
466
|
path: planned.record.path,
|
|
254
467
|
content: planned.content,
|
|
255
|
-
expectedHash: current?.hash ?? null
|
|
468
|
+
expectedHash: current?.hash ?? null,
|
|
469
|
+
disclosure: planned.disclosure
|
|
256
470
|
});
|
|
257
471
|
hooks.push(planned.record);
|
|
258
472
|
}
|
|
259
473
|
}
|
|
474
|
+
else if (existing !== null) {
|
|
475
|
+
const selectedHarnesses = new Set(options.harness);
|
|
476
|
+
hooks.push(...(existing.manifest.hooks ?? []).filter(({ harness }) => selectedHarnesses.has(harness)));
|
|
477
|
+
}
|
|
478
|
+
if (reconcileExisting) {
|
|
479
|
+
const desiredHookPaths = new Set(hooks.map(({ path }) => pathKey(path)));
|
|
480
|
+
for (const hook of existing.manifest.hooks ?? []) {
|
|
481
|
+
if (desiredHookPaths.has(pathKey(hook.path))) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const current = await readCurrentFile(options.root, hook.path);
|
|
485
|
+
if (current === null) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const removal = planHookRemoval(hook, current.content);
|
|
489
|
+
operations.push(removal.content === null
|
|
490
|
+
? {
|
|
491
|
+
kind: "remove",
|
|
492
|
+
path: hook.path,
|
|
493
|
+
expectedHash: current.hash,
|
|
494
|
+
disclosure: removal.disclosure
|
|
495
|
+
}
|
|
496
|
+
: {
|
|
497
|
+
kind: "write",
|
|
498
|
+
path: hook.path,
|
|
499
|
+
content: removal.content,
|
|
500
|
+
expectedHash: current.hash,
|
|
501
|
+
disclosure: removal.disclosure
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
260
505
|
const manifest = {
|
|
261
|
-
schemaVersion:
|
|
506
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
262
507
|
scope: options.scope,
|
|
263
508
|
harness: options.harness,
|
|
264
509
|
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
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { AgentOpsError } from "../fs/paths.js";
|
|
2
|
-
const PROFILE_ORDER = ["core", "advisory", "guardrails"];
|
|
2
|
+
const PROFILE_ORDER = ["core", "advisory", "guardrails", "loop"];
|
|
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
|
+
loop: ["project-loop"]
|
|
7
8
|
};
|
|
8
9
|
export function resolveProfiles(inputProfiles) {
|
|
9
10
|
if (inputProfiles.length === 0) {
|
|
10
11
|
throw new AgentOpsError("PROFILE_REQUIRED", "At least one installation profile is required.");
|
|
11
12
|
}
|
|
12
13
|
const selectedProfiles = new Set(inputProfiles);
|
|
13
|
-
if (selectedProfiles.has("guardrails")
|
|
14
|
+
if (selectedProfiles.has("guardrails") ||
|
|
15
|
+
selectedProfiles.has("loop")) {
|
|
14
16
|
selectedProfiles.add("core");
|
|
15
17
|
}
|
|
16
18
|
const profiles = PROFILE_ORDER.filter((profile) => selectedProfiles.has(profile));
|
|
@@ -26,3 +28,10 @@ export function resolveProfiles(inputProfiles) {
|
|
|
26
28
|
}
|
|
27
29
|
return { profiles: [...profiles], capabilities };
|
|
28
30
|
}
|
|
31
|
+
export function resolveCapabilities(config) {
|
|
32
|
+
const resolved = resolveProfiles(config.profiles);
|
|
33
|
+
if (config.features.stopVerification.enabled) {
|
|
34
|
+
resolved.capabilities.push("optional-stop-verify");
|
|
35
|
+
}
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|