@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
@@ -21,6 +21,14 @@ import {
21
21
  import { INDEX_NAME_REQUIREMENTS, isValidIndexName } from "../app/index-name";
22
22
  import { resolveDepthPolicy } from "../core/depth-policy";
23
23
  import { parseAndValidateTagFilter } from "../core/tags";
24
+ import {
25
+ formatWriteLeaseBusyJson,
26
+ formatWriteLeaseBusyMessage,
27
+ isWriteLeaseBusyResult,
28
+ parseLockWaitMs,
29
+ withCliWriteLease,
30
+ type WriteLeaseBusyFailure,
31
+ } from "../core/write-lease";
24
32
  import { setColorsEnabled } from "./colors";
25
33
  import {
26
34
  applyGlobalOptions,
@@ -221,6 +229,49 @@ function parseCsvValues(raw: unknown): string[] | undefined {
221
229
  return values.length > 0 ? values : undefined;
222
230
  }
223
231
 
232
+ function addWriteLeaseFlags(command: Command): Command {
233
+ return command
234
+ .option(
235
+ "--lock-wait <duration>",
236
+ "how long to wait for the index write lease (default: 120s)",
237
+ "120s"
238
+ )
239
+ .option("--no-wait", "fail immediately if the index write lease is held");
240
+ }
241
+
242
+ function parseWriteLeaseFlags(cmdOpts: Record<string, unknown>): {
243
+ lockWaitMs: number;
244
+ noWait: boolean;
245
+ } {
246
+ const parsed = parseLockWaitMs(cmdOpts.lockWait);
247
+ if (parsed === null) {
248
+ throw new CliError(
249
+ "VALIDATION",
250
+ `Invalid --lock-wait duration: ${String(cmdOpts.lockWait)}. Use seconds ("120"), "120s", or "2m".`
251
+ );
252
+ }
253
+ return {
254
+ lockWaitMs: parsed,
255
+ noWait: cmdOpts.wait === false,
256
+ };
257
+ }
258
+
259
+ function throwIfWriteLeaseBusy<
260
+ T extends { success: boolean; error?: string; contention?: unknown },
261
+ >(result: T | WriteLeaseBusyFailure, json: boolean): asserts result is T {
262
+ if (!isWriteLeaseBusyResult(result)) {
263
+ return;
264
+ }
265
+ if (json) {
266
+ process.stdout.write(
267
+ `${JSON.stringify(formatWriteLeaseBusyJson(result.contention))}\n`
268
+ );
269
+ } else {
270
+ process.stderr.write(`${formatWriteLeaseBusyMessage(result.contention)}\n`);
271
+ }
272
+ throw new CliError("BUSY", result.error, { silent: true });
273
+ }
274
+
224
275
  function parseContextInteger(
225
276
  name: string,
226
277
  value: unknown,
@@ -313,6 +364,7 @@ export function createProgram(): Command {
313
364
  wireKnowledgeDeltaCommands(program);
314
365
  wireMcpCommand(program);
315
366
  wireSkillCommands(program);
367
+ wireAgentsCommands(program);
316
368
  wireDaemonCommand(program);
317
369
  wireServeCommand(program);
318
370
  wireCompletionCommand(program);
@@ -1505,39 +1557,51 @@ function wireOnboardingCommands(program: Command): void {
1505
1557
  });
1506
1558
 
1507
1559
  // index - Index collections
1508
- program
1509
- .command("index [collection]")
1510
- .description("Index files from collections")
1511
- .option("--no-embed", "skip embedding after sync")
1512
- .option("--git-pull", "run git pull in git repositories")
1513
- .option("--models-pull", "download models if missing")
1514
- .option("--json", "JSON output")
1515
- .action(
1516
- async (
1517
- collection: string | undefined,
1518
- cmdOpts: Record<string, unknown>
1519
- ) => {
1520
- const globals = getGlobals();
1521
- const { index, formatIndex } = await import("./commands/index-cmd");
1522
- const opts = {
1523
- configPath: globals.config,
1524
- indexName: globals.index,
1525
- collection,
1526
- noEmbed: cmdOpts.embed === false,
1527
- gitPull: Boolean(cmdOpts.gitPull),
1528
- modelsPull: Boolean(cmdOpts.modelsPull),
1529
- yes: globals.yes,
1530
- verbose: globals.verbose,
1531
- json: getFormat(cmdOpts) === "json",
1532
- };
1533
- const result = await index(opts);
1560
+ addWriteLeaseFlags(
1561
+ program
1562
+ .command("index [collection]")
1563
+ .description("Index files from collections")
1564
+ .option("--no-embed", "skip embedding after sync")
1565
+ .option("--git-pull", "run git pull in git repositories")
1566
+ .option("--models-pull", "download models if missing")
1567
+ .option("--json", "JSON output")
1568
+ ).action(
1569
+ async (
1570
+ collection: string | undefined,
1571
+ cmdOpts: Record<string, unknown>
1572
+ ) => {
1573
+ const globals = getGlobals();
1574
+ const { index, formatIndex } = await import("./commands/index-cmd");
1575
+ const lease = parseWriteLeaseFlags(cmdOpts);
1576
+ const opts = {
1577
+ configPath: globals.config,
1578
+ indexName: globals.index,
1579
+ collection,
1580
+ noEmbed: cmdOpts.embed === false,
1581
+ gitPull: Boolean(cmdOpts.gitPull),
1582
+ modelsPull: Boolean(cmdOpts.modelsPull),
1583
+ yes: globals.yes,
1584
+ verbose: globals.verbose,
1585
+ json: getFormat(cmdOpts) === "json",
1586
+ lockWaitMs: lease.lockWaitMs,
1587
+ noWait: lease.noWait,
1588
+ };
1589
+ const result = await index(opts);
1534
1590
 
1535
- if (!result.success) {
1536
- throw new CliError("RUNTIME", result.error ?? "Index failed");
1537
- }
1538
- process.stdout.write(`${formatIndex(result, opts)}\n`);
1591
+ if (!result.success) {
1592
+ throwIfWriteLeaseBusy(result, opts.json);
1593
+ throw new CliError("RUNTIME", result.error ?? "Index failed");
1539
1594
  }
1540
- );
1595
+ process.stdout.write(`${formatIndex(result, opts)}\n`);
1596
+ if ((result.embedResult?.contentionErrors ?? 0) > 0) {
1597
+ throw new CliError(
1598
+ "BUSY",
1599
+ "Some chunks were deferred by index contention",
1600
+ { silent: true }
1601
+ );
1602
+ }
1603
+ }
1604
+ );
1541
1605
 
1542
1606
  // audit - Read-only workspace integrity report
1543
1607
  program
@@ -2225,19 +2289,33 @@ function wireManagementCommands(program: Command): void {
2225
2289
  });
2226
2290
  });
2227
2291
 
2228
- collectionCmd
2229
- .command("clear-embeddings <name>")
2230
- .description("Clear stale or all embeddings for a collection")
2231
- .option("--all", "remove all embeddings for the collection")
2232
- .option("--json", "JSON output")
2233
- .action(async (name: string, cmdOpts: Record<string, unknown>) => {
2234
- const { collectionClearEmbeddings } =
2235
- await import("./commands/collection");
2236
- await collectionClearEmbeddings(name, {
2237
- all: Boolean(cmdOpts.all),
2238
- json: Boolean(cmdOpts.json),
2239
- });
2240
- });
2292
+ addWriteLeaseFlags(
2293
+ collectionCmd
2294
+ .command("clear-embeddings <name>")
2295
+ .description("Clear stale or all embeddings for a collection")
2296
+ .option("--all", "remove all embeddings for the collection")
2297
+ .option("--json", "JSON output")
2298
+ ).action(async (name: string, cmdOpts: Record<string, unknown>) => {
2299
+ const { collectionClearEmbeddings } = await import("./commands/collection");
2300
+ const json = Boolean(cmdOpts.json);
2301
+ const lease = parseWriteLeaseFlags(cmdOpts);
2302
+ const result = await withCliWriteLease(
2303
+ {
2304
+ indexName: getGlobals().index,
2305
+ lockWaitMs: lease.lockWaitMs,
2306
+ noWait: lease.noWait,
2307
+ },
2308
+ async () => {
2309
+ await collectionClearEmbeddings(name, {
2310
+ all: Boolean(cmdOpts.all),
2311
+ indexName: getGlobals().index,
2312
+ json,
2313
+ });
2314
+ return { success: true as const };
2315
+ }
2316
+ );
2317
+ throwIfWriteLeaseBusy(result, json);
2318
+ });
2241
2319
 
2242
2320
  const collectionPolicyCmd = collectionCmd
2243
2321
  .command("policy")
@@ -2771,85 +2849,110 @@ function wireManagementCommands(program: Command): void {
2771
2849
  });
2772
2850
 
2773
2851
  // update - Sync files from disk
2774
- program
2775
- .command("update")
2776
- .description("Sync files from disk into the index")
2777
- .option("--git-pull", "run git pull in git repositories")
2778
- .option("--json", "JSON output")
2779
- .action(async (cmdOpts: Record<string, unknown>) => {
2852
+ addWriteLeaseFlags(
2853
+ program
2854
+ .command("update")
2855
+ .description("Sync files from disk into the index")
2856
+ .option("--git-pull", "run git pull in git repositories")
2857
+ .option("--json", "JSON output")
2858
+ ).action(async (cmdOpts: Record<string, unknown>) => {
2859
+ const globals = getGlobals();
2860
+ const { update, formatUpdate } = await import("./commands/update");
2861
+ const lease = parseWriteLeaseFlags(cmdOpts);
2862
+ const opts = {
2863
+ configPath: globals.config,
2864
+ indexName: globals.index,
2865
+ gitPull: Boolean(cmdOpts.gitPull),
2866
+ verbose: globals.verbose,
2867
+ json: getFormat(cmdOpts) === "json",
2868
+ lockWaitMs: lease.lockWaitMs,
2869
+ noWait: lease.noWait,
2870
+ };
2871
+ const result = await update(opts);
2872
+
2873
+ if (!result.success) {
2874
+ throwIfWriteLeaseBusy(result, opts.json);
2875
+ throw new CliError("RUNTIME", result.error ?? "Update failed");
2876
+ }
2877
+ process.stdout.write(`${formatUpdate(result, opts)}\n`);
2878
+ });
2879
+
2880
+ // embed - Generate embeddings
2881
+ addWriteLeaseFlags(
2882
+ program
2883
+ .command("embed [collection]")
2884
+ .description("Generate embeddings for indexed documents")
2885
+ .option("--collection <name>", "restrict to one collection")
2886
+ .option("--model <uri>", "embedding model URI")
2887
+ .option("--batch-size <num>", "batch size", "32")
2888
+ .option("--force", "regenerate all embeddings")
2889
+ .option("--dry-run", "show what would be done")
2890
+ .option("--json", "JSON output")
2891
+ ).action(
2892
+ async (
2893
+ collectionArg: string | undefined,
2894
+ cmdOpts: Record<string, unknown>
2895
+ ) => {
2780
2896
  const globals = getGlobals();
2781
- const { update, formatUpdate } = await import("./commands/update");
2897
+ const format = getFormat(cmdOpts);
2898
+
2899
+ const { embed, formatEmbed } = await import("./commands/embed");
2900
+ const collection =
2901
+ collectionArg ?? (cmdOpts.collection as string | undefined);
2902
+ const lease = parseWriteLeaseFlags(cmdOpts);
2782
2903
  const opts = {
2783
2904
  configPath: globals.config,
2784
2905
  indexName: globals.index,
2785
- gitPull: Boolean(cmdOpts.gitPull),
2906
+ collection,
2907
+ model: cmdOpts.model as string | undefined,
2908
+ batchSize: parsePositiveInt("batch-size", cmdOpts.batchSize),
2909
+ force: Boolean(cmdOpts.force),
2910
+ dryRun: Boolean(cmdOpts.dryRun),
2911
+ yes: globals.yes,
2912
+ json: format === "json",
2786
2913
  verbose: globals.verbose,
2787
- json: getFormat(cmdOpts) === "json",
2914
+ offline: globals.offline,
2915
+ lockWaitMs: lease.lockWaitMs,
2916
+ noWait: lease.noWait,
2788
2917
  };
2789
- const result = await update(opts);
2918
+ const result = await embed(opts);
2790
2919
 
2791
2920
  if (!result.success) {
2792
- throw new CliError("RUNTIME", result.error ?? "Update failed");
2921
+ throwIfWriteLeaseBusy(result, opts.json);
2922
+ throw new CliError("RUNTIME", result.error ?? "Embed failed");
2793
2923
  }
2794
- process.stdout.write(`${formatUpdate(result, opts)}\n`);
2795
- });
2796
-
2797
- // embed - Generate embeddings
2798
- program
2799
- .command("embed [collection]")
2800
- .description("Generate embeddings for indexed documents")
2801
- .option("--collection <name>", "restrict to one collection")
2802
- .option("--model <uri>", "embedding model URI")
2803
- .option("--batch-size <num>", "batch size", "32")
2804
- .option("--force", "regenerate all embeddings")
2805
- .option("--dry-run", "show what would be done")
2806
- .option("--json", "JSON output")
2807
- .action(
2808
- async (
2809
- collectionArg: string | undefined,
2810
- cmdOpts: Record<string, unknown>
2811
- ) => {
2812
- const globals = getGlobals();
2813
- const format = getFormat(cmdOpts);
2814
-
2815
- const { embed, formatEmbed } = await import("./commands/embed");
2816
- const collection =
2817
- collectionArg ?? (cmdOpts.collection as string | undefined);
2818
- const opts = {
2819
- configPath: globals.config,
2820
- indexName: globals.index,
2821
- collection,
2822
- model: cmdOpts.model as string | undefined,
2823
- batchSize: parsePositiveInt("batch-size", cmdOpts.batchSize),
2824
- force: Boolean(cmdOpts.force),
2825
- dryRun: Boolean(cmdOpts.dryRun),
2826
- yes: globals.yes,
2827
- json: format === "json",
2828
- verbose: globals.verbose,
2829
- offline: globals.offline,
2830
- };
2831
- const result = await embed(opts);
2832
-
2833
- if (!result.success) {
2834
- throw new CliError("RUNTIME", result.error ?? "Embed failed");
2835
- }
2836
- process.stdout.write(`${formatEmbed(result, opts)}\n`);
2924
+ process.stdout.write(`${formatEmbed(result, opts)}\n`);
2925
+ if (result.contentionErrors > 0) {
2926
+ throw new CliError(
2927
+ "BUSY",
2928
+ "Some chunks were deferred by index contention",
2929
+ { silent: true }
2930
+ );
2837
2931
  }
2838
- );
2932
+ }
2933
+ );
2839
2934
 
2840
2935
  // cleanup - Clean stale data
2841
- program
2842
- .command("cleanup")
2843
- .description("Clean orphaned data from index")
2844
- .action(async () => {
2845
- const { cleanup, formatCleanup } = await import("./commands/cleanup");
2846
- const result = await cleanup();
2936
+ addWriteLeaseFlags(
2937
+ program.command("cleanup").description("Clean orphaned data from index")
2938
+ ).action(async (cmdOpts: Record<string, unknown>) => {
2939
+ const { cleanup, formatCleanup } = await import("./commands/cleanup");
2940
+ const lease = parseWriteLeaseFlags(cmdOpts);
2941
+ const result = await withCliWriteLease(
2942
+ {
2943
+ indexName: getGlobals().index,
2944
+ lockWaitMs: lease.lockWaitMs,
2945
+ noWait: lease.noWait,
2946
+ },
2947
+ () => cleanup({ indexName: getGlobals().index })
2948
+ );
2949
+ throwIfWriteLeaseBusy(result, false);
2847
2950
 
2848
- if (!result.success) {
2849
- throw new CliError("RUNTIME", result.error ?? "Cleanup failed");
2850
- }
2851
- process.stdout.write(`${formatCleanup(result)}\n`);
2852
- });
2951
+ if (!result.success) {
2952
+ throw new CliError("RUNTIME", result.error ?? "Cleanup failed");
2953
+ }
2954
+ process.stdout.write(`${formatCleanup(result)}\n`);
2955
+ });
2853
2956
 
2854
2957
  // reset - Reset GNO to fresh state
2855
2958
  program
@@ -2879,52 +2982,76 @@ function wireVecCommands(program: Command): void {
2879
2982
  const vecCmd = program.command("vec").description("Vector index maintenance");
2880
2983
 
2881
2984
  // vec sync
2882
- vecCmd
2883
- .command("sync")
2884
- .description("Sync vec0 index with content_vectors")
2885
- .option("--json", "JSON output")
2886
- .action(async (cmdOpts: Record<string, unknown>) => {
2887
- const format = getFormat(cmdOpts);
2888
- const globals = getGlobals();
2985
+ addWriteLeaseFlags(
2986
+ vecCmd
2987
+ .command("sync")
2988
+ .description("Sync vec0 index with content_vectors")
2989
+ .option("--json", "JSON output")
2990
+ ).action(async (cmdOpts: Record<string, unknown>) => {
2991
+ const format = getFormat(cmdOpts);
2992
+ const globals = getGlobals();
2889
2993
 
2890
- const { vecSync, formatVecSync } = await import("./commands/vec");
2891
- const result = await vecSync({
2892
- configPath: globals.config,
2893
- json: format === "json",
2894
- });
2994
+ const { vecSync, formatVecSync } = await import("./commands/vec");
2995
+ const lease = parseWriteLeaseFlags(cmdOpts);
2996
+ const result = await withCliWriteLease(
2997
+ {
2998
+ indexName: globals.index,
2999
+ lockWaitMs: lease.lockWaitMs,
3000
+ noWait: lease.noWait,
3001
+ },
3002
+ () =>
3003
+ vecSync({
3004
+ configPath: globals.config,
3005
+ indexName: globals.index,
3006
+ json: format === "json",
3007
+ })
3008
+ );
3009
+ throwIfWriteLeaseBusy(result, format === "json");
2895
3010
 
2896
- if (!result.success) {
2897
- throw new CliError("RUNTIME", result.error);
2898
- }
3011
+ if (!result.success) {
3012
+ throw new CliError("RUNTIME", result.error);
3013
+ }
2899
3014
 
2900
- process.stdout.write(
2901
- `${formatVecSync(result, { json: format === "json" })}\n`
2902
- );
2903
- });
3015
+ process.stdout.write(
3016
+ `${formatVecSync(result, { json: format === "json" })}\n`
3017
+ );
3018
+ });
2904
3019
 
2905
3020
  // vec rebuild
2906
- vecCmd
2907
- .command("rebuild")
2908
- .description("Rebuild vec0 index from content_vectors")
2909
- .option("--json", "JSON output")
2910
- .action(async (cmdOpts: Record<string, unknown>) => {
2911
- const format = getFormat(cmdOpts);
2912
- const globals = getGlobals();
3021
+ addWriteLeaseFlags(
3022
+ vecCmd
3023
+ .command("rebuild")
3024
+ .description("Rebuild vec0 index from content_vectors")
3025
+ .option("--json", "JSON output")
3026
+ ).action(async (cmdOpts: Record<string, unknown>) => {
3027
+ const format = getFormat(cmdOpts);
3028
+ const globals = getGlobals();
2913
3029
 
2914
- const { vecRebuild, formatVecRebuild } = await import("./commands/vec");
2915
- const result = await vecRebuild({
2916
- configPath: globals.config,
2917
- json: format === "json",
2918
- });
3030
+ const { vecRebuild, formatVecRebuild } = await import("./commands/vec");
3031
+ const lease = parseWriteLeaseFlags(cmdOpts);
3032
+ const result = await withCliWriteLease(
3033
+ {
3034
+ indexName: globals.index,
3035
+ lockWaitMs: lease.lockWaitMs,
3036
+ noWait: lease.noWait,
3037
+ },
3038
+ () =>
3039
+ vecRebuild({
3040
+ configPath: globals.config,
3041
+ indexName: globals.index,
3042
+ json: format === "json",
3043
+ })
3044
+ );
3045
+ throwIfWriteLeaseBusy(result, format === "json");
2919
3046
 
2920
- if (!result.success) {
2921
- throw new CliError("RUNTIME", result.error);
2922
- }
3047
+ if (!result.success) {
3048
+ throw new CliError("RUNTIME", result.error);
3049
+ }
2923
3050
 
2924
- process.stdout.write(
2925
- `${formatVecRebuild(result, { json: format === "json" })}\n`
2926
- );
2927
- });
3051
+ process.stdout.write(
3052
+ `${formatVecRebuild(result, { json: format === "json" })}\n`
3053
+ );
3054
+ });
2928
3055
  }
2929
3056
 
2930
3057
  function wirePublishCommand(program: Command): void {
@@ -3194,6 +3321,109 @@ function wireSkillCommands(program: Command): void {
3194
3321
  });
3195
3322
  }
3196
3323
 
3324
+ // ─────────────────────────────────────────────────────────────────────────────
3325
+ // Agents Commands (install, update, verify, uninstall)
3326
+ // ─────────────────────────────────────────────────────────────────────────────
3327
+
3328
+ function wireAgentsCommands(program: Command): void {
3329
+ const agentsCmd = program
3330
+ .command("agents")
3331
+ .description(
3332
+ "Manage the GNO protocol block in global harness instruction files"
3333
+ );
3334
+
3335
+ const targetDescription =
3336
+ "target harness (claude, codex, cursor, opencode, grok, hermes, openclaw, all)";
3337
+
3338
+ const collectExtraDir = (value: string, previous: string[]): string[] => [
3339
+ ...previous,
3340
+ value,
3341
+ ];
3342
+
3343
+ const parseCommonOpts = async (cmdOpts: Record<string, unknown>) => {
3344
+ const { parseTargetOption } = await import("./commands/agents/commands.js");
3345
+ return {
3346
+ target: parseTargetOption((cmdOpts.target as string) ?? "all"),
3347
+ extraDirs: (cmdOpts.extraDir as string[] | undefined) ?? [],
3348
+ dryRun: Boolean(cmdOpts.dryRun),
3349
+ // The root program also defines a global --json and may consume the
3350
+ // flag; leave undefined here so the runner falls back to globals.
3351
+ json: cmdOpts.json ? true : undefined,
3352
+ };
3353
+ };
3354
+
3355
+ agentsCmd
3356
+ .command("install")
3357
+ .description(
3358
+ "Install the GNO protocol block into detected harnesses' global instruction files"
3359
+ )
3360
+ .option("-t, --target <target>", targetDescription, "all")
3361
+ .option(
3362
+ "--extra-dir <path>",
3363
+ "additional instruction dir (repeatable, for nonstandard layouts)",
3364
+ collectExtraDir,
3365
+ [] as string[]
3366
+ )
3367
+ .option("--dry-run", "print the unified diff and write nothing")
3368
+ .option("--json", "JSON output")
3369
+ .action(async (cmdOpts: Record<string, unknown>) => {
3370
+ const { installAgents } = await import("./commands/agents/commands.js");
3371
+ await installAgents(await parseCommonOpts(cmdOpts), "install");
3372
+ });
3373
+
3374
+ agentsCmd
3375
+ .command("update")
3376
+ .description("Refresh an installed GNO protocol block in place")
3377
+ .option("-t, --target <target>", targetDescription, "all")
3378
+ .option(
3379
+ "--extra-dir <path>",
3380
+ "additional instruction dir (repeatable, for nonstandard layouts)",
3381
+ collectExtraDir,
3382
+ [] as string[]
3383
+ )
3384
+ .option("--dry-run", "print the unified diff and write nothing")
3385
+ .option("--json", "JSON output")
3386
+ .action(async (cmdOpts: Record<string, unknown>) => {
3387
+ const { installAgents } = await import("./commands/agents/commands.js");
3388
+ await installAgents(await parseCommonOpts(cmdOpts), "update");
3389
+ });
3390
+
3391
+ agentsCmd
3392
+ .command("verify")
3393
+ .description(
3394
+ "Verify installed GNO protocol blocks (one block, version + hash)"
3395
+ )
3396
+ .option("-t, --target <target>", targetDescription, "all")
3397
+ .option(
3398
+ "--extra-dir <path>",
3399
+ "additional instruction dir (repeatable, for nonstandard layouts)",
3400
+ collectExtraDir,
3401
+ [] as string[]
3402
+ )
3403
+ .option("--json", "JSON output")
3404
+ .action(async (cmdOpts: Record<string, unknown>) => {
3405
+ const { verifyAgents } = await import("./commands/agents/commands.js");
3406
+ await verifyAgents(await parseCommonOpts(cmdOpts));
3407
+ });
3408
+
3409
+ agentsCmd
3410
+ .command("uninstall")
3411
+ .description("Remove the GNO protocol block and markers from harness files")
3412
+ .option("-t, --target <target>", targetDescription, "all")
3413
+ .option(
3414
+ "--extra-dir <path>",
3415
+ "additional instruction dir (repeatable, for nonstandard layouts)",
3416
+ collectExtraDir,
3417
+ [] as string[]
3418
+ )
3419
+ .option("--dry-run", "print the unified diff and write nothing")
3420
+ .option("--json", "JSON output")
3421
+ .action(async (cmdOpts: Record<string, unknown>) => {
3422
+ const { uninstallAgents } = await import("./commands/agents/commands.js");
3423
+ await uninstallAgents(await parseCommonOpts(cmdOpts));
3424
+ });
3425
+ }
3426
+
3197
3427
  // ─────────────────────────────────────────────────────────────────────────────
3198
3428
  // Tags Commands (list, add, rm)
3199
3429
  // ─────────────────────────────────────────────────────────────────────────────
@@ -3217,6 +3447,7 @@ function wireTagsCommands(program: Command): void {
3217
3447
  const { tagsList, formatTagsList } = await import("./commands/tags");
3218
3448
  const result = await tagsList({
3219
3449
  configPath: globals.config,
3450
+ indexName: globals.index,
3220
3451
  collection: cmdOpts.collection as string | undefined,
3221
3452
  prefix: cmdOpts.prefix as string | undefined,
3222
3453
  json: format === "json",
@@ -3238,62 +3469,86 @@ function wireTagsCommands(program: Command): void {
3238
3469
  });
3239
3470
 
3240
3471
  // tags add <doc> <tag>
3241
- tagsCmd
3242
- .command("add <doc> <tag>")
3243
- .description("Add a tag to a document")
3244
- .option("--json", "JSON output")
3245
- .action(
3246
- async (doc: string, tag: string, cmdOpts: Record<string, unknown>) => {
3247
- const format = getFormat(cmdOpts);
3248
- const globals = getGlobals();
3249
-
3250
- const { tagsAdd, formatTagsAdd } = await import("./commands/tags");
3251
- const result = await tagsAdd(doc, tag, {
3252
- configPath: globals.config,
3253
- json: format === "json",
3254
- });
3472
+ addWriteLeaseFlags(
3473
+ tagsCmd
3474
+ .command("add <doc> <tag>")
3475
+ .description("Add a tag to a document")
3476
+ .option("--json", "JSON output")
3477
+ ).action(
3478
+ async (doc: string, tag: string, cmdOpts: Record<string, unknown>) => {
3479
+ const format = getFormat(cmdOpts);
3480
+ const globals = getGlobals();
3255
3481
 
3256
- if (!result.success) {
3257
- throw new CliError(
3258
- result.isValidation ? "VALIDATION" : "RUNTIME",
3259
- result.error
3260
- );
3261
- }
3482
+ const { tagsAdd, formatTagsAdd } = await import("./commands/tags");
3483
+ const lease = parseWriteLeaseFlags(cmdOpts);
3484
+ const result = await withCliWriteLease(
3485
+ {
3486
+ indexName: globals.index,
3487
+ lockWaitMs: lease.lockWaitMs,
3488
+ noWait: lease.noWait,
3489
+ },
3490
+ () =>
3491
+ tagsAdd(doc, tag, {
3492
+ configPath: globals.config,
3493
+ indexName: globals.index,
3494
+ json: format === "json",
3495
+ })
3496
+ );
3497
+ throwIfWriteLeaseBusy(result, format === "json");
3262
3498
 
3263
- process.stdout.write(
3264
- `${formatTagsAdd(result, { json: format === "json" })}\n`
3499
+ if (!result.success) {
3500
+ throw new CliError(
3501
+ result.isValidation ? "VALIDATION" : "RUNTIME",
3502
+ result.error
3265
3503
  );
3266
3504
  }
3267
- );
3268
3505
 
3269
- // tags rm <doc> <tag>
3270
- tagsCmd
3271
- .command("rm <doc> <tag>")
3272
- .description("Remove a tag from a document")
3273
- .option("--json", "JSON output")
3274
- .action(
3275
- async (doc: string, tag: string, cmdOpts: Record<string, unknown>) => {
3276
- const format = getFormat(cmdOpts);
3277
- const globals = getGlobals();
3506
+ process.stdout.write(
3507
+ `${formatTagsAdd(result, { json: format === "json" })}\n`
3508
+ );
3509
+ }
3510
+ );
3278
3511
 
3279
- const { tagsRm, formatTagsRm } = await import("./commands/tags");
3280
- const result = await tagsRm(doc, tag, {
3281
- configPath: globals.config,
3282
- json: format === "json",
3283
- });
3512
+ // tags rm <doc> <tag>
3513
+ addWriteLeaseFlags(
3514
+ tagsCmd
3515
+ .command("rm <doc> <tag>")
3516
+ .description("Remove a tag from a document")
3517
+ .option("--json", "JSON output")
3518
+ ).action(
3519
+ async (doc: string, tag: string, cmdOpts: Record<string, unknown>) => {
3520
+ const format = getFormat(cmdOpts);
3521
+ const globals = getGlobals();
3284
3522
 
3285
- if (!result.success) {
3286
- throw new CliError(
3287
- result.isValidation ? "VALIDATION" : "RUNTIME",
3288
- result.error
3289
- );
3290
- }
3523
+ const { tagsRm, formatTagsRm } = await import("./commands/tags");
3524
+ const lease = parseWriteLeaseFlags(cmdOpts);
3525
+ const result = await withCliWriteLease(
3526
+ {
3527
+ indexName: globals.index,
3528
+ lockWaitMs: lease.lockWaitMs,
3529
+ noWait: lease.noWait,
3530
+ },
3531
+ () =>
3532
+ tagsRm(doc, tag, {
3533
+ configPath: globals.config,
3534
+ indexName: globals.index,
3535
+ json: format === "json",
3536
+ })
3537
+ );
3538
+ throwIfWriteLeaseBusy(result, format === "json");
3291
3539
 
3292
- process.stdout.write(
3293
- `${formatTagsRm(result, { json: format === "json" })}\n`
3540
+ if (!result.success) {
3541
+ throw new CliError(
3542
+ result.isValidation ? "VALIDATION" : "RUNTIME",
3543
+ result.error
3294
3544
  );
3295
3545
  }
3296
- );
3546
+
3547
+ process.stdout.write(
3548
+ `${formatTagsRm(result, { json: format === "json" })}\n`
3549
+ );
3550
+ }
3551
+ );
3297
3552
  }
3298
3553
 
3299
3554
  // ─────────────────────────────────────────────────────────────────────────────