@kylecheng3146/agent-ops 0.1.18 → 0.1.20

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 (39) hide show
  1. package/README.md +16 -9
  2. package/dist/packages/cli/src/bin.js +59 -9
  3. package/dist/packages/cli/src/cli.js +1 -1
  4. package/dist/packages/cli/src/commands/init.js +39 -9
  5. package/dist/packages/cli/src/commands/review.js +3 -0
  6. package/dist/packages/cli/src/commands/uninstall.js +20 -3
  7. package/dist/packages/cli/src/ui.js +3 -1
  8. package/dist/packages/cli/src/wizard.js +12 -6
  9. package/dist/runtime/src/adapters/agy/config.js +105 -0
  10. package/dist/runtime/src/adapters/agy/events.js +27 -0
  11. package/dist/runtime/src/adapters/agy/input.js +30 -0
  12. package/dist/runtime/src/adapters/agy/output.js +24 -0
  13. package/dist/runtime/src/adapters/agy/surfaces.js +9 -0
  14. package/dist/runtime/src/fs/managed-block.js +23 -10
  15. package/dist/runtime/src/install/codex-loop.js +6 -2
  16. package/dist/runtime/src/install/doctor.js +15 -0
  17. package/dist/runtime/src/install/harness.js +52 -1
  18. package/dist/runtime/src/install/hooks.js +6 -1
  19. package/dist/runtime/src/install/ownership.js +12 -8
  20. package/dist/runtime/src/install/plan.js +2 -2
  21. package/dist/runtime/src/install/probes.js +61 -0
  22. package/dist/runtime/src/install/surface-inspection.js +10 -1
  23. package/dist/runtime/src/install/uninstall.js +337 -6
  24. package/dist/runtime/src/review/execute.js +23 -20
  25. package/dist/runtime/src/review/extract.js +5 -11
  26. package/dist/runtime/src/review/render.js +6 -1
  27. package/dist/runtime/src/review/roles.js +4 -0
  28. package/dist/runtime/src/review/runner.js +7 -0
  29. package/dist/runtime/src/schema/validate.js +2 -2
  30. package/docs/en/guides/configuration.md +22 -6
  31. package/docs/en/spec/README.md +5 -2
  32. package/docs/en/spec/harness-adapters.md +20 -9
  33. package/docs/en/spec/review.md +6 -0
  34. package/docs/zh-TW/guides/configuration.md +20 -6
  35. package/docs/zh-TW/spec/README.md +4 -2
  36. package/docs/zh-TW/spec/harness-adapters.md +18 -10
  37. package/docs/zh-TW/spec/review.md +5 -0
  38. package/package.json +2 -2
  39. package/schemas/manifest.schema.json +2 -2
@@ -2,6 +2,13 @@ import { AgentOpsError } from "./paths.js";
2
2
  function escapeRegExp(value) {
3
3
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4
4
  }
5
+ export function normalizeToLF(source) {
6
+ return source.replace(/\r\n/g, "\n");
7
+ }
8
+ /** Newline sequence to use for newly rendered content, inferred from the file. */
9
+ function detectNewline(source) {
10
+ return source.includes("\r\n") ? "\r\n" : "\n";
11
+ }
5
12
  function assertBlockId(id) {
6
13
  if (!/^[a-z][a-z0-9-]{0,127}$/.test(id)) {
7
14
  throw new AgentOpsError("INVALID_BLOCK_ID", `Invalid block ID: ${id}`);
@@ -70,25 +77,30 @@ function locateMarkers(source, id, version, markerStyle = "html") {
70
77
  }
71
78
  return { start, end, startIndex, endIndex };
72
79
  }
73
- function renderBlock(options) {
80
+ function renderBlock(options, nl) {
74
81
  if (/(?:<!--|#)\s*agent-ops:/.test(options.content)) {
75
82
  throw new AgentOpsError("AMBIGUOUS_MANAGED_CONTENT", "Managed content must not contain agent-ops marker boundaries.");
76
83
  }
77
84
  const { start, end } = managedBlockMarkers(options.id, options.version, options.markerStyle);
78
- const content = options.content.replace(/\r\n/g, "\n").replace(/\n+$/g, "");
79
- return `${start}\n${content}\n${end}`;
85
+ const content = normalizeToLF(options.content)
86
+ .replace(/\n+$/g, "")
87
+ .split("\n")
88
+ .join(nl);
89
+ return `${start}${nl}${content}${nl}${end}`;
80
90
  }
81
91
  export function applyManagedBlock(source, options) {
82
- const block = renderBlock(options);
92
+ const nl = detectNewline(source);
93
+ const block = renderBlock(options, nl);
83
94
  const located = locateMarkers(source, options.id, options.version, options.markerStyle);
84
95
  if (located === null) {
85
96
  if (source.length === 0) {
86
- return `${block}\n`;
97
+ return `${block}${nl}`;
87
98
  }
88
- return `${source.replace(/\n*$/, "\n\n")}${block}\n`;
99
+ return `${source.replace(/(?:\r\n|\n)*$/, `${nl}${nl}`)}${block}${nl}`;
89
100
  }
90
101
  return `${source.slice(0, located.startIndex)}${block}${source.slice(located.endIndex + located.end.length)}`;
91
102
  }
103
+ const NEWLINE_STYLES = ["\r\n", "\n"];
92
104
  export function removeManagedBlock(source, id, markerStyle = "html") {
93
105
  const located = locateMarkers(source, id, undefined, markerStyle);
94
106
  if (located === null) {
@@ -96,12 +108,13 @@ export function removeManagedBlock(source, id, markerStyle = "html") {
96
108
  }
97
109
  let before = source.slice(0, located.startIndex);
98
110
  let after = source.slice(located.endIndex + located.end.length);
99
- if (before.length === 0 && after === "\n") {
111
+ if (before.length === 0 && NEWLINE_STYLES.some((nl) => after === nl)) {
100
112
  return "";
101
113
  }
102
- if (before.endsWith("\n\n") && after.startsWith("\n")) {
103
- before = before.slice(0, -1);
104
- after = after.slice(1);
114
+ const collapseNl = NEWLINE_STYLES.find((nl) => before.endsWith(`${nl}${nl}`) && after.startsWith(nl));
115
+ if (collapseNl !== undefined) {
116
+ before = before.slice(0, -collapseNl.length);
117
+ after = after.slice(collapseNl.length);
105
118
  }
106
119
  return `${before}${after}`;
107
120
  }
@@ -134,12 +134,16 @@ export function planLoopContribution(options) {
134
134
  return { artifacts: [], blocks: [] };
135
135
  }
136
136
  const harnesses = selectedLoopHarnesses(options.harnesses);
137
- if (options.scope !== "project" || harnesses.length === 0) {
138
- throw new AgentOpsError("LOOP_PROFILE_UNSUPPORTED", "The loop profile requires project scope and the Codex or Claude harness.");
137
+ if (options.scope !== "project" ||
138
+ (harnesses.length === 0 && !options.harnesses.includes("agy"))) {
139
+ throw new AgentOpsError("LOOP_PROFILE_UNSUPPORTED", "The loop profile requires project scope and the agy, Codex, or Claude harness.");
139
140
  }
140
141
  if (options.hookRuntimePath === undefined) {
141
142
  throw new AgentOpsError("LOOP_RUNTIME_REQUIRED", "The loop profile requires the installed hook runtime path.");
142
143
  }
144
+ if (harnesses.length === 0) {
145
+ return { artifacts: [], blocks: [] };
146
+ }
143
147
  const artifacts = harnesses.flatMap((harness) => [
144
148
  {
145
149
  id: loopLauncherArtifactId(harness),
@@ -309,6 +309,14 @@ function checkLifecycleSummary(manifest, config) {
309
309
  }
310
310
  return check("lifecycle-summary", "PASS", "Lifecycle summary is reachable for every selected harness.");
311
311
  }
312
+ function checkProjectLoop(manifest, config) {
313
+ if (manifest === undefined || config === undefined || !config.profiles.includes("loop")) {
314
+ return undefined;
315
+ }
316
+ return manifest.harness.includes("agy")
317
+ ? check("project-loop", "DEGRADED", "agy loop uses only PreInvocation and PreToolUse(run_command); prompt, permission, compact, and subagent events are unavailable.")
318
+ : check("project-loop", "PASS", "Project loop events are fully registered.");
319
+ }
312
320
  async function checkSurfaceInventory(root, manifest, config) {
313
321
  if (manifest === undefined || config === undefined) {
314
322
  return {
@@ -420,6 +428,13 @@ export async function doctorInstallation(options) {
420
428
  await checkRegistrationDrift(options.root, manifest.manifest, config.config),
421
429
  await checkProbe("hook-registration", options.probes?.hookRegistration),
422
430
  checkLifecycleSummary(manifest.manifest, config.config),
431
+ ...(() => {
432
+ const projectLoop = checkProjectLoop(manifest.manifest, config.config);
433
+ return projectLoop === undefined ? [] : [projectLoop];
434
+ })(),
435
+ ...(manifest.manifest?.harness.includes("agy") === true
436
+ ? [await checkProbe("agy-runtime", options.probes?.agyRuntime)]
437
+ : []),
423
438
  await checkProbe("repository-trust", options.probes?.repositoryTrust),
424
439
  await checkProbe("smoke-availability", options.probes?.smokeAvailability),
425
440
  await checkReviewTargets(config.config, options.probes?.reviewTarget, options.checkReviewTargetAuth === true)
@@ -1,3 +1,8 @@
1
+ import { buildAgyHookSettings, isAgyHookRegistered, isAgyManagedHook, mergeAgyHooks, stripAgyHooks } from "../adapters/agy/config.js";
2
+ import { AGY_CAPABILITY_REGISTRATIONS } from "../adapters/agy/events.js";
3
+ import { normalizeAgyHookInput } from "../adapters/agy/input.js";
4
+ import { agyHookOutput } from "../adapters/agy/output.js";
5
+ import { agySurfaces } from "../adapters/agy/surfaces.js";
1
6
  import { buildClaudeHookSettings, isClaudeManagedHandler, mergeClaudeSettings, stripClaudeManagedHooks } from "../adapters/claude/config.js";
2
7
  import { CLAUDE_CAPABILITY_REGISTRATIONS } from "../adapters/claude/events.js";
3
8
  import { normalizeClaudeHookInput } from "../adapters/claude/input.js";
@@ -16,6 +21,7 @@ import { opencodeSurfaces } from "../adapters/opencode/surfaces.js";
16
21
  import { AgentOpsError } from "../fs/paths.js";
17
22
  import { findSurfaceById, findSurfaceByPath, isWritableSurface } from "./surfaces.js";
18
23
  export const HARNESS_IDS = [
24
+ "agy",
19
25
  "codex",
20
26
  "claude",
21
27
  "opencode"
@@ -108,7 +114,52 @@ const CLAUDE_ROUTING = {
108
114
  "## Loop Engineering\n\nUse `.agent-ops/CLAUDE.md` as the canonical Loop Engineering specification for this project.\n"
109
115
  ]
110
116
  };
117
+ const AGY_ROUTING = {
118
+ desired: "## Loop Engineering\n\nLoad `.agent-ops/GEMINI.md` as the agent-ops managed baseline.\nProject-specific instructions in this file remain authoritative.\n",
119
+ legacy: []
120
+ };
111
121
  const DESCRIPTORS = {
122
+ agy: {
123
+ id: "agy",
124
+ control: {
125
+ instructionFile: "GEMINI.md",
126
+ routing: AGY_ROUTING,
127
+ hookPath: ".agents/hooks.json",
128
+ hookPathForScope: (scope) => scope === "project" ? ".agents/hooks.json" : ".gemini/config/hooks.json",
129
+ surfaces: agySurfaces,
130
+ ownSettingsKeys: [],
131
+ buildHooks: buildAgyHookSettings,
132
+ mergeHooks: mergeAgyHooks,
133
+ stripHooks: stripAgyHooks,
134
+ isManagedHandler: isAgyManagedHook,
135
+ registrations: AGY_CAPABILITY_REGISTRATIONS,
136
+ hookRegistered: (source, capabilities) => isAgyHookRegistered(parseJsonSource(source), capabilities),
137
+ plan: async (context) => {
138
+ if (context.scope === "project") {
139
+ return await planCommonHarnessContribution("codex", context);
140
+ }
141
+ const descriptor = DESCRIPTORS.agy;
142
+ return {
143
+ artifacts: [{
144
+ id: "gemini-rules",
145
+ path: ".agent-ops/GEMINI.md",
146
+ content: managedRules(descriptor, context)
147
+ }],
148
+ blocks: [{
149
+ id: "agy-routing",
150
+ path: ".gemini/GEMINI.md",
151
+ version: 1,
152
+ content: AGY_ROUTING.desired
153
+ }]
154
+ };
155
+ }
156
+ },
157
+ runtime: {
158
+ normalizeInput: normalizeAgyHookInput,
159
+ formatOutput: agyHookOutput,
160
+ formatRuntimeFailure: (event, capability, remedy) => agyHookOutput(event, runtimeFailureResult(capability, AGY_CAPABILITY_REGISTRATIONS, remedy))
161
+ }
162
+ },
112
163
  codex: createJsonDescriptor({
113
164
  id: "codex",
114
165
  instructionFile: "AGENTS.md",
@@ -244,7 +295,7 @@ export function managedRules(descriptor, context) {
244
295
  ""
245
296
  ];
246
297
  if (context.capabilities.includes("rules")) {
247
- lines.push("For every change:", "", "1. Define two to five mechanically verifiable acceptance criteria.", "2. Inspect the smallest relevant scope and preserve unrelated changes.", "3. Apply the smallest safe change.", "4. Run evidence-producing verification for every criterion.", "5. Obtain independent review before claiming completion, via", " `agent-ops review --yes` (or the CLI's equivalent invocation). Never call a", " review-target CLI (agy, codex, claude) directly — direct calls skip", " the enforced read-only sandbox flags and can hang or fail on command", " permission prompts.", "", "Treat `.agent-ops/config.json` as verifier authority. Discovery output is", "only a proposal until a user confirms it. Repository commands require an", "exact matching trust record. Confirmed project init/update grants it", "automatically when verification commands are configured.", "");
298
+ lines.push("For every change:", "", "1. Define two to five mechanically verifiable acceptance criteria.", "2. Inspect the smallest relevant scope and preserve unrelated changes.", "3. Apply the smallest safe change.", "4. Run evidence-producing verification for every criterion.", "5. Obtain independent review before claiming completion, via", " `agent-ops review --yes` (or the CLI's equivalent invocation). Never call a", " review-target CLI (agy, codex, claude) directly — direct calls skip", " the enforced read-only sandbox flags and can hang or fail on command", " permission prompts.", " Set `AGENT_OPS_HOST` to the current CLI id when invoking review so", " agent-ops tries a different CLI first and uses isolated self-review", " only when no other configured reviewer is usable.", "", "Treat `.agent-ops/config.json` as verifier authority. Discovery output is", "only a proposal until a user confirms it. Repository commands require an", "exact matching trust record. Confirmed project init/update grants it", "automatically when verification commands are configured.", "");
248
299
  }
249
300
  if (context.capabilities.includes("task")) {
250
301
  lines.push("Split a change that exceeds five acceptance criteria into subtasks:", "`agent-ops task create --parent <task-id>` records one, and", "`agent-ops task status --parent <task-id>` lists them. Each subtask", "carries its own criteria, verification, and independent review;", "completing one never completes its parent.", "");
@@ -35,7 +35,9 @@ export function planHookRegistration(options) {
35
35
  }
36
36
  const path = options.path ?? hookRegistrationPath(options.harness, options.scope);
37
37
  const managed = descriptor.control.buildHooks(options.capabilities, options.runtimePath, options.platform);
38
- const events = Object.keys(managed.hooks);
38
+ const events = Object.keys(managed.hooks).map((event) => options.harness === "agy" && event === "PreInvocation"
39
+ ? "SessionStart"
40
+ : event);
39
41
  if (events.length === 0) {
40
42
  return null;
41
43
  }
@@ -54,6 +56,9 @@ export function planHookRegistration(options) {
54
56
  };
55
57
  }
56
58
  function onlyManagedRemains(harness, value) {
59
+ if (harness === "agy") {
60
+ return Object.keys(value).length === 0;
61
+ }
57
62
  const ownKeys = new Set(harnessDescriptor(harness).control.ownSettingsKeys ?? []);
58
63
  const hooks = value.hooks;
59
64
  return (Object.keys(value).every((key) => ownKeys.has(key)) &&
@@ -1,16 +1,18 @@
1
- import { applyManagedBlock, managedBlockMarkers } from "../fs/managed-block.js";
1
+ import { applyManagedBlock, managedBlockMarkers, normalizeToLF } from "../fs/managed-block.js";
2
2
  import { AgentOpsError } from "../fs/paths.js";
3
3
  import { harnessDescriptor, harnessHookPath, routingBlockId, selectHarnessHookSurface, rulesArtifactId } from "./harness.js";
4
4
  import { isOpencodePluginPath } from "../adapters/opencode/config.js";
5
5
  import { LOOP_MARKER_ID, LOOP_MARKER_VERSION, loopIgnoreContent, loopLauncherArtifactId, loopWindowsLauncherArtifactId, loopWindowsLauncherPath, loopLauncherPath, selectedLoopHarnesses } from "./codex-loop.js";
6
6
  function expectedMarker(manifest, id, markerId) {
7
- const descriptor = harnessDescriptor(id);
7
+ const descriptor = harnessDescriptor(id === "agy" && manifest.scope === "project" ? "codex" : id);
8
8
  const markers = managedBlockMarkers(markerId, 1, "html");
9
9
  return {
10
10
  id: markerId,
11
11
  path: manifest.scope === "project"
12
12
  ? descriptor.control.instructionFile
13
- : `.${id}/${descriptor.control.instructionFile}`,
13
+ : id === "agy"
14
+ ? ".gemini/GEMINI.md"
15
+ : `.${id}/${descriptor.control.instructionFile}`,
14
16
  startMarker: markers.start,
15
17
  endMarker: markers.end,
16
18
  markerStyle: "html",
@@ -96,7 +98,7 @@ export function assertSupportedManifestOwnership(manifest, root) {
96
98
  throw manifestOwnershipError();
97
99
  }
98
100
  for (const id of harnesses) {
99
- const descriptor = harnessDescriptor(id);
101
+ const descriptor = harnessDescriptor(id === "agy" && manifest.scope === "project" ? "codex" : id);
100
102
  const artifactPath = `.agent-ops/${descriptor.control.instructionFile}`;
101
103
  const artifactKey = pathKey(artifactPath);
102
104
  const artifactEntry = expectedArtifactPaths.get(artifactKey);
@@ -112,7 +114,9 @@ export function assertSupportedManifestOwnership(manifest, root) {
112
114
  requiredArtifactPaths.add(artifactKey);
113
115
  const markerPath = manifest.scope === "project"
114
116
  ? descriptor.control.instructionFile
115
- : `.${id}/${descriptor.control.instructionFile}`;
117
+ : id === "agy"
118
+ ? ".gemini/GEMINI.md"
119
+ : `.${id}/${descriptor.control.instructionFile}`;
116
120
  const markerKey = pathKey(markerPath);
117
121
  expectedMarkerPaths.add(markerKey);
118
122
  const currentId = routingBlockId(id, manifest.scope, descriptor);
@@ -211,18 +215,18 @@ export function assertExpectedManagedBlock(source, marker, expected) {
211
215
  startIndex >= endIndex) {
212
216
  throw new AgentOpsError("MANAGED_BLOCK_CHANGED", `Managed block boundaries changed after installation: ${marker.path}`);
213
217
  }
214
- const currentBlock = source.slice(startIndex, endIndex + marker.endMarker.length);
218
+ const currentBlock = normalizeToLF(source.slice(startIndex, endIndex + marker.endMarker.length));
215
219
  const candidates = [
216
220
  ["desired", expected.content],
217
221
  ...expected.legacyContent.map((content) => ["legacy", content])
218
222
  ];
219
223
  for (const [kind, content] of candidates) {
220
- const expectedBlock = applyManagedBlock("", {
224
+ const expectedBlock = normalizeToLF(applyManagedBlock("", {
221
225
  id: expected.id,
222
226
  version: 1,
223
227
  content,
224
228
  markerStyle: expected.markerStyle
225
- }).replace(/\n$/u, "");
229
+ }).replace(/\n$/u, ""));
226
230
  if (currentBlock === expectedBlock) {
227
231
  return kind;
228
232
  }
@@ -280,8 +280,8 @@ function assertLoopProfileSupport(scope, harness, capabilities) {
280
280
  return;
281
281
  }
282
282
  if (scope !== "project" ||
283
- !harness.some((id) => id === "codex" || id === "claude")) {
284
- throw new AgentOpsError("LOOP_PROFILE_UNSUPPORTED", "The loop profile requires project scope and the Codex or Claude harness.");
283
+ !harness.some((id) => id === "agy" || id === "codex" || id === "claude")) {
284
+ throw new AgentOpsError("LOOP_PROFILE_UNSUPPORTED", "The loop profile requires project scope and the agy, Codex, or Claude harness.");
285
285
  }
286
286
  }
287
287
  async function assertCodexLoopConfiguration(root, harness, capabilities) {
@@ -1,5 +1,66 @@
1
1
  import { harnessDescriptor } from "./harness.js";
2
2
  import { resolveCapabilities } from "./profiles.js";
3
+ const MINIMUM_AGY_VERSION = [1, 1, 12];
4
+ export function agyVersionSupported(versionOutput) {
5
+ const match = /\b(\d+)\.(\d+)\.(\d+)\b/u.exec(versionOutput);
6
+ const version = match?.slice(1).map(Number);
7
+ return version !== undefined && !version.some((part, index) => part < MINIMUM_AGY_VERSION[index] &&
8
+ version.slice(0, index).every((prior, priorIndex) => prior === MINIMUM_AGY_VERSION[priorIndex]));
9
+ }
10
+ export function agyRuntimeStatus(versionOutput, hooksOutput, expectedEvents = []) {
11
+ const match = /\b(\d+)\.(\d+)\.(\d+)\b/u.exec(versionOutput);
12
+ if (!agyVersionSupported(versionOutput)) {
13
+ return {
14
+ status: "FAIL",
15
+ message: "agy 1.1.12 or newer is required.",
16
+ remediation: "Update agy, then run `agent-ops doctor` again."
17
+ };
18
+ }
19
+ try {
20
+ const parsed = JSON.parse(hooksOutput);
21
+ const hooks = parsed.command?.data?.hooks;
22
+ const loaded = Array.isArray(hooks) && hooks.some((hook) => {
23
+ if (typeof hook !== "object" || hook === null || Array.isArray(hook))
24
+ return false;
25
+ const value = hook;
26
+ const actions = value.actions;
27
+ if (!(value.name === "agent-ops" &&
28
+ value.enabled === true &&
29
+ Array.isArray(actions) &&
30
+ actions.length > 0 &&
31
+ actions.every((action) => typeof action === "object" && action !== null && !Array.isArray(action))))
32
+ return false;
33
+ return expectedEvents.every((expected) => actions.some((action) => {
34
+ const nativeEvent = expected === "SessionStart" ? "PreInvocation" : expected;
35
+ return (typeof action === "object" && action !== null && !Array.isArray(action) &&
36
+ action.event === nativeEvent &&
37
+ typeof action.command === "string" &&
38
+ action.command.endsWith(` agy ${expected} --managed-by=agent-ops`));
39
+ }));
40
+ });
41
+ if (!Array.isArray(hooks) || (expectedEvents.length > 0 && !loaded)) {
42
+ return {
43
+ status: "FAIL",
44
+ message: "agy is installed, but its loaded hook list does not include agent-ops.",
45
+ code: "UPDATE_REQUIRED",
46
+ remediation: "Run `agent-ops update`, restart agy, then run doctor again."
47
+ };
48
+ }
49
+ return {
50
+ status: "PASS",
51
+ message: expectedEvents.length > 0
52
+ ? `agy ${match?.[0]} loaded the agent-ops hook.`
53
+ : `agy ${match?.[0]} meets the minimum supported version.`
54
+ };
55
+ }
56
+ catch {
57
+ return {
58
+ status: "FAIL",
59
+ message: "agy returned an unreadable /hooks response.",
60
+ remediation: "Run `agy -p \"/hooks\" --output-format json` and inspect the result."
61
+ };
62
+ }
63
+ }
3
64
  /**
4
65
  * Returns the harness ids missing an agent-ops owned handler for the hook
5
66
  * events implied by the installed profiles. Empty when installations without
@@ -90,6 +90,9 @@ function managedJsonCount(source, isManagedHandler) {
90
90
  }
91
91
  return jsonHandlerCounts(source, isManagedHandler)?.managed ?? 0;
92
92
  }
93
+ function desiredHookEvents(harness, events) {
94
+ return events.map((event) => harness === "agy" && event === "PreInvocation" ? "SessionStart" : event);
95
+ }
93
96
  export async function inspectHarnessRegistrations(options) {
94
97
  const capabilities = desiredCapabilities(options.config);
95
98
  const statuses = [];
@@ -100,7 +103,7 @@ export async function inspectHarnessRegistrations(options) {
100
103
  const recordedEvents = hookRecord?.events ?? [];
101
104
  const desiredEvents = control.buildHooks === undefined
102
105
  ? []
103
- : Object.keys(control.buildHooks(capabilities, "probe").hooks);
106
+ : desiredHookEvents(harness, Object.keys(control.buildHooks(capabilities, "probe").hooks));
104
107
  if (control.buildHooks !== undefined) {
105
108
  const surfaces = harnessSurfaces(harness, options.manifest.scope, options.root);
106
109
  const writableJsonSurfaces = surfaces.filter((surface) => isWritableSurface(surface) && surface.representation === "json");
@@ -186,6 +189,12 @@ function jsonHandlerCounts(source, isManagedHandler) {
186
189
  if (!isRecord(parsed)) {
187
190
  return null;
188
191
  }
192
+ if (isManagedHandler?.(parsed) === true) {
193
+ return {
194
+ managed: 1,
195
+ foreign: Math.max(0, Object.keys(parsed).length - 1)
196
+ };
197
+ }
189
198
  const hooks = parsed.hooks;
190
199
  if (hooks === undefined) {
191
200
  return { managed: 0, foreign: 0 };