@gmickel/gno 1.37.1 → 1.39.0

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 (47) hide show
  1. package/assets/skill/README.md +2 -0
  2. package/browser-extension/artifacts/{gno-browser-clipper-v1.37.1.zip → gno-browser-clipper-v1.39.0.zip} +0 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.39.0.zip.sha256 +1 -0
  4. package/browser-extension/dist/manifest.json +1 -1
  5. package/package.json +1 -1
  6. package/spec/cli.md +243 -26
  7. package/spec/output-schemas/agents-mutation.schema.json +108 -0
  8. package/spec/output-schemas/agents-verify.schema.json +89 -0
  9. package/src/cli/commands/agents/block.ts +164 -0
  10. package/src/cli/commands/agents/commands.ts +413 -0
  11. package/src/cli/commands/agents/engine.ts +417 -0
  12. package/src/cli/commands/agents/harnesses.ts +298 -0
  13. package/src/cli/commands/agents/index.ts +35 -0
  14. package/src/cli/commands/cleanup.ts +8 -2
  15. package/src/cli/commands/collection/clear-embeddings.ts +6 -1
  16. package/src/cli/commands/completion/scripts.ts +5 -0
  17. package/src/cli/commands/doctor-activation.ts +5 -1
  18. package/src/cli/commands/doctor.ts +72 -2
  19. package/src/cli/commands/embed.ts +227 -194
  20. package/src/cli/commands/index-cmd.ts +74 -50
  21. package/src/cli/commands/init.ts +5 -1
  22. package/src/cli/commands/profile-apply.ts +5 -1
  23. package/src/cli/commands/setup-activation.ts +2 -1
  24. package/src/cli/commands/setup.ts +2 -1
  25. package/src/cli/commands/shared.ts +5 -1
  26. package/src/cli/commands/status.ts +5 -1
  27. package/src/cli/commands/tags.ts +18 -3
  28. package/src/cli/commands/update.ts +34 -27
  29. package/src/cli/commands/vec.ts +13 -4
  30. package/src/cli/errors.ts +3 -2
  31. package/src/cli/program.ts +449 -194
  32. package/src/config/defaults.ts +2 -0
  33. package/src/config/index.ts +3 -0
  34. package/src/config/types.ts +32 -1
  35. package/src/core/file-lock.ts +16 -4
  36. package/src/core/write-lease.ts +354 -0
  37. package/src/embed/backlog.ts +9 -1
  38. package/src/embed/retry.ts +116 -3
  39. package/src/sdk/client.ts +3 -1
  40. package/src/sdk/embed.ts +8 -3
  41. package/src/sdk/types.ts +2 -0
  42. package/src/serve/embed-scheduler.ts +8 -0
  43. package/src/serve/resident-runtime.ts +5 -1
  44. package/src/store/sqlite/adapter.ts +28 -4
  45. package/src/store/sqlite/scoped-index.ts +5 -1
  46. package/src/store/vector/sqlite-vec.ts +2 -1
  47. package/browser-extension/artifacts/gno-browser-clipper-v1.37.1.zip.sha256 +0 -1
@@ -0,0 +1,164 @@
1
+ /**
2
+ * GNO agents protocol block: content, markers, hashing, extraction.
3
+ *
4
+ * One compact, versioned instruction block bounded by stable BEGIN/END
5
+ * markers. Install/update/uninstall touch ONLY the owned block; content
6
+ * outside the markers stays byte-identical. The block content is static —
7
+ * identical on every machine — so a block is current exactly when its stamp
8
+ * version matches the installed release and its hash matches its body.
9
+ *
10
+ * @module src/cli/commands/agents/block
11
+ */
12
+
13
+ import { CliError } from "../../errors.js";
14
+
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+ // Constants
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+
19
+ /** Version of the protocol block content. Bump on any content change. */
20
+ export const BLOCK_VERSION = 2;
21
+
22
+ /** Stable across block versions — never change these once shipped. */
23
+ export const BEGIN_MARKER = "<!-- gno:agents:begin -->";
24
+ export const END_MARKER = "<!-- gno:agents:end -->";
25
+
26
+ const STAMP_RE = /^<!-- gno-agents block v(\d+) sha256:([0-9a-f]{16}) /;
27
+ const HASH_PREFIX_LENGTH = 16;
28
+
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+ // Rendering
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+
33
+ /**
34
+ * The protocol block body (between stamp line and end marker). Compact by
35
+ * design: the retrieval ladder + the writing contract. Detailed workflows live
36
+ * in the GNO skill; the block just points at it. No filesystem paths — the
37
+ * text is the same on every machine.
38
+ */
39
+ export function renderBlockBody(): string {
40
+ return `## GNO knowledge retrieval
41
+
42
+ Local knowledge search over indexed collections. Source files are the truth; the GNO index is disposable, machine-local.
43
+
44
+ Ladder — scope to a collection first (\`--collection <name>\`):
45
+
46
+ 1. Exact term/identifier/quote/error: \`gno search "<text>"\`
47
+ 2. Entity or known document: \`gno query "<question>" --fast -n 10\`
48
+ 3. Multi-document evidence for a goal: \`gno context build "<goal>" --budget 12000\`
49
+ 4. Change/dependency questions: \`gno changes\` / \`gno diff <doc>\` / \`gno impact <doc>\`
50
+ 5. Generated factual answer: \`gno ask "<question>" --verify\` (abstention is valid)
51
+ 6. Expected document missing: reformulate + re-check collection scope (\`gno query diagnose "<query>" --target <doc>\`) before any grep fallback.
52
+
53
+ Writing: retrieve first — a question alone is read-only. Edit an existing canonical note in its source file; \`gno capture\` creates genuinely new notes (collection, title/path, source kind, provenance) — never an update API. After writes: reindex the collection, verify retrieval.
54
+
55
+ Cite with gno:// URIs. Advanced retrieval (structured queries, filters, backlinks, similar, capture recipes) lives in the \`gno\` skill: load it (\`/gno\`) when installed, otherwise run \`gno skill install --scope user\` first.`;
56
+ }
57
+
58
+ /** SHA-256 hex digest of the body, truncated for the stamp line. */
59
+ export function hashBlockBody(body: string): string {
60
+ const hasher = new Bun.CryptoHasher("sha256");
61
+ hasher.update(body);
62
+ return hasher.digest("hex").slice(0, HASH_PREFIX_LENGTH);
63
+ }
64
+
65
+ /** Render the stamp line carrying version + body hash. */
66
+ export function renderStampLine(body: string): string {
67
+ return `<!-- gno-agents block v${BLOCK_VERSION} sha256:${hashBlockBody(body)} — managed by \`gno agents\`; manual edits inside the markers are overwritten -->`;
68
+ }
69
+
70
+ /** Render the complete block: BEGIN marker, stamp, body, END marker. */
71
+ export function renderBlock(): string {
72
+ const body = renderBlockBody();
73
+ return `${BEGIN_MARKER}\n${renderStampLine(body)}\n${body}\n${END_MARKER}`;
74
+ }
75
+
76
+ /** True when the extracted stamp's hash matches the extracted body. */
77
+ export function stampAuthenticates(block: ExtractedBlock): boolean {
78
+ return block.stamp !== null && block.stamp.hash === hashBlockBody(block.body);
79
+ }
80
+
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+ // Extraction & Validation
83
+ // ─────────────────────────────────────────────────────────────────────────────
84
+
85
+ export interface ExtractedBlock {
86
+ /** Offset of the BEGIN marker in the file content. */
87
+ start: number;
88
+ /** Offset just past the END marker. */
89
+ end: number;
90
+ /** Everything between the markers (stamp line + body), without markers. */
91
+ inner: string;
92
+ /** Body without the stamp line (equal to inner when no stamp present). */
93
+ body: string;
94
+ /** Parsed stamp, when present and well-formed. */
95
+ stamp: { version: number; hash: string } | null;
96
+ }
97
+
98
+ export type BlockExtraction =
99
+ | { found: false }
100
+ | { found: true; block: ExtractedBlock };
101
+
102
+ function countOccurrences(haystack: string, needle: string): number {
103
+ let count = 0;
104
+ let idx = haystack.indexOf(needle);
105
+ while (idx !== -1) {
106
+ count += 1;
107
+ idx = haystack.indexOf(needle, idx + needle.length);
108
+ }
109
+ return count;
110
+ }
111
+
112
+ const MARKER_GUIDANCE =
113
+ "Fix or remove the markers manually (or restore the file from its .gno-agents.bak backup), then re-run.";
114
+
115
+ /**
116
+ * Extract the managed block from file content.
117
+ * Fail-closed: malformed or duplicate markers throw with guidance — the
118
+ * installer never guesses or "repairs".
119
+ */
120
+ export function extractBlock(
121
+ content: string,
122
+ filePath: string
123
+ ): BlockExtraction {
124
+ const begins = countOccurrences(content, BEGIN_MARKER);
125
+ const ends = countOccurrences(content, END_MARKER);
126
+
127
+ if (begins === 0 && ends === 0) {
128
+ return { found: false };
129
+ }
130
+ if (begins !== 1 || ends !== 1) {
131
+ throw new CliError(
132
+ "VALIDATION",
133
+ `Malformed GNO agents markers in ${filePath}: found ${begins} BEGIN and ${ends} END marker(s), expected exactly one of each. ${MARKER_GUIDANCE}`
134
+ );
135
+ }
136
+
137
+ const start = content.indexOf(BEGIN_MARKER);
138
+ const endMarkerStart = content.indexOf(END_MARKER);
139
+ if (endMarkerStart < start) {
140
+ throw new CliError(
141
+ "VALIDATION",
142
+ `Malformed GNO agents markers in ${filePath}: END marker appears before BEGIN marker. ${MARKER_GUIDANCE}`
143
+ );
144
+ }
145
+
146
+ const end = endMarkerStart + END_MARKER.length;
147
+ const rawInner = content.slice(start + BEGIN_MARKER.length, endMarkerStart);
148
+ // Trim exactly the structural newlines install added around the inner text.
149
+ const inner = rawInner.replace(/^\n/, "").replace(/\n$/, "");
150
+
151
+ const newlineIdx = inner.indexOf("\n");
152
+ const firstLine = newlineIdx === -1 ? inner : inner.slice(0, newlineIdx);
153
+ const stampMatch = STAMP_RE.exec(firstLine);
154
+ const version = stampMatch ? Number(stampMatch[1]) : Number.NaN;
155
+ // An absurd version (not a safe integer) is an unparseable stamp, not a
156
+ // numeric verdict — it would otherwise serialize as null in JSON receipts.
157
+ const stamp =
158
+ stampMatch && Number.isSafeInteger(version)
159
+ ? { version, hash: stampMatch[2] ?? "" }
160
+ : null;
161
+ const body = stamp && newlineIdx !== -1 ? inner.slice(newlineIdx + 1) : inner;
162
+
163
+ return { found: true, block: { start, end, inner, body, stamp } };
164
+ }
@@ -0,0 +1,413 @@
1
+ /**
2
+ * `gno agents` command runners: install, update, verify, uninstall.
3
+ *
4
+ * @module src/cli/commands/agents/commands
5
+ */
6
+
7
+ import { CliError } from "../../errors.js";
8
+ import { getGlobals } from "../../program.js";
9
+ import {
10
+ BLOCK_VERSION,
11
+ extractBlock,
12
+ renderBlock,
13
+ stampAuthenticates,
14
+ } from "./block.js";
15
+ import {
16
+ applyPlan,
17
+ decodeInstructionFile,
18
+ type PlanMode,
19
+ planTargets,
20
+ planWrites,
21
+ type TargetPlan,
22
+ unifiedDiff,
23
+ } from "./engine.js";
24
+ import {
25
+ type HarnessId,
26
+ HARNESS_IDS,
27
+ type ResolvedTarget,
28
+ resolveTargets,
29
+ } from "./harnesses.js";
30
+
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+ // Shared
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+
35
+ export interface AgentsOptions {
36
+ target?: HarnessId | "all";
37
+ extraDirs?: string[];
38
+ dryRun?: boolean;
39
+ json?: boolean;
40
+ quiet?: boolean;
41
+ /** Override for testing / sandboxed live verification. */
42
+ homeDir?: string;
43
+ }
44
+
45
+ function safeGetGlobals(): { json: boolean; quiet: boolean } {
46
+ try {
47
+ return getGlobals();
48
+ } catch {
49
+ return { json: false, quiet: false };
50
+ }
51
+ }
52
+
53
+ export function parseTargetOption(raw: string): HarnessId | "all" {
54
+ if (raw === "all" || (HARNESS_IDS as string[]).includes(raw)) {
55
+ return raw as HarnessId | "all";
56
+ }
57
+ throw new CliError(
58
+ "VALIDATION",
59
+ `Invalid target: ${raw}. Must be one of ${HARNESS_IDS.join(", ")}, or 'all'.`
60
+ );
61
+ }
62
+
63
+ function outputSettings(opts: AgentsOptions): {
64
+ json: boolean;
65
+ quiet: boolean;
66
+ } {
67
+ const globals = safeGetGlobals();
68
+ return {
69
+ json: opts.json ?? globals.json,
70
+ quiet: opts.quiet ?? globals.quiet,
71
+ };
72
+ }
73
+
74
+ interface TargetReport {
75
+ target: string;
76
+ label: string;
77
+ path: string;
78
+ action: string;
79
+ detected: boolean;
80
+ via?: string;
81
+ detail?: string;
82
+ backup?: string | null;
83
+ }
84
+
85
+ function reportFor(
86
+ plan: TargetPlan,
87
+ backup: string | null | undefined
88
+ ): TargetReport {
89
+ return {
90
+ target: plan.target.id,
91
+ label: plan.target.label,
92
+ path: plan.target.file,
93
+ action: plan.action,
94
+ detected: plan.target.detected,
95
+ ...(plan.via && { via: plan.via }),
96
+ ...(plan.detail && { detail: plan.detail }),
97
+ ...(backup !== undefined && { backup }),
98
+ };
99
+ }
100
+
101
+ function printHumanReports(reports: TargetReport[]): void {
102
+ for (const r of reports) {
103
+ const via = r.via ? ` (${r.detail ?? `via ${r.via}`})` : "";
104
+ const detail = !r.via && r.detail ? `: ${r.detail}` : "";
105
+ process.stdout.write(
106
+ `${r.action.padEnd(12)} ${r.target.padEnd(10)} ${r.path}${via}${detail}\n`
107
+ );
108
+ }
109
+ }
110
+
111
+ // ─────────────────────────────────────────────────────────────────────────────
112
+ // Install / Update / Uninstall (shared mutation runner)
113
+ // ─────────────────────────────────────────────────────────────────────────────
114
+
115
+ async function runMutation(
116
+ verb: "install" | "update" | "uninstall",
117
+ mode: PlanMode,
118
+ opts: AgentsOptions
119
+ ): Promise<void> {
120
+ const { json, quiet } = outputSettings(opts);
121
+ const dryRun = opts.dryRun ?? false;
122
+
123
+ const targets = resolveTargets(opts.target ?? "all", {
124
+ homeDir: opts.homeDir,
125
+ extraDirs: opts.extraDirs,
126
+ });
127
+ const plans = await planTargets(targets, mode);
128
+
129
+ const reports: TargetReport[] = [];
130
+ const diffs: string[] = [];
131
+ const failedPaths: string[] = [];
132
+ let validationErrors = 0;
133
+
134
+ for (const plan of plans) {
135
+ if (plan.action === "error") {
136
+ failedPaths.push(plan.target.file);
137
+ if (plan.errorCode !== "RUNTIME") {
138
+ validationErrors += 1;
139
+ }
140
+ reports.push(reportFor(plan, undefined));
141
+ continue;
142
+ }
143
+ if (!planWrites(plan)) {
144
+ reports.push(reportFor(plan, undefined));
145
+ continue;
146
+ }
147
+ if (dryRun) {
148
+ diffs.push(
149
+ unifiedDiff(plan.oldContent, plan.newContent, plan.target.file)
150
+ );
151
+ reports.push(reportFor(plan, null));
152
+ continue;
153
+ }
154
+ // A failing write must not abort the run before the receipt is emitted:
155
+ // earlier targets may already have been written, and the operator needs
156
+ // to see exactly which. Record an `error` row and keep going.
157
+ try {
158
+ reports.push(reportFor(plan, await applyPlan(plan)));
159
+ } catch (err) {
160
+ failedPaths.push(plan.target.file);
161
+ reports.push({
162
+ ...reportFor(plan, undefined),
163
+ action: "error",
164
+ detail: err instanceof Error ? err.message : String(err),
165
+ });
166
+ }
167
+ }
168
+
169
+ // Fallback: when the installer could not apply the block somewhere, hand
170
+ // the operator the exact block to paste (install/update only — uninstall
171
+ // guidance is in the per-target detail).
172
+ const manualBlock =
173
+ failedPaths.length > 0 && mode === "install" ? renderBlock() : undefined;
174
+
175
+ if (json) {
176
+ process.stdout.write(
177
+ `${JSON.stringify(
178
+ {
179
+ command: verb,
180
+ blockVersion: BLOCK_VERSION,
181
+ dryRun,
182
+ results: reports,
183
+ ...(dryRun && { diffs }),
184
+ ...(manualBlock !== undefined && { manualBlock }),
185
+ },
186
+ null,
187
+ 2
188
+ )}\n`
189
+ );
190
+ } else if (!quiet) {
191
+ printHumanReports(reports);
192
+ if (dryRun) {
193
+ for (const diff of diffs) {
194
+ if (diff) {
195
+ process.stdout.write(`\n${diff}\n`);
196
+ }
197
+ }
198
+ process.stdout.write("\nDry run — nothing was written.\n");
199
+ }
200
+ if (manualBlock !== undefined) {
201
+ process.stdout.write(
202
+ `\nCould not apply the block to: ${failedPaths.join(", ")}\n` +
203
+ "Append this block to the file yourself (replacing any existing gno:agents block):\n\n" +
204
+ `${manualBlock}\n`
205
+ );
206
+ }
207
+ }
208
+
209
+ if (failedPaths.length > 0) {
210
+ // Exit-code contract (spec/cli.md): 1 = validation (malformed markers,
211
+ // non-UTF-8), 2 = runtime (I/O). Any validation failure makes it 1.
212
+ throw new CliError(
213
+ validationErrors > 0 ? "VALIDATION" : "RUNTIME",
214
+ `${verb} failed for ${failedPaths.length} target(s); see per-target detail above. Nothing was written to the failing file(s).`
215
+ );
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Install or refresh the protocol block. Both verbs converge the block to
221
+ * the current release: install appends when absent, update replaces an
222
+ * older/stale block in place; a current block is a no-op for either.
223
+ */
224
+ export function installAgents(
225
+ opts: AgentsOptions = {},
226
+ verb: "install" | "update" = "install"
227
+ ): Promise<void> {
228
+ return runMutation(verb, "install", opts);
229
+ }
230
+
231
+ /** Remove the protocol block and its markers, leaving the rest untouched. */
232
+ export function uninstallAgents(opts: AgentsOptions = {}): Promise<void> {
233
+ return runMutation("uninstall", "uninstall", opts);
234
+ }
235
+
236
+ // ─────────────────────────────────────────────────────────────────────────────
237
+ // Verify
238
+ // ─────────────────────────────────────────────────────────────────────────────
239
+
240
+ type VerifyStatus =
241
+ | "ok"
242
+ | "outdated"
243
+ | "missing"
244
+ | "malformed"
245
+ | "error"
246
+ | "covered"
247
+ | "not-detected";
248
+
249
+ interface VerifyReport {
250
+ target: string;
251
+ label: string;
252
+ path: string;
253
+ status: VerifyStatus;
254
+ detected: boolean;
255
+ via?: string;
256
+ detail?: string;
257
+ blockVersion?: number;
258
+ hashOk?: boolean;
259
+ }
260
+
261
+ /** Inner text (stamp + body, no markers) of the current release's block. */
262
+ function expectedInner(): string {
263
+ return renderBlock().split("\n").slice(1, -1).join("\n");
264
+ }
265
+
266
+ async function verifyTarget(
267
+ target: ResolvedTarget,
268
+ runContext: {
269
+ resolvedIds: Set<string>;
270
+ requiredCovering: Set<string>;
271
+ /** Real-file identity → id of the target that owns verification. */
272
+ owners: Map<string, string>;
273
+ }
274
+ ): Promise<VerifyReport> {
275
+ const base = {
276
+ target: target.id,
277
+ label: target.label,
278
+ path: target.file,
279
+ detected: target.detected,
280
+ };
281
+
282
+ if (!(target.detected || runContext.requiredCovering.has(target.id))) {
283
+ return { ...base, status: "not-detected" };
284
+ }
285
+ if (target.coveredBy) {
286
+ if (!runContext.resolvedIds.has(target.coveredBy)) {
287
+ return {
288
+ ...base,
289
+ status: "missing",
290
+ detail: `covered via ${target.coveredBy}, but ${target.coveredBy} was not resolved in this run`,
291
+ };
292
+ }
293
+ return {
294
+ ...base,
295
+ status: "covered",
296
+ via: target.coveredBy,
297
+ detail: `covered via ${target.coveredBy}`,
298
+ };
299
+ }
300
+ const owner = runContext.owners.get(target.realFile);
301
+ if (owner) {
302
+ return {
303
+ ...base,
304
+ status: "covered",
305
+ via: owner,
306
+ detail: `covered via ${owner} (same file)`,
307
+ };
308
+ }
309
+ runContext.owners.set(target.realFile, target.id);
310
+
311
+ const file = Bun.file(target.file);
312
+ if (!(await file.exists())) {
313
+ return { ...base, status: "missing", detail: "instruction file not found" };
314
+ }
315
+ let bytes: Uint8Array;
316
+ try {
317
+ bytes = await file.bytes();
318
+ } catch (err) {
319
+ return {
320
+ ...base,
321
+ status: "error",
322
+ detail: `could not read instruction file: ${err instanceof Error ? err.message : String(err)}`,
323
+ };
324
+ }
325
+ let extraction: ReturnType<typeof extractBlock>;
326
+ try {
327
+ const { content } = decodeInstructionFile(bytes, target.file);
328
+ extraction = extractBlock(content, target.file);
329
+ } catch (err) {
330
+ return {
331
+ ...base,
332
+ status: "malformed",
333
+ detail: err instanceof Error ? err.message : String(err),
334
+ };
335
+ }
336
+ if (!extraction.found) {
337
+ return { ...base, status: "missing", detail: "no GNO agents block" };
338
+ }
339
+
340
+ const { block } = extraction;
341
+ const hashOk = stampAuthenticates(block);
342
+ const versionOk = block.stamp?.version === BLOCK_VERSION;
343
+ const isCurrent = versionOk && hashOk && block.inner === expectedInner();
344
+ const versioned = { blockVersion: block.stamp?.version, hashOk };
345
+ if (isCurrent) {
346
+ return { ...base, status: "ok", ...versioned };
347
+ }
348
+ const detail = !block.stamp
349
+ ? "block has no valid stamp line (missing or unparseable) — run `gno agents update`"
350
+ : !hashOk
351
+ ? "block content does not match its stamp hash (edited inside markers?) — run `gno agents update`"
352
+ : !versionOk
353
+ ? `block v${block.stamp?.version ?? "?"} does not match installed release v${BLOCK_VERSION} — run \`gno agents update\``
354
+ : "block content differs from the installed release — run `gno agents update`";
355
+ return { ...base, status: "outdated", ...versioned, detail };
356
+ }
357
+
358
+ export async function verifyAgents(opts: AgentsOptions = {}): Promise<void> {
359
+ const { json, quiet } = outputSettings(opts);
360
+ const targets = resolveTargets(opts.target ?? "all", {
361
+ homeDir: opts.homeDir,
362
+ extraDirs: opts.extraDirs,
363
+ });
364
+
365
+ const resolvedIds = new Set(targets.map((t) => t.id as string));
366
+ const requiredCovering = new Set<string>();
367
+ for (const t of targets) {
368
+ if (t.detected && t.coveredBy) {
369
+ requiredCovering.add(t.coveredBy);
370
+ }
371
+ }
372
+ const owners = new Map<string, string>();
373
+ const results: VerifyReport[] = [];
374
+ for (const target of targets) {
375
+ results.push(
376
+ await verifyTarget(target, { resolvedIds, requiredCovering, owners })
377
+ );
378
+ }
379
+
380
+ const failing = results.filter(
381
+ (r) => !["ok", "covered", "not-detected"].includes(r.status)
382
+ );
383
+ const ok = failing.length === 0;
384
+
385
+ if (json) {
386
+ process.stdout.write(
387
+ `${JSON.stringify(
388
+ { command: "verify", blockVersion: BLOCK_VERSION, ok, results },
389
+ null,
390
+ 2
391
+ )}\n`
392
+ );
393
+ } else if (!quiet) {
394
+ for (const r of results) {
395
+ const detail = r.detail ? ` — ${r.detail}` : "";
396
+ process.stdout.write(
397
+ `${r.status.padEnd(13)} ${r.target.padEnd(10)} ${r.path}${detail}\n`
398
+ );
399
+ }
400
+ }
401
+
402
+ if (!ok) {
403
+ // 2 when the only failures were unreadable files (could not check),
404
+ // 1 for any content verdict (outdated / missing / malformed).
405
+ const allRuntime = failing.every((r) => r.status === "error");
406
+ throw new CliError(
407
+ allRuntime ? "RUNTIME" : "VALIDATION",
408
+ `verification failed for ${failing.length} target(s): ${failing
409
+ .map((r) => `${r.target} (${r.status})`)
410
+ .join(", ")}`
411
+ );
412
+ }
413
+ }