@kylecheng3146/agent-ops 0.1.9 → 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 +15 -3
- package/dist/packages/cli/src/bin.js +56 -8
- package/dist/packages/cli/src/commands/doctor.js +14 -9
- package/dist/runtime/src/install/doctor.js +59 -47
- package/dist/runtime/src/install/probes.js +14 -6
- package/dist/runtime/src/install/surface-inspection.js +6 -2
- package/docs/en/guides/configuration.md +10 -1
- package/docs/zh-TW/guides/configuration.md +9 -1
- package/package.json +1 -1
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`
|
|
169
|
-
`trust grant` runs
|
|
170
|
-
verification
|
|
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 {
|
|
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 () =>
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
51
|
-
? [{ code, message }]
|
|
52
|
-
: []
|
|
57
|
+
errors: isActionable ? [{ code, message }] : []
|
|
53
58
|
};
|
|
54
59
|
}
|
|
@@ -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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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.
|
|
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.
|
|
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。
|
|
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
|