@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
@@ -0,0 +1,23 @@
1
+ import { opencodePluginTarget } from "./config.js";
2
+ export const OPENCODE_SURFACE_IDS = {
3
+ plugin: "opencode-plugin",
4
+ config: "opencode-config"
5
+ };
6
+ export function opencodeSurfaces(scope, root) {
7
+ return [
8
+ {
9
+ id: OPENCODE_SURFACE_IDS.plugin,
10
+ path: opencodePluginTarget(scope, root).path,
11
+ scope,
12
+ access: "managed-default",
13
+ representation: "javascript"
14
+ },
15
+ {
16
+ id: OPENCODE_SURFACE_IDS.config,
17
+ path: "opencode.json",
18
+ scope,
19
+ access: "inspect-only",
20
+ representation: "json"
21
+ }
22
+ ];
23
+ }
@@ -1,6 +1,13 @@
1
1
  export function explainConfig(merged) {
2
2
  return {
3
3
  schemaVersion: merged.config.schemaVersion,
4
+ features: {
5
+ stopVerification: {
6
+ enabled: merged.config.features.stopVerification.enabled,
7
+ source: merged.provenance.features.source,
8
+ sourcePath: merged.provenance.features.sourcePath
9
+ }
10
+ },
4
11
  profiles: merged.provenance.profiles.map((entry) => ({
5
12
  id: entry.value,
6
13
  source: entry.source,
@@ -0,0 +1,24 @@
1
+ import { sha256 } from "../fs/hash.js";
2
+ import { AgentOpsError } from "../fs/paths.js";
3
+ function canonicalJson(value) {
4
+ if (value === null ||
5
+ typeof value === "boolean" ||
6
+ typeof value === "number" ||
7
+ typeof value === "string") {
8
+ return JSON.stringify(value);
9
+ }
10
+ if (Array.isArray(value)) {
11
+ return `[${value.map(canonicalJson).join(",")}]`;
12
+ }
13
+ if (typeof value === "object") {
14
+ const record = value;
15
+ return `{${Object.keys(record)
16
+ .sort()
17
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
18
+ .join(",")}}`;
19
+ }
20
+ throw new AgentOpsError("CONFIG_HASH_INVALID", "Configuration contains an unsupported value.");
21
+ }
22
+ export function calculateConfigHash(config) {
23
+ return sha256(canonicalJson(config));
24
+ }
@@ -108,8 +108,10 @@ export function mergeConfigLayers(inputLayers) {
108
108
  const mappings = new Map();
109
109
  const exceptions = new Map();
110
110
  let schemaVersion;
111
+ let features;
111
112
  for (const layer of layers) {
112
113
  schemaVersion = effective(layer.config.schemaVersion, layer);
114
+ features = effective(layer.config.features, layer);
113
115
  for (const profile of layer.config.profiles) {
114
116
  profiles.set(profile, effective(profile, layer));
115
117
  }
@@ -139,22 +141,24 @@ export function mergeConfigLayers(inputLayers) {
139
141
  exceptions.set(key, effective(securityException, layer));
140
142
  }
141
143
  }
142
- if (schemaVersion === undefined) {
144
+ if (schemaVersion === undefined || features === undefined) {
143
145
  throw new AgentOpsError("CONFIG_LAYER_REQUIRED", "At least one configuration layer is required.");
144
146
  }
145
147
  const provenance = {
146
148
  schemaVersion,
149
+ features,
147
150
  profiles: [...profiles.values()],
148
151
  verificationCommands: [...commands.values()],
149
152
  pathMappings: [...mappings.values()],
150
153
  securityExceptions: [...exceptions.values()]
151
154
  };
152
155
  const config = {
153
- schemaVersion: 1,
156
+ schemaVersion: schemaVersion.value,
154
157
  profiles: provenance.profiles.map(({ value }) => value),
155
158
  verification: {
156
159
  commands: provenance.verificationCommands.map(({ value }) => value)
157
160
  },
161
+ features: provenance.features.value,
158
162
  pathMappings: provenance.pathMappings.map(({ value }) => value),
159
163
  securityExceptions: provenance.securityExceptions.map(({ value }) => value)
160
164
  };
@@ -1,4 +1,4 @@
1
- import { SCHEMA_VERSION } from "../contracts.js";
1
+ import { CONFIG_SCHEMA_VERSION } from "../contracts.js";
2
2
  import { AgentOpsError } from "../fs/paths.js";
3
3
  import { validateConfig } from "../schema/validate.js";
4
4
  function isRecord(value) {
@@ -18,6 +18,21 @@ const MIGRATIONS = new Map([
18
18
  verification: { commands: clone(commands) }
19
19
  };
20
20
  }
21
+ ],
22
+ [
23
+ 1,
24
+ (input) => {
25
+ const { schemaVersion: _schemaVersion, ...rest } = input;
26
+ return {
27
+ ...rest,
28
+ schemaVersion: CONFIG_SCHEMA_VERSION,
29
+ features: {
30
+ stopVerification: {
31
+ enabled: false
32
+ }
33
+ }
34
+ };
35
+ }
21
36
  ]
22
37
  ]);
23
38
  function schemaVersionOf(value) {
@@ -30,13 +45,13 @@ function schemaVersionOf(value) {
30
45
  }
31
46
  export function previewConfigMigration(input) {
32
47
  const sourceVersion = schemaVersionOf(input);
33
- if (sourceVersion > SCHEMA_VERSION) {
34
- throw new AgentOpsError("CONFIG_SCHEMA_FUTURE", `Config schemaVersion ${sourceVersion} is newer than supported version ${SCHEMA_VERSION}.`);
48
+ if (sourceVersion > CONFIG_SCHEMA_VERSION) {
49
+ throw new AgentOpsError("CONFIG_SCHEMA_FUTURE", `Config schemaVersion ${sourceVersion} is newer than supported version ${CONFIG_SCHEMA_VERSION}.`);
35
50
  }
36
51
  let migrated = clone(input);
37
52
  let version = sourceVersion;
38
53
  const steps = [];
39
- while (version < SCHEMA_VERSION) {
54
+ while (version < CONFIG_SCHEMA_VERSION) {
40
55
  const migration = MIGRATIONS.get(version);
41
56
  if (migration === undefined || !isRecord(migrated)) {
42
57
  throw new AgentOpsError("CONFIG_MIGRATION_MISSING", `No migration is registered from config schemaVersion ${version}.`);
@@ -1 +1,10 @@
1
- export const SCHEMA_VERSION = 1;
1
+ export const CONFIG_SCHEMA_VERSION = 2;
2
+ export const TASK_SCHEMA_VERSION = 1;
3
+ export const EVIDENCE_SCHEMA_VERSION = 1;
4
+ /** @deprecated Use the document-specific schema version constants. */
5
+ export const SCHEMA_VERSION = CONFIG_SCHEMA_VERSION;
6
+ /**
7
+ * The manifest versions independently of config, tasks, and evidence: only its
8
+ * own shape changed when the harness selection became a list.
9
+ */
10
+ export const MANIFEST_SCHEMA_VERSION = 2;
@@ -1,6 +1,37 @@
1
+ import { MANIFEST_SCHEMA_VERSION } from "../contracts.js";
1
2
  import { validateManifest } from "../schema/validate.js";
2
3
  import { AgentOpsError } from "./paths.js";
3
4
  export const PROJECT_MANIFEST_PATH = ".agent-ops/manifest.json";
5
+ const LEGACY_HARNESS_SELECTIONS = {
6
+ both: ["codex", "claude"],
7
+ claude: ["claude"],
8
+ codex: ["codex"]
9
+ };
10
+ /**
11
+ * Manifests written up to 0.1.4 stored a single harness string, with `"both"`
12
+ * standing in for two harnesses. Reading one upgrades it in memory; the file is
13
+ * rewritten the next time `update` runs.
14
+ */
15
+ function migrateLegacyManifest(value) {
16
+ if (typeof value !== "object" ||
17
+ value === null ||
18
+ Array.isArray(value)) {
19
+ return value;
20
+ }
21
+ const record = value;
22
+ if (record.schemaVersion !== 1 || typeof record.harness !== "string") {
23
+ return value;
24
+ }
25
+ const harness = LEGACY_HARNESS_SELECTIONS[record.harness];
26
+ if (harness === undefined) {
27
+ return value;
28
+ }
29
+ return {
30
+ ...record,
31
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
32
+ harness
33
+ };
34
+ }
4
35
  export function parseInstallManifest(source) {
5
36
  let value;
6
37
  try {
@@ -9,7 +40,7 @@ export function parseInstallManifest(source) {
9
40
  catch (error) {
10
41
  throw new AgentOpsError("MANIFEST_INVALID_JSON", "The installation manifest is not valid JSON.", { cause: error });
11
42
  }
12
- const result = validateManifest(value);
43
+ const result = validateManifest(migrateLegacyManifest(value));
13
44
  if (!result.ok) {
14
45
  throw new AgentOpsError("MANIFEST_INVALID", `${result.errors[0]?.path ?? "$"}: ${result.errors[0]?.message ?? "Invalid installation manifest."}`);
15
46
  }
@@ -147,13 +147,25 @@ function assertTransactionPlan(plan) {
147
147
  }
148
148
  if (operation.kind === "write") {
149
149
  if (typeof operation.content !== "string" ||
150
- !hasOnlyKeys(operation, ["kind", "path", "content", "expectedHash"])) {
150
+ (operation.disclosure !== undefined &&
151
+ operation.disclosure !== "full" &&
152
+ operation.disclosure !== "opaque") ||
153
+ !hasOnlyKeys(operation, [
154
+ "kind",
155
+ "path",
156
+ "content",
157
+ "expectedHash",
158
+ "disclosure"
159
+ ])) {
151
160
  throw new AgentOpsError("INVALID_TRANSACTION_PLAN", `Invalid write operation at index ${index}.`);
152
161
  }
153
162
  continue;
154
163
  }
155
164
  if (operation.kind !== "remove" ||
156
- !hasOnlyKeys(operation, ["kind", "path", "expectedHash"])) {
165
+ (operation.disclosure !== undefined &&
166
+ operation.disclosure !== "full" &&
167
+ operation.disclosure !== "opaque") ||
168
+ !hasOnlyKeys(operation, ["kind", "path", "expectedHash", "disclosure"])) {
157
169
  throw new AgentOpsError("INVALID_TRANSACTION_PLAN", `Invalid remove operation at index ${index}.`);
158
170
  }
159
171
  }
@@ -0,0 +1,16 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { appendLocalLog } from "../logging/local-log.js";
4
+ import { localStatePaths } from "../security/permissions.js";
5
+ /**
6
+ * Records the bounded lifecycle summary signal locally. It deliberately does
7
+ * not include project paths, prompts, commands, or verification evidence.
8
+ */
9
+ export async function runLifecycleAdvisory(_event, options = {}) {
10
+ const home = options.homeDirectory ?? process.env.AGENT_OPS_HOME ?? homedir();
11
+ const state = localStatePaths(home);
12
+ await appendLocalLog(join(state.logDirectory, "events.ndjson"), { type: "diagnostic", code: "LIFECYCLE_SUMMARY" }, {
13
+ anchorDirectory: state.anchorDirectory,
14
+ ...(options.now === undefined ? {} : { now: options.now })
15
+ });
16
+ }
@@ -0,0 +1,70 @@
1
+ import { calculateConfigHash } from "../config/hash.js";
2
+ import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
3
+ import { aggregateVerificationStatus, executeConfiguredCommand } from "../verify/command-executor.js";
4
+ import { collectChangeSurface } from "../verify/change-surface.js";
5
+ import { selectVerificationScope } from "../verify/scope.js";
6
+ export const STOP_VERIFICATION_ENV = "AGENT_OPS_STOP_VERIFY_ACTIVE";
7
+ const HASH_PATTERN = /^[a-f0-9]{64}$/;
8
+ function stopError(code, message) {
9
+ return new AgentOpsError(code, message);
10
+ }
11
+ function commandById(config, commandId) {
12
+ const command = config.verification.commands.find((candidate) => candidate.id === commandId);
13
+ if (command === undefined) {
14
+ throw stopError("STOP_VERIFICATION_COMMAND_NOT_FOUND", `Stop verification command not found: ${commandId}`);
15
+ }
16
+ return command;
17
+ }
18
+ export class StopVerificationService {
19
+ #options;
20
+ constructor(options) {
21
+ this.#options = options;
22
+ }
23
+ #assertReady() {
24
+ if (!this.#options.config.features.stopVerification.enabled) {
25
+ throw stopError("STOP_VERIFICATION_DISABLED", "Stop verification is not enabled in configuration.");
26
+ }
27
+ if (this.#options.config.verification.commands.length === 0) {
28
+ throw stopError("STOP_VERIFICATION_COMMANDS_REQUIRED", "Stop verification requires at least one configured command.");
29
+ }
30
+ if (!this.#options.trusted) {
31
+ throw stopError("STOP_VERIFICATION_UNTRUSTED", "Stop verification requires a trusted repository.");
32
+ }
33
+ if (this.#options.recursionActive ||
34
+ process.env[STOP_VERIFICATION_ENV] === "1") {
35
+ throw stopError("STOP_VERIFICATION_RECURSION", "Stop verification recursion is active.");
36
+ }
37
+ if (!HASH_PATTERN.test(this.#options.configHash) ||
38
+ calculateConfigHash(this.#options.config) !== this.#options.configHash) {
39
+ throw stopError("STOP_VERIFICATION_UNCONFIRMED", "Stop verification configuration is stale or unconfirmed.");
40
+ }
41
+ }
42
+ async verify() {
43
+ this.#assertReady();
44
+ const surface = await collectChangeSurface(this.#options.gitRunner);
45
+ const selection = selectVerificationScope(surface.paths, this.#options.config);
46
+ const executions = [];
47
+ for (const commandId of selection.verifierIds) {
48
+ const command = commandById(this.#options.config, commandId);
49
+ const cwd = command.cwd === "."
50
+ ? this.#options.root
51
+ : await resolveContainedPath(this.#options.root, command.cwd);
52
+ const result = await executeConfiguredCommand(command, {
53
+ cwd,
54
+ runner: this.#options.processRunner,
55
+ trusted: true,
56
+ env: { [STOP_VERIFICATION_ENV]: "1" }
57
+ });
58
+ executions.push(result);
59
+ }
60
+ const results = executions.map(({ commandId, exitCode, testCount }) => ({
61
+ commandId,
62
+ exitCode,
63
+ testCount
64
+ }));
65
+ return {
66
+ status: aggregateVerificationStatus(executions),
67
+ results
68
+ };
69
+ }
70
+ }
@@ -16,11 +16,14 @@ function boundedEvidence(results) {
16
16
  (!Number.isSafeInteger(result.testCount) || result.testCount < 0)))) {
17
17
  return null;
18
18
  }
19
- return results.map(({ commandId, exitCode, testCount }) => ({
19
+ const bounded = results.map(({ commandId, exitCode, testCount }) => ({
20
20
  commandId,
21
21
  exitCode,
22
22
  testCount
23
23
  }));
24
+ return Buffer.byteLength(JSON.stringify(bounded), "utf8") <= 4096
25
+ ? bounded
26
+ : null;
24
27
  }
25
28
  function evidenceSupportsStatus(status, results) {
26
29
  if (status !== "PASS") {
@@ -5,11 +5,17 @@ import { parseInstallManifest, PROJECT_MANIFEST_PATH } from "../fs/manifest.js";
5
5
  import { resolveContainedPath } from "../fs/paths.js";
6
6
  import { validateConfig } from "../schema/validate.js";
7
7
  import { assertExpectedManagedBlock, assertSupportedManifestOwnership } from "./ownership.js";
8
+ import { isOpencodeManagedPlugin } from "../adapters/opencode/config.js";
9
+ import { harnessDescriptor } from "./harness.js";
10
+ import { resolveProfiles } from "./profiles.js";
11
+ import { inspectHarnessSurfaces, inspectHarnessRegistrations } from "./surface-inspection.js";
8
12
  const CONFIG_PATH = ".agent-ops/config.json";
9
13
  const MINIMUM_NODE_VERSION = [22, 14, 0];
10
14
  const MAX_DOCTOR_FILE_BYTES = 1024 * 1024;
11
- function check(id, status, message) {
12
- return { id, status, message };
15
+ function check(id, status, message, code) {
16
+ return code === undefined
17
+ ? { id, status, message }
18
+ : { id, status, message, code };
13
19
  }
14
20
  function parseNodeVersion(version) {
15
21
  const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
@@ -80,7 +86,7 @@ async function readContainedText(root, path) {
80
86
  async function checkManifest(root) {
81
87
  try {
82
88
  const manifest = parseInstallManifest(await readContainedText(root, PROJECT_MANIFEST_PATH));
83
- assertSupportedManifestOwnership(manifest);
89
+ assertSupportedManifestOwnership(manifest, root);
84
90
  return {
85
91
  check: check("manifest", "PASS", "Installation manifest is valid."),
86
92
  manifest
@@ -124,7 +130,9 @@ async function checkArtifacts(root, manifest) {
124
130
  for (const artifact of manifest.artifacts) {
125
131
  try {
126
132
  const content = await readContained(root, artifact.path);
127
- if (sha256(content) !== artifact.hash) {
133
+ if (sha256(content) !== artifact.hash ||
134
+ (artifact.id === "opencode-plugin" &&
135
+ !isOpencodeManagedPlugin(content.toString("utf8")))) {
128
136
  failures.push(artifact.path);
129
137
  }
130
138
  }
@@ -141,7 +149,8 @@ async function checkMarkers(root, manifest) {
141
149
  return check("markers", "FAIL", "Managed blocks cannot be verified without a valid manifest.");
142
150
  }
143
151
  const failures = [];
144
- const expectedMarkers = assertSupportedManifestOwnership(manifest);
152
+ const legacyPaths = [];
153
+ const expectedMarkers = assertSupportedManifestOwnership(manifest, root);
145
154
  for (const marker of manifest.markers) {
146
155
  try {
147
156
  const source = await readContainedText(root, marker.path);
@@ -150,15 +159,22 @@ async function checkMarkers(root, manifest) {
150
159
  failures.push(marker.path);
151
160
  continue;
152
161
  }
153
- assertExpectedManagedBlock(source, marker, expected);
162
+ const match = assertExpectedManagedBlock(source, marker, expected);
163
+ if (match === "legacy") {
164
+ legacyPaths.push(marker.path);
165
+ }
154
166
  }
155
167
  catch {
156
168
  failures.push(marker.path);
157
169
  }
158
170
  }
159
- return failures.length === 0
160
- ? check("markers", "PASS", "All managed block markers are intact.")
161
- : check("markers", "FAIL", `Managed block markers failed verification: ${failures.join(", ")}.`);
171
+ if (failures.length > 0) {
172
+ return check("markers", "FAIL", `Managed block markers failed verification: ${failures.join(", ")}.`);
173
+ }
174
+ if (legacyPaths.length > 0) {
175
+ return check("markers", "DEGRADED", `Legacy managed routing blocks need migration: ${legacyPaths.join(", ")}.`);
176
+ }
177
+ return check("markers", "PASS", "All managed block markers are intact.");
162
178
  }
163
179
  async function checkProbe(id, probe) {
164
180
  if (probe === undefined) {
@@ -177,21 +193,115 @@ async function checkProbe(id, probe) {
177
193
  return check(id, "FAIL", "Probe failed.");
178
194
  }
179
195
  }
196
+ function checkLifecycleSummary(manifest, config) {
197
+ if (manifest === undefined || config === undefined) {
198
+ return check("lifecycle-summary", "UNKNOWN", "Lifecycle summary cannot be assessed without a valid manifest and configuration.");
199
+ }
200
+ const hasLifecycleSummary = config.profiles.length > 0 &&
201
+ resolveProfiles(config.profiles).capabilities.includes("lifecycle-summary");
202
+ if (!hasLifecycleSummary) {
203
+ return check("lifecycle-summary", "PASS", "Lifecycle summary has no harness-specific degradation.");
204
+ }
205
+ const registrations = manifest.harness.map((id) => ({
206
+ id,
207
+ registration: harnessDescriptor(id).control.registrations.find((candidate) => candidate.capability === "lifecycle-summary")
208
+ }));
209
+ const missing = registrations
210
+ .filter(({ registration }) => registration === undefined)
211
+ .map(({ id }) => id);
212
+ if (missing.length > 0) {
213
+ return check("lifecycle-summary", "UNKNOWN", `Lifecycle summary registration is missing for ${missing.join(", ")}.`);
214
+ }
215
+ const unsupported = registrations
216
+ .filter(({ registration }) => registration?.support === "unsupported")
217
+ .map(({ id }) => id);
218
+ if (unsupported.length > 0) {
219
+ return check("lifecycle-summary", "UNSUPPORTED", `Lifecycle summary is not dispatched for ${unsupported.join(", ")}; advisory runtime wiring is unavailable.`);
220
+ }
221
+ const degraded = registrations
222
+ .filter(({ registration }) => registration?.support === "degraded")
223
+ .map(({ id }) => id);
224
+ if (degraded.length > 0) {
225
+ return check("lifecycle-summary", "DEGRADED", `Lifecycle summary is degraded for ${degraded.join(", ")}.`);
226
+ }
227
+ const unknown = registrations
228
+ .filter(({ registration }) => registration?.support === "unknown")
229
+ .map(({ id }) => id);
230
+ if (unknown.length > 0) {
231
+ return check("lifecycle-summary", "UNKNOWN", `Lifecycle summary support is unknown for ${unknown.join(", ")}.`);
232
+ }
233
+ return check("lifecycle-summary", "PASS", "Lifecycle summary is reachable for every selected harness.");
234
+ }
235
+ async function checkSurfaceInventory(root, manifest, config) {
236
+ if (manifest === undefined || config === undefined) {
237
+ return {
238
+ check: check("surface-inventory", "UNKNOWN", "Harness surfaces cannot be inventoried without a valid manifest and configuration."),
239
+ surfaces: []
240
+ };
241
+ }
242
+ try {
243
+ const surfaces = await inspectHarnessSurfaces({
244
+ root,
245
+ scope: manifest.scope,
246
+ harness: manifest.harness,
247
+ profiles: config.profiles
248
+ });
249
+ const unknown = surfaces.filter(({ status }) => status === "unknown");
250
+ return {
251
+ check: check("surface-inventory", unknown.length > 0 ? "UNKNOWN" : "PASS", unknown.length > 0
252
+ ? `${unknown.length} harness surface(s) could not be inspected.`
253
+ : "Harness surfaces were inventoried without exposing settings values."),
254
+ surfaces
255
+ };
256
+ }
257
+ catch {
258
+ return {
259
+ check: check("surface-inventory", "UNKNOWN", "Harness surfaces could not be inspected safely."),
260
+ surfaces: []
261
+ };
262
+ }
263
+ }
264
+ async function checkRegistrationDrift(root, manifest, config) {
265
+ if (manifest === undefined || config === undefined) {
266
+ return check("registration-drift", "UNKNOWN", "Hook registration drift cannot be assessed without a valid manifest and configuration.");
267
+ }
268
+ try {
269
+ const statuses = await inspectHarnessRegistrations({
270
+ root,
271
+ manifest,
272
+ config
273
+ });
274
+ const drifted = statuses
275
+ .filter(({ registered }) => !registered)
276
+ .map(({ harness }) => harness);
277
+ return drifted.length === 0
278
+ ? check("registration-drift", "PASS", "Managed hook registrations match the desired capabilities.")
279
+ : check("registration-drift", "FAIL", `Hook registration drift detected for ${drifted.join(", ")}; run agent-ops update.`, "UPDATE_REQUIRED");
280
+ }
281
+ catch {
282
+ return check("registration-drift", "UNKNOWN", "Hook registration drift could not be assessed safely.");
283
+ }
284
+ }
180
285
  export async function doctorInstallation(options) {
181
286
  const manifest = await checkManifest(options.root);
182
287
  const config = await checkConfig(options.root);
288
+ const surfaceInventory = await checkSurfaceInventory(options.root, manifest.manifest, config.config);
183
289
  const checks = [
184
290
  checkNodeVersion(options.nodeVersion ?? process.versions.node),
185
291
  manifest.check,
186
292
  config.check,
187
293
  await checkArtifacts(options.root, manifest.manifest),
188
294
  await checkMarkers(options.root, manifest.manifest),
295
+ surfaceInventory.check,
296
+ await checkRegistrationDrift(options.root, manifest.manifest, config.config),
189
297
  await checkProbe("hook-registration", options.probes?.hookRegistration),
298
+ checkLifecycleSummary(manifest.manifest, config.config),
190
299
  await checkProbe("repository-trust", options.probes?.repositoryTrust),
191
300
  await checkProbe("smoke-availability", options.probes?.smokeAvailability)
192
301
  ];
193
302
  return {
194
303
  checks,
304
+ surfaces: surfaceInventory.surfaces,
195
305
  ...(manifest.manifest === undefined
196
306
  ? {}
197
307
  : { manifest: manifest.manifest }),