@kylecheng3146/agent-ops 0.1.8 → 0.1.10

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 CHANGED
@@ -165,9 +165,21 @@ directory. `update` also requires that the project already has a valid managed
165
165
  `.agent-ops/manifest.json` created by `init`.
166
166
 
167
167
  The commands after `init --yes` are post-apply operations. `doctor` reports
168
- `UNKNOWN` for a probe that has nothing to verify yet: `repository-trust` until
169
- `trust grant` runs, and `smoke-availability` until the configuration declares a
170
- verification command.
168
+ `UNKNOWN` for a probe that has nothing to verify yet: `repository-trust`
169
+ until `trust grant` runs (only actionable once `verification.commands` is
170
+ configured — a project that never runs verification has nothing for trust
171
+ to unlock), and `smoke-availability` until the configuration declares a
172
+ verification command. Every non-`PASS` check carries a `remediation` string
173
+ explaining what, if anything, to do about it; text output prints it as an
174
+ indented ` → ` line, and `--json` exposes it as a field. `doctor` never
175
+ writes: it only reports what `agent-ops update` or `agent-ops trust grant`
176
+ would fix.
177
+
178
+ `doctor` exits non-zero only when a check `FAIL`s or names a specific
179
+ agent-ops command to run. `UNKNOWN`, `UNSUPPORTED`, and a `DEGRADED` check
180
+ with no such command (for example, a harness that only partially supports a
181
+ capability by design, such as opencode's `lifecycle-summary`) are permanent,
182
+ benign findings and exit 0 — there is nothing to fix.
171
183
 
172
184
  `artifact-staleness` reports `DEGRADED` with `UPDATE_REQUIRED` when a toolkit
173
185
  upgrade or effective profile or capability change makes intact managed rules
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { commonHarnessAdapters, harnessHookPath, HARNESS_IDS } from "../../../runtime/src/install/harness.js";
8
- import { hookRegistrationSatisfied, repositoryTrustStatus, smokeAvailabilityStatus } from "../../../runtime/src/install/probes.js";
8
+ import { hookRegistrationDrift, repositoryTrustStatus, smokeAvailabilityStatus } from "../../../runtime/src/install/probes.js";
9
9
  import { parseInstallManifest } from "../../../runtime/src/fs/manifest.js";
10
10
  import { NpmRegistryClient } from "../../../runtime/src/registry/npm.js";
11
11
  import { TaskService } from "../../../runtime/src/task/service.js";
@@ -159,13 +159,61 @@ else {
159
159
  root,
160
160
  toolkitVersion: CLI_VERSION,
161
161
  probes: {
162
- hookRegistration: async () => hookRegistrationSatisfied({
163
- harness: await installedHarness(root),
164
- config,
165
- sources: await hookSources(root, args.scope === "user" ? "user" : "project")
166
- }),
167
- repositoryTrust: async () => repositoryTrustStatus(await repositoryTrust(root, config, CLI_VERSION)),
168
- smokeAvailability: () => smokeAvailabilityStatus(config),
162
+ hookRegistration: async () => {
163
+ const drifted = hookRegistrationDrift({
164
+ harness: await installedHarness(root),
165
+ config,
166
+ sources: await hookSources(root, args.scope === "user" ? "user" : "project")
167
+ });
168
+ return drifted.length === 0
169
+ ? { status: "PASS" }
170
+ : {
171
+ status: "FAIL",
172
+ message: `Hook registration is missing for ${drifted.join(", ")}.`,
173
+ code: "UPDATE_REQUIRED",
174
+ remediation: "Run `agent-ops update`."
175
+ };
176
+ },
177
+ repositoryTrust: async () => {
178
+ const trust = await repositoryTrust(root, config, CLI_VERSION);
179
+ const status = repositoryTrustStatus(trust);
180
+ if (trust === "STALE") {
181
+ return {
182
+ status,
183
+ message: "Repository trust binding is stale.",
184
+ code: "TRUST_REQUIRED",
185
+ remediation: "Run `agent-ops trust grant`."
186
+ };
187
+ }
188
+ if (trust === "UNTRUSTED") {
189
+ const verificationConfigured = config.verification.commands.length > 0;
190
+ return {
191
+ status,
192
+ message: verificationConfigured
193
+ ? "Repository is not trusted; Stop verification will not run."
194
+ : "Repository is not trusted.",
195
+ ...(verificationConfigured
196
+ ? {
197
+ code: "TRUST_REQUIRED",
198
+ remediation: "Run `agent-ops trust grant`."
199
+ }
200
+ : {
201
+ remediation: "No action needed; trust is only required once verification.commands is set."
202
+ })
203
+ };
204
+ }
205
+ return { status };
206
+ },
207
+ smokeAvailability: () => {
208
+ const status = smokeAvailabilityStatus(config);
209
+ return status === "UNKNOWN"
210
+ ? {
211
+ status,
212
+ message: "verification.commands is empty.",
213
+ remediation: "No action needed; add verification.commands to .agent-ops/config.json to enable smoke checks."
214
+ }
215
+ : { status };
216
+ },
169
217
  reviewTarget: async (target, deep) => await probeReviewTarget(target, { cwd: root, deep })
170
218
  },
171
219
  ...(args.checkAuth === true
@@ -3,13 +3,17 @@ function formatDoctorReport(report) {
3
3
  const surfaces = report.surfaces ?? [];
4
4
  return `${[
5
5
  "Installation doctor",
6
- ...report.checks.map(({ id, status, message, code }) => `- ${status} ${id}${code === undefined ? "" : ` [${code}]`}: ${message}`),
6
+ ...report.checks.flatMap(({ id, status, message, code, remediation }) => [
7
+ `- ${status} ${id}${code === undefined ? "" : ` [${code}]`}: ${message}`,
8
+ ...(remediation === undefined ? [] : [` → ${remediation}`])
9
+ ]),
7
10
  ...(surfaces.length === 0
8
11
  ? []
9
12
  : [
10
13
  "Surfaces:",
11
- ...surfaces.map(({ harness, surfaceId, path, status, managedHandlerCount, foreignHandlerCount }) => `- ${status} ${harness}/${surfaceId}: ${path} ` +
12
- `(managed ${managedHandlerCount}, foreign ${foreignHandlerCount})`)
14
+ ...surfaces.map(({ harness, surfaceId, path, status, managedHandlerCount, foreignHandlerCount, reason }) => `- ${status} ${harness}/${surfaceId}: ${path} ` +
15
+ `(managed ${managedHandlerCount}, foreign ${foreignHandlerCount})` +
16
+ (reason === undefined ? "" : ` (${reason})`))
13
17
  ])
14
18
  ].join("\n")}\n`;
15
19
  }
@@ -37,18 +41,19 @@ export async function runDoctorCommand(options) {
37
41
  : hasDegraded
38
42
  ? "Installation diagnostics found degraded checks."
39
43
  : "Installation diagnostics passed.";
44
+ // Exit non-zero only when there is something to do: a hard failure, or a
45
+ // check that names an agent-ops command via `code`. UNKNOWN, UNSUPPORTED,
46
+ // and codeless DEGRADED (e.g. a harness-declared permanent degradation)
47
+ // are benign findings and must not force a non-zero exit.
48
+ const isActionable = report.checks.some(({ status, code: checkCode }) => status === "FAIL" || checkCode !== undefined);
40
49
  return {
41
50
  code,
42
- status: hasFailure || hasUnsupported || hasUnknown || hasDegraded
43
- ? "error"
44
- : "ok",
51
+ status: isActionable ? "error" : "ok",
45
52
  data: {
46
53
  report,
47
54
  message,
48
55
  text: formatDoctorReport(report)
49
56
  },
50
- errors: hasFailure || hasUnsupported || hasUnknown || hasDegraded
51
- ? [{ code, message }]
52
- : []
57
+ errors: isActionable ? [{ code, message }] : []
53
58
  };
54
59
  }
@@ -1,3 +1,26 @@
1
- // Single source of the published CLI version. Release preparation updates
2
- // this constant; bin.ts and the hook entry both read it from here.
3
- export const CLI_VERSION = "0.1.7";
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ function readPackageVersion() {
5
+ let directory = dirname(fileURLToPath(import.meta.url));
6
+ while (true) {
7
+ try {
8
+ const metadata = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
9
+ if (metadata.name === "@kylecheng3146/agent-ops" &&
10
+ typeof metadata.version === "string") {
11
+ return metadata.version;
12
+ }
13
+ }
14
+ catch {
15
+ // Keep walking until the package root is found.
16
+ }
17
+ const parent = dirname(directory);
18
+ if (parent === directory) {
19
+ break;
20
+ }
21
+ directory = parent;
22
+ }
23
+ throw new Error("Unable to locate the package version.");
24
+ }
25
+ // package.json is the single source of the published CLI version.
26
+ export const CLI_VERSION = readPackageVersion();
@@ -12,10 +12,14 @@ import { inspectHarnessSurfaces, inspectHarnessRegistrations } from "./surface-i
12
12
  const CONFIG_PATH = ".agent-ops/config.json";
13
13
  const MINIMUM_NODE_VERSION = [22, 14, 0];
14
14
  const MAX_DOCTOR_FILE_BYTES = 1024 * 1024;
15
- function check(id, status, message, code) {
16
- return code === undefined
17
- ? { id, status, message }
18
- : { id, status, message, code };
15
+ function check(id, status, message, code, remediation) {
16
+ return {
17
+ id,
18
+ status,
19
+ message,
20
+ ...(code === undefined ? {} : { code }),
21
+ ...(remediation === undefined ? {} : { remediation })
22
+ };
19
23
  }
20
24
  function parseNodeVersion(version) {
21
25
  const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
@@ -38,7 +42,7 @@ function meetsMinimumNodeVersion(version) {
38
42
  function checkNodeVersion(version) {
39
43
  const parsed = parseNodeVersion(version);
40
44
  if (parsed === null || !meetsMinimumNodeVersion(parsed)) {
41
- return check("node-version", "FAIL", `Node ${version} does not meet the minimum version 22.14.0.`);
45
+ return check("node-version", "FAIL", `Node ${version} does not meet the minimum version 22.14.0.`, undefined, "Install Node 22.14.0 or newer.");
42
46
  }
43
47
  return check("node-version", "PASS", `Node ${version} meets the minimum version 22.14.0.`);
44
48
  }
@@ -94,7 +98,7 @@ async function checkManifest(root) {
94
98
  }
95
99
  catch {
96
100
  return {
97
- check: check("manifest", "FAIL", "Installation manifest is missing, unsafe, or invalid.")
101
+ check: check("manifest", "FAIL", "Installation manifest is missing, unsafe, or invalid.", undefined, "Run `agent-ops init` to create a managed installation.")
98
102
  };
99
103
  }
100
104
  }
@@ -105,7 +109,7 @@ async function checkConfig(root) {
105
109
  }
106
110
  catch {
107
111
  return {
108
- check: check("config", "FAIL", "Configuration is missing, unsafe, or invalid JSON.")
112
+ check: check("config", "FAIL", "Configuration is missing, unsafe, or invalid JSON.", undefined, `Fix ${CONFIG_PATH}. Do not run \`agent-ops init\` — it would discard configuration.`)
109
113
  };
110
114
  }
111
115
  const result = validateConfig(parsed);
@@ -114,7 +118,9 @@ async function checkConfig(root) {
114
118
  return {
115
119
  check: check("config", "FAIL", error === undefined
116
120
  ? "Configuration failed validation."
117
- : `Configuration failed validation: ${error.code} at ${error.path}.`)
121
+ : `Configuration failed validation: ${error.code} at ${error.path}.`, undefined, error === undefined
122
+ ? `Fix ${CONFIG_PATH}. Do not run \`agent-ops init\` — it would discard configuration.`
123
+ : `Fix ${error.path} in ${CONFIG_PATH}. Do not run \`agent-ops init\` — it would discard configuration.`)
118
124
  };
119
125
  }
120
126
  return {
@@ -125,7 +131,7 @@ async function checkConfig(root) {
125
131
  async function checkArtifacts(root, manifest) {
126
132
  if (manifest === undefined) {
127
133
  return {
128
- check: check("artifacts", "FAIL", "Artifacts cannot be verified without a valid manifest."),
134
+ check: check("artifacts", "FAIL", "Artifacts cannot be verified without a valid manifest.", undefined, "Run `agent-ops init` to create a managed installation."),
129
135
  hashesByPath: new Map()
130
136
  };
131
137
  }
@@ -151,19 +157,20 @@ async function checkArtifacts(root, manifest) {
151
157
  return {
152
158
  check: failures.length === 0
153
159
  ? check("artifacts", "PASS", "All managed artifacts match their hashes.")
154
- : check("artifacts", "FAIL", `Managed artifacts failed verification: ${failures.join(", ")}.`),
160
+ : check("artifacts", "FAIL", `Managed artifacts failed verification: ${failures.join(", ")}.`, "UPDATE_REQUIRED", `Run \`agent-ops update\` to restore managed artifacts. This overwrites ` +
161
+ `${failures.join(", ")}; any local edits to those files will be lost.`),
155
162
  hashesByPath
156
163
  };
157
164
  }
158
165
  function checkArtifactStaleness(manifest, config, artifacts, toolkitVersion) {
159
166
  if (manifest === undefined || config === undefined) {
160
- return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness cannot be assessed without a valid manifest and configuration.");
167
+ return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness cannot be assessed without a valid manifest and configuration.", undefined, "No action needed; fix the manifest or config check above first.");
161
168
  }
162
169
  if (artifacts.check.status !== "PASS") {
163
- return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness cannot be assessed until artifact integrity passes.");
170
+ return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness cannot be assessed until artifact integrity passes.", undefined, "No action needed; fix the artifacts check above first.");
164
171
  }
165
172
  if (toolkitVersion === undefined) {
166
- return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness cannot be assessed without the running toolkit version.");
173
+ return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness cannot be assessed without the running toolkit version.", undefined, "No action needed; the CLI did not report its own version.");
167
174
  }
168
175
  const expectedHashesByPath = new Map();
169
176
  try {
@@ -184,13 +191,13 @@ function checkArtifactStaleness(manifest, config, artifacts, toolkitVersion) {
184
191
  }
185
192
  }
186
193
  catch {
187
- return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness could not be assessed safely.");
194
+ return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness could not be assessed safely.", undefined, "No action needed; staleness could not be computed.");
188
195
  }
189
196
  const stalePaths = [];
190
197
  for (const [path, expectedHash] of expectedHashesByPath) {
191
198
  const actualHash = artifacts.hashesByPath.get(path);
192
199
  if (actualHash === undefined) {
193
- return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness could not be assessed safely.");
200
+ return check("artifact-staleness", "UNKNOWN", "Managed artifact staleness could not be assessed safely.", undefined, "No action needed; staleness could not be computed.");
194
201
  }
195
202
  if (actualHash !== expectedHash) {
196
203
  stalePaths.push(path);
@@ -198,11 +205,11 @@ function checkArtifactStaleness(manifest, config, artifacts, toolkitVersion) {
198
205
  }
199
206
  return stalePaths.length === 0
200
207
  ? check("artifact-staleness", "PASS", "Managed artifacts match the current toolkit and configuration.")
201
- : check("artifact-staleness", "DEGRADED", `Managed artifacts need update: ${stalePaths.join(", ")}; run agent-ops update.`, "UPDATE_REQUIRED");
208
+ : check("artifact-staleness", "DEGRADED", `Managed artifacts need update: ${stalePaths.join(", ")}; run agent-ops update.`, "UPDATE_REQUIRED", "Run `agent-ops update`.");
202
209
  }
203
210
  async function checkMarkers(root, manifest) {
204
211
  if (manifest === undefined) {
205
- return check("markers", "FAIL", "Managed blocks cannot be verified without a valid manifest.");
212
+ return check("markers", "FAIL", "Managed blocks cannot be verified without a valid manifest.", undefined, "Run `agent-ops init` to create a managed installation.");
206
213
  }
207
214
  const failures = [];
208
215
  const legacyPaths = [];
@@ -225,25 +232,39 @@ async function checkMarkers(root, manifest) {
225
232
  }
226
233
  }
227
234
  if (failures.length > 0) {
228
- return check("markers", "FAIL", `Managed block markers failed verification: ${failures.join(", ")}.`);
235
+ return check("markers", "FAIL", `Managed block markers failed verification: ${failures.join(", ")}.`, "UPDATE_REQUIRED", "Run `agent-ops update`.");
229
236
  }
230
237
  if (legacyPaths.length > 0) {
231
- return check("markers", "DEGRADED", `Legacy managed routing blocks need migration: ${legacyPaths.join(", ")}.`);
238
+ return check("markers", "DEGRADED", `Legacy managed routing blocks need migration: ${legacyPaths.join(", ")}.`, "UPDATE_REQUIRED", "Run `agent-ops update`.");
232
239
  }
233
240
  return check("markers", "PASS", "All managed block markers are intact.");
234
241
  }
242
+ function defaultProbeRemediation(status) {
243
+ return status === "PASS" || status === "FAIL"
244
+ ? undefined
245
+ : "No action needed; nothing to verify yet.";
246
+ }
235
247
  async function checkProbe(id, probe) {
236
248
  if (probe === undefined) {
237
- return check(id, "UNKNOWN", "No probe was provided.");
249
+ return check(id, "UNKNOWN", "No probe was provided.", undefined, "No action needed; this check requires wiring from the CLI.");
238
250
  }
239
251
  try {
240
252
  const result = await probe();
253
+ if (typeof result === "object") {
254
+ const status = result.status;
255
+ return check(id, status, result.message ??
256
+ (status === "PASS"
257
+ ? "Probe passed."
258
+ : status === "FAIL"
259
+ ? "Probe failed."
260
+ : "Probe has nothing to verify yet."), result.code, result.remediation ?? defaultProbeRemediation(status));
261
+ }
241
262
  const status = typeof result === "boolean" ? (result ? "PASS" : "FAIL") : result;
242
263
  return check(id, status, status === "PASS"
243
264
  ? "Probe passed."
244
265
  : status === "FAIL"
245
266
  ? "Probe failed."
246
- : "Probe has nothing to verify yet.");
267
+ : "Probe has nothing to verify yet.", undefined, defaultProbeRemediation(status));
247
268
  }
248
269
  catch {
249
270
  return check(id, "FAIL", "Probe failed.");
@@ -251,7 +272,7 @@ async function checkProbe(id, probe) {
251
272
  }
252
273
  function checkLifecycleSummary(manifest, config) {
253
274
  if (manifest === undefined || config === undefined) {
254
- return check("lifecycle-summary", "UNKNOWN", "Lifecycle summary cannot be assessed without a valid manifest and configuration.");
275
+ return check("lifecycle-summary", "UNKNOWN", "Lifecycle summary cannot be assessed without a valid manifest and configuration.", undefined, "No action needed; fix the manifest or config check above first.");
255
276
  }
256
277
  const hasLifecycleSummary = config.profiles.length > 0 &&
257
278
  resolveProfiles(config.profiles).capabilities.includes("lifecycle-summary");
@@ -266,32 +287,32 @@ function checkLifecycleSummary(manifest, config) {
266
287
  .filter(({ registration }) => registration === undefined)
267
288
  .map(({ id }) => id);
268
289
  if (missing.length > 0) {
269
- return check("lifecycle-summary", "UNKNOWN", `Lifecycle summary registration is missing for ${missing.join(", ")}.`);
290
+ return check("lifecycle-summary", "UNKNOWN", `Lifecycle summary registration is missing for ${missing.join(", ")}.`, undefined, "No action needed; the named harness has no lifecycle-summary registration.");
270
291
  }
271
292
  const unsupported = registrations
272
293
  .filter(({ registration }) => registration?.support === "unsupported")
273
294
  .map(({ id }) => id);
274
295
  if (unsupported.length > 0) {
275
- return check("lifecycle-summary", "UNSUPPORTED", `Lifecycle summary is not dispatched for ${unsupported.join(", ")}; advisory runtime wiring is unavailable.`);
296
+ return check("lifecycle-summary", "UNSUPPORTED", `Lifecycle summary is not dispatched for ${unsupported.join(", ")}; advisory runtime wiring is unavailable.`, undefined, "No action needed; the named harness does not dispatch lifecycle-summary.");
276
297
  }
277
298
  const degraded = registrations
278
299
  .filter(({ registration }) => registration?.support === "degraded")
279
300
  .map(({ id }) => id);
280
301
  if (degraded.length > 0) {
281
- return check("lifecycle-summary", "DEGRADED", `Lifecycle summary is degraded for ${degraded.join(", ")}.`);
302
+ return check("lifecycle-summary", "DEGRADED", `Lifecycle summary is degraded for ${degraded.join(", ")}.`, undefined, "No action needed; the named harness only partially supports lifecycle-summary.");
282
303
  }
283
304
  const unknown = registrations
284
305
  .filter(({ registration }) => registration?.support === "unknown")
285
306
  .map(({ id }) => id);
286
307
  if (unknown.length > 0) {
287
- return check("lifecycle-summary", "UNKNOWN", `Lifecycle summary support is unknown for ${unknown.join(", ")}.`);
308
+ return check("lifecycle-summary", "UNKNOWN", `Lifecycle summary support is unknown for ${unknown.join(", ")}.`, undefined, "No action needed; support for the named harness has not been characterized.");
288
309
  }
289
310
  return check("lifecycle-summary", "PASS", "Lifecycle summary is reachable for every selected harness.");
290
311
  }
291
312
  async function checkSurfaceInventory(root, manifest, config) {
292
313
  if (manifest === undefined || config === undefined) {
293
314
  return {
294
- check: check("surface-inventory", "UNKNOWN", "Harness surfaces cannot be inventoried without a valid manifest and configuration."),
315
+ check: check("surface-inventory", "UNKNOWN", "Harness surfaces cannot be inventoried without a valid manifest and configuration.", undefined, "No action needed; fix the manifest or config check above first."),
295
316
  surfaces: []
296
317
  };
297
318
  }
@@ -306,20 +327,22 @@ async function checkSurfaceInventory(root, manifest, config) {
306
327
  return {
307
328
  check: check("surface-inventory", unknown.length > 0 ? "UNKNOWN" : "PASS", unknown.length > 0
308
329
  ? `${unknown.length} harness surface(s) could not be inspected.`
309
- : "Harness surfaces were inventoried without exposing settings values."),
330
+ : "Harness surfaces were inventoried without exposing settings values.", undefined, unknown.length > 0
331
+ ? "No action needed; see Surfaces below for which ones and why."
332
+ : undefined),
310
333
  surfaces
311
334
  };
312
335
  }
313
336
  catch {
314
337
  return {
315
- check: check("surface-inventory", "UNKNOWN", "Harness surfaces could not be inspected safely."),
338
+ check: check("surface-inventory", "UNKNOWN", "Harness surfaces could not be inspected safely.", undefined, "No action needed; inspection failed safely."),
316
339
  surfaces: []
317
340
  };
318
341
  }
319
342
  }
320
343
  async function checkRegistrationDrift(root, manifest, config) {
321
344
  if (manifest === undefined || config === undefined) {
322
- return check("registration-drift", "UNKNOWN", "Hook registration drift cannot be assessed without a valid manifest and configuration.");
345
+ return check("registration-drift", "UNKNOWN", "Hook registration drift cannot be assessed without a valid manifest and configuration.", undefined, "No action needed; fix the manifest or config check above first.");
323
346
  }
324
347
  try {
325
348
  const statuses = await inspectHarnessRegistrations({
@@ -332,19 +355,12 @@ async function checkRegistrationDrift(root, manifest, config) {
332
355
  .map(({ harness }) => harness);
333
356
  return drifted.length === 0
334
357
  ? check("registration-drift", "PASS", "Managed hook registrations match the desired capabilities.")
335
- : check("registration-drift", "FAIL", `Hook registration drift detected for ${drifted.join(", ")}; run agent-ops update.`, "UPDATE_REQUIRED");
358
+ : check("registration-drift", "FAIL", `Hook registration drift detected for ${drifted.join(", ")}; run agent-ops update.`, "UPDATE_REQUIRED", "Run `agent-ops update`.");
336
359
  }
337
360
  catch {
338
- return check("registration-drift", "UNKNOWN", "Hook registration drift could not be assessed safely.");
361
+ return check("registration-drift", "UNKNOWN", "Hook registration drift could not be assessed safely.", undefined, "No action needed; drift could not be computed.");
339
362
  }
340
363
  }
341
- /**
342
- * Guidance lives in `message` rather than a `remediation` field: as of this
343
- * check, `remediation` does not exist on DoctorCheck. Because target
344
- * authentication failures surface as one unexplained review failure — the
345
- * chain deliberately does not sniff stderr for "not logged in" — this text is
346
- * the operator's only route out, so it names the exact command.
347
- */
348
364
  async function checkReviewTargets(config, probe, checkAuth) {
349
365
  const targets = config?.reviewRoles?.find((role) => role.role === "independent-review")?.targets ?? [];
350
366
  if (targets.length === 0) {
@@ -357,20 +373,16 @@ async function checkReviewTargets(config, probe, checkAuth) {
357
373
  for (const target of targets) {
358
374
  const result = await probe(target, checkAuth);
359
375
  if (result === "missing-executable") {
360
- return check("review-targets", "FAIL", `${target} not found. Install it, or remove "${target}" from ` +
361
- "reviewRoles[].targets.", "UPDATE_REQUIRED");
376
+ return check("review-targets", "FAIL", `${target} not found.`, undefined, `Install ${target}, or remove "${target}" from reviewRoles[].targets.`);
362
377
  }
363
378
  if (result === "ineligible") {
364
- return check("review-targets", "FAIL", `${target} has no read-only mode and cannot review. Remove ` +
365
- `"${target}" from reviewRoles[].targets.`, "UPDATE_REQUIRED");
379
+ return check("review-targets", "FAIL", `${target} has no read-only mode and cannot review.`, undefined, `Remove "${target}" from reviewRoles[].targets.`);
366
380
  }
367
381
  if (result === "timeout") {
368
- return check("review-targets", "FAIL", `${target} did not answer in time. Re-run: ` +
369
- "agent-ops doctor --check-auth", "UPDATE_REQUIRED");
382
+ return check("review-targets", "FAIL", `${target} did not answer in time.`, undefined, "Re-run: agent-ops doctor --check-auth");
370
383
  }
371
384
  if (checkAuth && result !== "ok") {
372
- return check("review-targets", "FAIL", `${target} is installed but not authenticated, or it rejected the ` +
373
- `call. Run: ${target} login`, "UPDATE_REQUIRED");
385
+ return check("review-targets", "FAIL", `${target} is installed but not authenticated, or it rejected the call.`, undefined, `Run: ${target} login`);
374
386
  }
375
387
  }
376
388
  return check("review-targets", "PASS", checkAuth
@@ -1,19 +1,27 @@
1
1
  import { harnessDescriptor } from "./harness.js";
2
2
  import { resolveCapabilities } from "./profiles.js";
3
3
  /**
4
- * Hook registration is satisfied when every hook event implied by the
5
- * installed profiles carries an agent-ops owned handler for every installed
6
- * harness. Installations without hook capabilities have nothing to register.
4
+ * Returns the harness ids missing an agent-ops owned handler for the hook
5
+ * events implied by the installed profiles. Empty when installations without
6
+ * hook capabilities have nothing to register.
7
7
  */
8
- export function hookRegistrationSatisfied(input) {
8
+ export function hookRegistrationDrift(input) {
9
9
  const capabilities = input.config.profiles.length === 0
10
10
  ? []
11
11
  : resolveCapabilities(input.config).capabilities;
12
- return input.harness.every((id) => {
12
+ return input.harness.filter((id) => {
13
13
  const descriptor = harnessDescriptor(id);
14
- return descriptor.control.hookRegistered(input.sources[id], capabilities);
14
+ return !descriptor.control.hookRegistered(input.sources[id], capabilities);
15
15
  });
16
16
  }
17
+ /**
18
+ * Hook registration is satisfied when every hook event implied by the
19
+ * installed profiles carries an agent-ops owned handler for every installed
20
+ * harness. Installations without hook capabilities have nothing to register.
21
+ */
22
+ export function hookRegistrationSatisfied(input) {
23
+ return hookRegistrationDrift(input).length === 0;
24
+ }
17
25
  /**
18
26
  * Smoke availability stays UNKNOWN until the repository declares a
19
27
  * verification command; the toolkit never invents one.
@@ -242,7 +242,8 @@ export async function inspectHarnessSurfaces(options) {
242
242
  path: surface.path,
243
243
  status: "unknown",
244
244
  managedHandlerCount: 0,
245
- foreignHandlerCount: 0
245
+ foreignHandlerCount: 0,
246
+ reason: "Outside the installation root; not inspected by design."
246
247
  });
247
248
  continue;
248
249
  }
@@ -255,7 +256,10 @@ export async function inspectHarnessSurfaces(options) {
255
256
  path: surface.path,
256
257
  status: "missing",
257
258
  managedHandlerCount: 0,
258
- foreignHandlerCount: 0
259
+ foreignHandlerCount: 0,
260
+ ...(surface.access === "inspect-only"
261
+ ? { reason: "Optional file that agent-ops never writes." }
262
+ : {})
259
263
  });
260
264
  continue;
261
265
  }
@@ -203,7 +203,16 @@ alters an intact path-independent managed rules artifact,
203
203
  `artifact-staleness` reports `DEGRADED` with `UPDATE_REQUIRED`. `agent-ops
204
204
  update` regenerates the artifact and clears that result; a missing or
205
205
  hash-mismatched artifact remains an `artifacts` `FAIL`. Without the new trust
206
- grant, trust-gated hooks remain stale. Stop is report-only: it continues the
206
+ grant, trust-gated hooks remain stale.
207
+
208
+ Doctor never writes, and some findings have no fix: a surface outside the
209
+ installation root, or a capability a harness only partially supports by
210
+ descriptor declaration (opencode's `lifecycle-summary`, for example), report
211
+ `UNKNOWN` or `DEGRADED` permanently and exit 0. CI that wants automatic
212
+ repair calls `agent-ops update` / `agent-ops trust grant` directly rather
213
+ than parsing doctor's output.
214
+
215
+ Stop is report-only: it continues the
207
216
  harness for `PASS`, `FAIL`, or `UNKNOWN`, emits only bounded command ID, exit
208
217
  code, test-count, config-hash, and timestamp evidence, and never completes a
209
218
  task. Config v1 migrates deterministically to v2 with Stop disabled; old
@@ -183,7 +183,15 @@ agent-ops trust grant
183
183
  path-independent managed rules artifact 改變時,`artifact-staleness` 會回報帶有
184
184
  `UPDATE_REQUIRED` 的 `DEGRADED`。`agent-ops update` 會重新產生 artifact 並清除
185
185
  這個結果;artifact 缺失或 hash 不符時,`artifacts` check 仍為 `FAIL`。未重新
186
- grant trust 時,trust-gated hook 仍會是 stale。Stop 是 report-only:`PASS`、`FAIL`
186
+ grant trust 時,trust-gated hook 仍會是 stale。
187
+
188
+ doctor 從不寫入檔案,部分結果本來就無法修復:安裝根目錄以外的 surface,
189
+ 或 harness 依 descriptor 宣告只部分支援的 capability(例如 opencode 的
190
+ `lifecycle-summary`),會永久回報 `UNKNOWN` 或 `DEGRADED` 且 exit 0。若 CI
191
+ 需要自動修復,請直接呼叫 `agent-ops update` / `agent-ops trust grant`,
192
+ 不要解析 doctor 的輸出。
193
+
194
+ Stop 是 report-only:`PASS`、`FAIL`
187
195
  與 `UNKNOWN` 都會讓 harness 繼續,只輸出有界的 command ID、exit code、test-count、
188
196
  config-hash 與 timestamp evidence,且永遠不會完成 task。Config v1 會決定性遷移
189
197
  為 Stop disabled 的 v2;舊 binary 無法讀取遷移後的 config,routing migration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylecheng3146/agent-ops",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Evidence-driven development loops for Codex, Claude Code, and opencode",
5
5
  "type": "module",
6
6
  "license": "MIT",