run402 4.68.2 → 4.69.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 (138) hide show
  1. package/cli.mjs +2 -2
  2. package/core-dist/allowance-auth.js +1 -1
  3. package/core-dist/allowance.js +1 -1
  4. package/core-dist/config.js +4 -4
  5. package/core-dist/keystore.js +7 -7
  6. package/core-dist/profiles.js +1 -1
  7. package/core-dist/redact.js +6 -7
  8. package/git-remote-run402.mjs +2 -2
  9. package/gitvault-surface.json +2 -1
  10. package/lib/agent.mjs +1 -1
  11. package/lib/allowance.mjs +3 -4
  12. package/lib/argparse.mjs +7 -9
  13. package/lib/billing.mjs +33 -35
  14. package/lib/buzz-notifications.mjs +3 -2
  15. package/lib/buzz.mjs +14 -8
  16. package/lib/buzz.test.mjs +1 -1
  17. package/lib/command-manifest.mjs +38 -20
  18. package/lib/config.mjs +1 -1
  19. package/lib/deploy-v2.mjs +3 -3
  20. package/lib/doctor-source-scan.mjs +1 -1
  21. package/lib/doctor.mjs +42 -25
  22. package/lib/email.mjs +2 -3
  23. package/lib/harness-context.mjs +1 -2
  24. package/lib/init.mjs +1 -1
  25. package/lib/next-actions.mjs +1 -1
  26. package/lib/org-context.mjs +83 -2
  27. package/lib/org.mjs +231 -114
  28. package/lib/projects.mjs +2 -2
  29. package/lib/remote-helper-session.mjs +26 -32
  30. package/lib/repos.mjs +117 -15
  31. package/lib/rooms-context.mjs +5 -5
  32. package/lib/sdk-errors.mjs +1 -2
  33. package/lib/status.mjs +1 -1
  34. package/lib/update-check.test.mjs +1 -1
  35. package/lib/wallet-context.mjs +10 -10
  36. package/lib/wallet-context.test.mjs +6 -7
  37. package/lib/wallets.mjs +2 -2
  38. package/lib/webhooks.mjs +2 -2
  39. package/package.json +1 -1
  40. package/sdk/core-dist/allowance-auth.js +1 -1
  41. package/sdk/core-dist/allowance.js +1 -1
  42. package/sdk/core-dist/config.js +4 -4
  43. package/sdk/core-dist/keystore.js +7 -7
  44. package/sdk/core-dist/profiles.js +1 -1
  45. package/sdk/core-dist/redact.js +6 -7
  46. package/sdk/dist/delegate-credentials.d.ts +1 -1
  47. package/sdk/dist/delegate-credentials.js +1 -1
  48. package/sdk/dist/errors.d.ts +10 -4
  49. package/sdk/dist/errors.d.ts.map +1 -1
  50. package/sdk/dist/errors.js +2 -3
  51. package/sdk/dist/errors.js.map +1 -1
  52. package/sdk/dist/kernel.js +1 -1
  53. package/sdk/dist/kernel.js.map +1 -1
  54. package/sdk/dist/namespaces/allowance.d.ts +1 -1
  55. package/sdk/dist/namespaces/allowance.js +1 -1
  56. package/sdk/dist/namespaces/allowance.js.map +1 -1
  57. package/sdk/dist/namespaces/assets.js +1 -1
  58. package/sdk/dist/namespaces/assets.js.map +1 -1
  59. package/sdk/dist/namespaces/assets.types.d.ts +1 -1
  60. package/sdk/dist/namespaces/ci.d.ts.map +1 -1
  61. package/sdk/dist/namespaces/ci.js +4 -6
  62. package/sdk/dist/namespaces/ci.js.map +1 -1
  63. package/sdk/dist/namespaces/deploy.js +7 -8
  64. package/sdk/dist/namespaces/deploy.js.map +1 -1
  65. package/sdk/dist/namespaces/gitvault.crypto.d.ts +86 -3
  66. package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -1
  67. package/sdk/dist/namespaces/gitvault.crypto.js +102 -3
  68. package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -1
  69. package/sdk/dist/namespaces/gitvault.d.ts +197 -17
  70. package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
  71. package/sdk/dist/namespaces/gitvault.js +698 -58
  72. package/sdk/dist/namespaces/gitvault.js.map +1 -1
  73. package/sdk/dist/namespaces/gitvault.types.d.ts +91 -3
  74. package/sdk/dist/namespaces/gitvault.types.d.ts.map +1 -1
  75. package/sdk/dist/namespaces/gitvault.types.js.map +1 -1
  76. package/sdk/dist/namespaces/operator.d.ts +1 -1
  77. package/sdk/dist/namespaces/operator.js +1 -1
  78. package/sdk/dist/namespaces/org.d.ts +14 -1
  79. package/sdk/dist/namespaces/org.d.ts.map +1 -1
  80. package/sdk/dist/namespaces/org.js +15 -0
  81. package/sdk/dist/namespaces/org.js.map +1 -1
  82. package/sdk/dist/namespaces/org.types.d.ts +30 -0
  83. package/sdk/dist/namespaces/org.types.d.ts.map +1 -1
  84. package/sdk/dist/namespaces/projects.types.d.ts +1 -1
  85. package/sdk/dist/namespaces/subdomains.js +1 -1
  86. package/sdk/dist/namespaces/subdomains.js.map +1 -1
  87. package/sdk/dist/namespaces/transfers.d.ts +3 -4
  88. package/sdk/dist/namespaces/transfers.d.ts.map +1 -1
  89. package/sdk/dist/namespaces/transfers.js +1 -2
  90. package/sdk/dist/namespaces/transfers.js.map +1 -1
  91. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  92. package/sdk/dist/node/actions-node.js +3 -4
  93. package/sdk/dist/node/actions-node.js.map +1 -1
  94. package/sdk/dist/node/deploy-manifest.js +2 -2
  95. package/sdk/dist/node/deploy-manifest.js.map +1 -1
  96. package/sdk/dist/node/gitvault-address.js +2 -2
  97. package/sdk/dist/node/gitvault-address.js.map +1 -1
  98. package/sdk/dist/node/gitvault-creation-journal.d.ts +1 -1
  99. package/sdk/dist/node/gitvault-creation-journal.js +2 -2
  100. package/sdk/dist/node/gitvault-creation-journal.js.map +1 -1
  101. package/sdk/dist/node/gitvault-deploy.d.ts +2 -2
  102. package/sdk/dist/node/gitvault-deploy.js +2 -2
  103. package/sdk/dist/node/gitvault-handoff.d.ts +142 -3
  104. package/sdk/dist/node/gitvault-handoff.d.ts.map +1 -1
  105. package/sdk/dist/node/gitvault-handoff.js +251 -21
  106. package/sdk/dist/node/gitvault-handoff.js.map +1 -1
  107. package/sdk/dist/node/gitvault-keystore.d.ts +78 -3
  108. package/sdk/dist/node/gitvault-keystore.d.ts.map +1 -1
  109. package/sdk/dist/node/gitvault-keystore.js +46 -5
  110. package/sdk/dist/node/gitvault-keystore.js.map +1 -1
  111. package/sdk/dist/node/gitvault-mirror.d.ts.map +1 -1
  112. package/sdk/dist/node/gitvault-mirror.js +3 -5
  113. package/sdk/dist/node/gitvault-mirror.js.map +1 -1
  114. package/sdk/dist/node/gitvault-profile-scan.js +1 -1
  115. package/sdk/dist/node/gitvault-publication.d.ts +379 -21
  116. package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
  117. package/sdk/dist/node/gitvault-publication.js +664 -41
  118. package/sdk/dist/node/gitvault-publication.js.map +1 -1
  119. package/sdk/dist/node/gitvault-recover.d.ts.map +1 -1
  120. package/sdk/dist/node/gitvault-recover.js +7 -10
  121. package/sdk/dist/node/gitvault-recover.js.map +1 -1
  122. package/sdk/dist/node/gitvault-snapshot.d.ts +1 -2
  123. package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -1
  124. package/sdk/dist/node/gitvault-snapshot.js +1 -2
  125. package/sdk/dist/node/gitvault-snapshot.js.map +1 -1
  126. package/sdk/dist/node/gitvault-writer-state.d.ts +201 -0
  127. package/sdk/dist/node/gitvault-writer-state.d.ts.map +1 -0
  128. package/sdk/dist/node/gitvault-writer-state.js +357 -0
  129. package/sdk/dist/node/gitvault-writer-state.js.map +1 -0
  130. package/sdk/dist/node/index.d.ts +4 -0
  131. package/sdk/dist/node/index.d.ts.map +1 -1
  132. package/sdk/dist/node/index.js +20 -1
  133. package/sdk/dist/node/index.js.map +1 -1
  134. package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
  135. package/sdk/dist/node/paid-fetch.js +3 -4
  136. package/sdk/dist/node/paid-fetch.js.map +1 -1
  137. package/sdk/dist/node/sites-node.js +1 -1
  138. package/sdk/dist/node/sites-node.js.map +1 -1
@@ -84,12 +84,12 @@
84
84
  * no per-ref object selection, so one batch = one full restore. That is a
85
85
  * superset of what git asked for (git writes the refs itself from `list`),
86
86
  * never a subset — but it is not incremental.
87
- * - `push` REPAIRS a DANGLING vault HEAD and otherwise never moves it
88
- * (kychee-com/run402#568). A fresh vault defaults its HEAD symref to
89
- * `refs/heads/main`; before this fix, a first push of any OTHER branch
90
- * left that symref naming a ref that would never exist, and the first
91
- * `git clone` warned "remote HEAD refers to nonexistent ref" and checked
92
- * out an EMPTY tree — publishing landed, but nothing was reachable from
87
+ * - `push` REPAIRS a DANGLING vault HEAD and otherwise never moves it.
88
+ * A fresh vault defaults its HEAD symref to `refs/heads/main`. Without
89
+ * the repair, a first push of any OTHER branch would leave that symref
90
+ * naming a ref that never exists, and the first
91
+ * `git clone` warns "remote HEAD refers to nonexistent ref" and checks
92
+ * out an EMPTY tree — publishing lands, but nothing is reachable from
93
93
  * it. Now: when the vault's current HEAD target is unset, or is a
94
94
  * symref naming a ref this push's own batch does not leave present, the
95
95
  * helper points it at one of the branches THIS push is publishing — the
@@ -98,7 +98,7 @@
98
98
  * which and why (see `chooseGitvaultHeadTargetForPush`). A HEALTHY HEAD
99
99
  * (one that already names a ref this push leaves present) is NEVER
100
100
  * touched — push moving history never means push moving HEAD.
101
- * - `option dry-run true` (kychee-com/run402#565) runs the REAL local
101
+ * - `option dry-run true` runs the REAL local
102
102
  * pipeline — pack building, encryption sizing, via `vault.planPush` — and
103
103
  * reports the per-ref `ok` lines a real push would, plus a stderr summary
104
104
  * (objects, encrypted bytes, refs, the generation it would admit as,
@@ -210,12 +210,11 @@ const endBlock = () => process.stdout.write("\n");
210
210
  const note = (line) => process.stderr.write(`git-remote-run402: ${line}\n`);
211
211
 
212
212
  /**
213
- * Wallet selection (kychee-com/run402#558). Before this, this file called
214
- * `getSdk()` directly and ran NO wallet selection at all — a `.run402.json`
215
- * binding, and even the global `wallets use` default, silently never
216
- * reached it; only the `RUN402_WALLET` env layer worked, so a bound
217
- * checkout's very next `git push run402 main` after a correctly-bound
218
- * `run402 repos create` used the WRONG wallet's (usually empty) allowance.
213
+ * Wallet selection. Calling `getSdk()` directly here would run NO wallet
214
+ * selection at all — a `.run402.json` binding, and even the global
215
+ * `wallets use` default, would silently never reach it; only the
216
+ * `RUN402_WALLET` env layer would work, so a bound checkout's `git push
217
+ * run402 main` would use the WRONG wallet's (usually empty) allowance.
219
218
  *
220
219
  * Shares `resolveWalletCore`/`enforceWalletExistsCore` with the CLI
221
220
  * (`cli/lib/wallet-context.mjs`) — ONE implementation, minus the CLI's
@@ -261,7 +260,7 @@ function walletSourceLabel(resolved) {
261
260
  * an env var or a binding DID name one, the remedy is actively harmful —
262
261
  * `run402 init` recreates the DEFAULT wallet's allowance, a DIFFERENT
263
262
  * wallet than the one that was actually resolved and whose allowance is
264
- * actually missing/broken (kychee-com/run402#558's second defect). Replace
263
+ * actually missing/broken. Replace
265
264
  * it with the resolved wallet's name and how selection works, so the fix is
266
265
  * "correct the selection" rather than "recreate the wrong wallet".
267
266
  */
@@ -358,7 +357,7 @@ export function partitionProtectedRefPushes(specs) {
358
357
 
359
358
  /**
360
359
  * Decide whether THIS push must repair a DANGLING vault HEAD, and to what
361
- * (kychee-com/run402#568 — the first-clone empty-tree hazard). The rule:
360
+ * — the first-clone empty-tree hazard. The rule:
362
361
  *
363
362
  * WHEN the vault's current materialized HEAD target is absent, OR is a
364
363
  * symref naming a ref this push's own batch does not leave present, set
@@ -432,11 +431,10 @@ export function shouldRunAutoGc(threshold, generationsSinceCheckpoint) {
432
431
  * `process.cwd()` only in CALLER convention, never inside the SDK's own
433
432
  * `open()` (which sets it ONLY when `options.repo_dir !== undefined`).
434
433
  * Building the auto-gc target from `{ ...target }` alone — omitting
435
- * `repo_dir` — made every real post-push auto-gc cycle fail closed with
434
+ * `repo_dir` — makes every real post-push auto-gc cycle fail closed with
436
435
  * `GITVAULT_REPO_DIR_REQUIRED`, silently degrading to the advisory on
437
- * EVERY push, live in production shape, until a push-latency guard bench
438
- * finally drove the real trigger path (found 2026-08-31; `repos gc`'s CLI
439
- * wrapper passes repo_dir explicitly and never hit this).
436
+ * EVERY push. `repos gc`'s CLI wrapper passes repo_dir explicitly, so it
437
+ * never exercises this path.
440
438
  */
441
439
  export function buildAutoGcCompactionTarget(target, repoDir) {
442
440
  return { ...target, repo_dir: repoDir };
@@ -462,10 +460,8 @@ async function main(argv, { onBackgroundWork } = {}) {
462
460
  const addressForm = gitvaultRemoteAddressForm(address);
463
461
  const target = { project_id: address.project_id, org_id: address.org_id };
464
462
  let verbosity = 1;
465
- // kychee-com/run402#565: `option dry-run true` used to be honestly
466
- // `unsupported` (this helper could not rehearse a publication, and
467
- // reporting a fake success would be worse than refusing). It now IS
468
- // real — see `handleOption`'s `dry-run` case and `runPush` below.
463
+ // `option dry-run true` is a REAL dry run — see `handleOption`'s
464
+ // `dry-run` case and `runPush` below. It never reports a fake success.
469
465
  let dryRun = false;
470
466
 
471
467
  /**
@@ -533,8 +529,8 @@ async function main(argv, { onBackgroundWork } = {}) {
533
529
  }
534
530
 
535
531
  /**
536
- * What to tell a human when we refuse. `git clone` is the case that used to
537
- * fail; naming the working alternative beats a bare error.
532
+ * What to tell a human when we refuse. `git clone` is the case that
533
+ * fails here; naming the working alternative beats a bare error.
538
534
  */
539
535
  function repoRefusalNote(err) {
540
536
  note(describeError(err));
@@ -757,7 +753,7 @@ async function main(argv, { onBackgroundWork } = {}) {
757
753
  for (const ref of Object.keys(refs).sort()) out(`${refs[ref]} ${ref}`);
758
754
  // A snapshot-only vault holds protocol refs but no branch heads, so a
759
755
  // plain `git clone` prints "cloned an empty repository" with no hint the
760
- // history exists (blind-acceptance finding, 2026-08-28). Say where it is.
756
+ // history exists. Say where it is.
761
757
  const refNames = Object.keys(refs);
762
758
  if (refNames.length > 0 && !refNames.some((r) => r.startsWith("refs/heads/"))) {
763
759
  note(`this vault has no branch heads yet — its history lives on ${refNames.sort()[0]}`);
@@ -898,9 +894,7 @@ async function main(argv, { onBackgroundWork } = {}) {
898
894
  // convention, not an SDK fallback (`open()` only sets repo_dir when
899
895
  // `options.repo_dir !== undefined`). Omitting it here made every real
900
896
  // post-push auto-gc cycle fail closed with GITVAULT_REPO_DIR_REQUIRED,
901
- // silently degrading to the advisory on every push (found live,
902
- // 2026-08-31, via a push-latency guard bench that finally drove the
903
- // ACTUAL trigger path instead of a synthetic `repos gc` invocation).
897
+ // silently degrading to the advisory on every push.
904
898
  const compactionTarget = buildAutoGcCompactionTarget(target, repoDir);
905
899
  const runCycle = async () => {
906
900
  const sdk = getCachedSdk();
@@ -966,7 +960,7 @@ async function main(argv, { onBackgroundWork } = {}) {
966
960
  }
967
961
 
968
962
  if (dryRun) {
969
- // kychee-com/run402#565: READ-ONLY resolution — `openVault`, never
963
+ // READ-ONLY resolution — `openVault`, never
970
964
  // `openOrCreateVault` — so a push-to-create dry run allocates
971
965
  // NOTHING. An unresolved vault means there is nothing to preview a
972
966
  // push against yet (no repo_id ⇒ no encryption key ⇒ sizing is
@@ -1034,7 +1028,7 @@ async function main(argv, { onBackgroundWork } = {}) {
1034
1028
  force: spec.force && expectedOld !== null,
1035
1029
  });
1036
1030
  }
1037
- // Repair a DANGLING HEAD from this batch's own branches (#568) — see
1031
+ // Repair a DANGLING HEAD from this batch's own branches — see
1038
1032
  // `chooseGitvaultHeadTargetForPush`'s own doc comment for the exact
1039
1033
  // rule. `localHeadRef` is read from THIS repository (never cwd, same
1040
1034
  // fail-closed resolution as everything else in this function).
@@ -1114,7 +1108,7 @@ async function main(argv, { onBackgroundWork } = {}) {
1114
1108
  out("ok");
1115
1109
  return;
1116
1110
  case "dry-run":
1117
- // kychee-com/run402#565: a REAL dry run — `runPush` runs the actual
1111
+ // a REAL dry run — `runPush` runs the actual
1118
1112
  // local pipeline (pack building, encryption sizing) and stops before
1119
1113
  // the two network mutations. `value` is git's own boolean spelling
1120
1114
  // ("true"/"false"); anything else is refused rather than guessed.
package/lib/repos.mjs CHANGED
@@ -95,6 +95,7 @@ Maintenance:
95
95
  run402 repos access repair [--project <id>] [--repo <repo_id>] --recipient-state-version <n> --recipient-revocation-version <n>
96
96
  run402 repos access revoke-key <principal_id> [--project <id>] [--repo <repo_id>]
97
97
  run402 repos access declare-exposure [--project <id>] [--repo <repo_id>]
98
+ run402 repos access repin [--project <id>] [--repo <repo_id>] --principal <principal_id> --fingerprint <ek_fingerprint>
98
99
  run402 repos policy <required|grandfathered> [--project <id>] [--repo <repo_id>] [--reason <why>]
99
100
  run402 repos policy auto-gc [<generations>|off] (local, per-checkout — no --project/--repo)
100
101
 
@@ -407,6 +408,10 @@ Options:
407
408
  verified prefix, so a budget-exceeded run resumes; a
408
409
  --no-write run does not, since nothing was persisted)
409
410
  --mirror fsck: also run the keyless mirror integrity probe
411
+ --no-reconcile view/access: defer the session-start envelope fulfilment
412
+ (a key-holder normally wraps every pending member on its
413
+ first ordinary read); reported as deferred_by_local_policy,
414
+ never as coverage. fsck/recover never wrap regardless.
410
415
  --no-write fsck: audit mode — compute and report the real answer,
411
416
  persist neither local trust pin
412
417
  --submit gc: submit the planned prune intent. Requires
@@ -629,6 +634,29 @@ async function formatRepoHuman(s, mirror) {
629
634
  : "can decrypt (read-only — no signing key)";
630
635
  lines.push(`This machine: ${decryptPart}. Policy: ${s.gitvault_policy ?? "(none)"}`);
631
636
 
637
+ // gitvault-multi-writer (rev 47) task 6.1 — the writer roster, one line
638
+ // per writer: `writer_set` is the chain-verified set exactly as `verify`/
639
+ // `fsck` would derive it (never re-verified here — this is `view`'s own
640
+ // side-effect-free read of the vault RECORD's already-verified pointer).
641
+ // `pending_writers` is the reverse direction (eligible org members not
642
+ // yet admitted); `read_only_terminal` (D228) is the forced sole-writer-
643
+ // removal terminal — surfaced prominently since a vault in that state
644
+ // still serves reads but can never accept another push until recovered.
645
+ const writerSet = s.vault.writer_set;
646
+ if (writerSet) {
647
+ lines.push(`Writers (${writerSet.writers.length}):`);
648
+ for (const w of writerSet.writers) {
649
+ lines.push(` ${w.writer_key_id} — admitted generation ${decimal(w.admitted_generation)} (${w.authorization_kind})`);
650
+ }
651
+ if (s.vault.read_only_terminal) {
652
+ lines.push(" ⚠ read-only terminal: the last writer was removed — this vault serves reads but cannot accept a push until a new writer is admitted through a recovery path.");
653
+ }
654
+ const pendingWriters = s.vault.pending_writers ?? [];
655
+ if (pendingWriters.length > 0) {
656
+ lines.push(`Pending writers (${pendingWriters.length}, eligible but not yet admitted): ${pendingWriters.map((p) => p.writer_key_id).join(", ")} — run 'run402 repos access sync' if this machine is already a writer.`);
657
+ }
658
+ }
659
+
632
660
  // gitvault-mirror-default: the SDK-computed vault_unmirrored finding is
633
661
  // echoed verbatim (never rephrased here) — informational, never blocking.
634
662
  if (mirror?.finding) {
@@ -855,10 +883,9 @@ async function createProvision(name, dir, a) {
855
883
 
856
884
  // A genuinely bare machine has no allowance file at all, and the NO_ALLOWANCE
857
885
  // precheck below would refuse before the provision call could ever answer
858
- // NO_ACTIVE_TIER so the cold-start fold (design D5) never ran for the one
859
- // case it was written for. Found 2026-09-02 by driving `repos create` on a
860
- // fresh config dir. Fold first when there is no wallet; `--no-init` keeps
861
- // the bare refusal.
886
+ // NO_ACTIVE_TIER, so the cold-start fold would never reach the one case it
887
+ // exists for. Fold first when there is no wallet; `--no-init` keeps the
888
+ // bare refusal.
862
889
  if (!isCoreApiTarget() && !loadLiveControlPlaneSession() && !readAllowance() && !a.includes("--no-init")) {
863
890
  console.error("no wallet on this machine — folding the cold-start chain (allowance -> faucet -> prototype tier)");
864
891
  try {
@@ -1066,7 +1093,7 @@ async function list(args) {
1066
1093
 
1067
1094
  async function view(args) {
1068
1095
  const a = normalizeArgv(args);
1069
- assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--human", "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
1096
+ assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--human", "--no-reconcile", "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
1070
1097
  requirePositionalCount(a, COMMON_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos view", missing: "" });
1071
1098
  const human = a.includes("--human");
1072
1099
  if (human && a.includes("--json")) {
@@ -1077,7 +1104,7 @@ async function view(args) {
1077
1104
  try {
1078
1105
  // Design D3: `view` NEVER passes `refs: true` — it is side-effect-free
1079
1106
  // by construction, not by convention. Materialization belongs to `fsck`.
1080
- const s = await sdk.gitvault.status(target);
1107
+ const s = await sdk.gitvault.status({ ...target, ...(a.includes("--no-reconcile") ? { reconcile: "deferred" } : {}) });
1081
1108
  const isByo = s.vault?.storage_profile === "byo";
1082
1109
  let mirror = null;
1083
1110
  if (s.repo_id) {
@@ -1493,8 +1520,7 @@ async function snapshot(args) {
1493
1520
  if (manifestOutPath != null) writeManifestOut(manifestOutPath, result);
1494
1521
  // Snapshot-only vault (no branch head published): a plain `git clone`
1495
1522
  // of this vault prints "cloned an empty repository" with no hint the
1496
- // snapshot exists (blind-acceptance finding, 2026-08-28). Name the
1497
- // restore path in the result itself.
1523
+ // snapshot exists. Name the restore path in the result itself.
1498
1524
  const resultRefNames = Object.keys(result.refs ?? {});
1499
1525
  const snapshotOnly = resultRefNames.length > 0 && !resultRefNames.some((r) => r.startsWith("refs/heads/"));
1500
1526
  const payload = summarizeSnapshotPayload(result, { verbose, manifestPath: manifestOutPath });
@@ -1674,6 +1700,12 @@ async function handoff(args) {
1674
1700
  console.error(w.message ?? `${w.code}`);
1675
1701
  }
1676
1702
  console.error(`handoff minted: role ${result.minted_role}, expires ${result.expires_at}`);
1703
+ // gitvault-multi-writer (rev 47) task 6.4 — a handoff is now also a
1704
+ // WRITER admission, not just a checkout pass: the recipient signs its
1705
+ // own future pushes with a NEW key this vault's chain recognizes as a
1706
+ // writer the moment `resume` claims it (design D4 — a grant minted here,
1707
+ // a two-signature acceptance the recipient's own resume completes).
1708
+ console.error(`the recipient becomes a WRITER on this vault the moment they resume — their own key signs future pushes, not yours.`);
1677
1709
  console.error(`recipient runs: kygit resume <key printed below>`);
1678
1710
  if (asJson) {
1679
1711
  printJson(sdk, result);
@@ -1781,8 +1813,7 @@ async function resume(args) {
1781
1813
  key = (await readStdinText()).trim();
1782
1814
  if (!key) fail({ code: "BAD_USAGE", message: "Missing the Handoff Key on stdin.", hint: "Pipe the kgh1_… key, or pass it as a positional argument." });
1783
1815
  }
1784
- // Decided 2026-09-02 (Tal; amends kygit-handoff design D5's "none on
1785
- // resume"): a resumed agent is a NEW run402 wallet, and the loop is the
1816
+ // A resumed agent is a NEW run402 wallet, and the loop is the
1786
1817
  // point — so on a wallet with no active tier `resume` folds the same
1787
1818
  // cold-start chain `create` does (allowance → faucet → one x402
1788
1819
  // prototype payment, announced) BEFORE the claim. The claim itself needs
@@ -1804,6 +1835,13 @@ async function resume(args) {
1804
1835
  }
1805
1836
  console.error("");
1806
1837
  console.error(`resumed into ${result.restored.dir} (branch ${result.restored.branch})`);
1838
+ // gitvault-multi-writer (rev 47) task 6.4 — this checkout's own writer
1839
+ // activation (design D5): the outcome is "active" either way, whether
1840
+ // this call submitted a fresh activation head or a prior attempt's
1841
+ // already landed (crash-resumable, task 5.6's own idempotent-skip).
1842
+ if (result.writer_activation) {
1843
+ console.error(`writer: ${result.writer_activation.outcome} (generation ${result.writer_activation.generation})`);
1844
+ }
1807
1845
  if (result.deduplicated) console.error("note: this key was already claimed by this same principal — the ORIGINAL envelope was reused (safe replay)");
1808
1846
  for (const na of result.next_actions ?? []) {
1809
1847
  if (na.command) console.error(`next: ${na.command}${na.why ? ` — ${na.why}` : ""}`);
@@ -2355,7 +2393,7 @@ function printMemberCustodySummary(mc) {
2355
2393
 
2356
2394
  async function accessRead(args) {
2357
2395
  const a = normalizeArgv(args);
2358
- assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--human", "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
2396
+ assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--human", "--no-reconcile", "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
2359
2397
  requirePositionalCount(a, COMMON_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos access", missing: "" });
2360
2398
  const human = a.includes("--human");
2361
2399
  if (human && a.includes("--json")) {
@@ -2364,7 +2402,7 @@ async function accessRead(args) {
2364
2402
  const sdk = getSdk();
2365
2403
  const target = await vaultTarget(a);
2366
2404
  try {
2367
- const result = await sdk.gitvault.access(target);
2405
+ const result = await sdk.gitvault.access({ ...target, ...(a.includes("--no-reconcile") ? { reconcile: "deferred" } : {}) });
2368
2406
  // gitvault-recovery-custody — the "you" block: YOUR OWN wrapper custody
2369
2407
  // (kind/state per wrapper, custody scheme), rendered inside the family's
2370
2408
  // custody roster read. Principal-scoped, so it needs your control-plane
@@ -2523,8 +2561,8 @@ async function accessRevokeKey(args) {
2523
2561
  * self-contained; the FOLLOW-UP rotation it authorizes is NOT auto-run
2524
2562
  * here, because — same confirmed gap as `access repair` — the D194
2525
2563
  * counters it must be fenced against have no client-visible read for this
2526
- * reason value either. This is the rekey remedy the exposed-key incident
2527
- * needs: declare here, then rotate (via `--recipient-state-version`/
2564
+ * reason value either. This is the rekey remedy for an exposed key:
2565
+ * declare here, then rotate (via `--recipient-state-version`/
2528
2566
  * `--recipient-revocation-version` once known, e.g. from platform staff).
2529
2567
  *
2530
2568
  * **If a `/confirm`/`/repin` receipt is already pending** (a directory
@@ -2532,7 +2570,7 @@ async function accessRevokeKey(args) {
2532
2570
  * the rotation is outstanding), do NOT call `publishPinManifestUpdate`
2533
2571
  * separately — that call is itself an ORDINARY admission and is itself
2534
2572
  * refused `EPOCH_ROTATION_REQUIRED` for as long as this declaration stays
2535
- * outstanding (reproduced live in production 2026-08-27). Pass the receipt
2573
+ * outstanding. Pass the receipt
2536
2574
  * to `r.gitvault.rotateEpoch({..., pending_confirmations: [{principal_id,
2537
2575
  * ek_fingerprint, receipt}]})` instead — it rides the SAME head as the
2538
2576
  * rotation this declaration requires, publishing durably without needing a
@@ -2541,6 +2579,33 @@ async function accessRevokeKey(args) {
2541
2579
  * excluded from THIS rotation's own envelope set (D196) and becomes
2542
2580
  * eligible starting at the NEXT rotation.
2543
2581
  */
2582
+ /**
2583
+ * `repos access repin --principal <id> --fingerprint <ek_…>` — a KEY-HOLDER
2584
+ * explicitly accepts a recipient's CHANGED key (gitvault-agent-envelopes D3).
2585
+ * The session-start reconcile refuses `pinned_key_mismatch` and never
2586
+ * bypasses it, not even after an owner's revoke — acceptance names the new
2587
+ * fingerprint (the out-of-band verification point) and moves the local pin.
2588
+ * Adapter only: `sdk.gitvault.acceptRecipientKeyChange`.
2589
+ */
2590
+ async function accessRepin(args) {
2591
+ const a = normalizeArgv(args);
2592
+ assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--principal", "--fingerprint", "--help", "-h"], [...COMMON_VALUE_FLAGS, "--principal", "--fingerprint"]);
2593
+ requirePositionalCount(a, [...COMMON_VALUE_FLAGS, "--principal", "--fingerprint"], { min: 0, max: 0, command: "run402 repos access repin --principal <principal_id> --fingerprint <ek_fingerprint>", missing: "" });
2594
+ const principalId = flagValue(a, "--principal");
2595
+ const fingerprint = flagValue(a, "--fingerprint");
2596
+ if (!principalId || !fingerprint) {
2597
+ fail({ code: "BAD_USAGE", message: "repin needs --principal <principal_id> and --fingerprint <ek_fingerprint> (read the fingerprint back with the recipient — it is public data).", hint: "run402 repos access repin --principal <principal_id> --fingerprint <ek_fingerprint>" });
2598
+ }
2599
+ const sdk = getSdk();
2600
+ const target = await vaultTarget(a);
2601
+ try {
2602
+ const result = await sdk.gitvault.acceptRecipientKeyChange({ ...target, principal_id: principalId, new_fingerprint: fingerprint });
2603
+ console.log(JSON.stringify({ ...result, next_actions: [{ type: "retry", command: "run402 repos view", why: "the next ordinary gitvault operation on this machine wraps the vault to the accepted key" }] }, null, 2));
2604
+ } catch (err) {
2605
+ reportSdkError(err);
2606
+ }
2607
+ }
2608
+
2544
2609
  async function accessDeclareExposure(args) {
2545
2610
  const a = normalizeArgv(args);
2546
2611
  assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
@@ -2561,11 +2626,48 @@ async function accessDeclareExposure(args) {
2561
2626
  }
2562
2627
  }
2563
2628
 
2629
+ // gitvault-multi-writer (rev 47) task 6.4 — an on-demand tail on the
2630
+ // existing `access` family: `r.gitvault.reconcile()` admits every eligible
2631
+ // `pending_writers[]` candidate right now, rather than waiting for this
2632
+ // machine's next push/deploy to do it as a side effect. Same shape as
2633
+ // `accessRepin` above — one SDK call, echo the result, a next_action
2634
+ // pointing at the normal follow-up.
2635
+ async function accessSync(args) {
2636
+ const a = normalizeArgv(args);
2637
+ assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--human", "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
2638
+ requirePositionalCount(a, COMMON_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos access sync", missing: "" });
2639
+ const human = a.includes("--human");
2640
+ if (human && a.includes("--json")) {
2641
+ fail({ code: "BAD_USAGE", message: "--human cannot be combined with --json.", details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") } });
2642
+ }
2643
+ const sdk = getSdk();
2644
+ const target = await vaultTarget(a);
2645
+ try {
2646
+ const result = await sdk.gitvault.reconcile(target);
2647
+ if (!human) console.log(JSON.stringify(result, null, 2));
2648
+ if (!result.eligible) {
2649
+ console.error("this machine's key is not an active writer on this vault — nothing to sync. Ask a current writer to admit you (org membership at role developer+ and a published signing key make you eligible).");
2650
+ } else if (result.admitted.length === 0) {
2651
+ console.error("no pending writer candidates — nothing to sync.");
2652
+ } else {
2653
+ for (const w of result.admitted) console.error(`admitted ${w.writer_key_id} (${w.principal_id}) at generation ${w.generation}.`);
2654
+ }
2655
+ if (result.skipped.length > 0) {
2656
+ for (const s of result.skipped) console.error(`skipped ${s.writer_key_id} (${s.principal_id}): ${s.reason}`);
2657
+ }
2658
+ printVerboseStats(a, sdk);
2659
+ } catch (err) {
2660
+ reportSdkError(err);
2661
+ }
2662
+ }
2663
+
2564
2664
  async function access(args) {
2565
2665
  const a = normalizeArgv(args);
2566
2666
  if (a[0] === "repair") return accessRepair(a.slice(1));
2567
2667
  if (a[0] === "revoke-key") return accessRevokeKey(a.slice(1));
2568
2668
  if (a[0] === "declare-exposure") return accessDeclareExposure(a.slice(1));
2669
+ if (a[0] === "repin") return accessRepin(a.slice(1));
2670
+ if (a[0] === "sync") return accessSync(a.slice(1));
2569
2671
  return accessRead(a);
2570
2672
  }
2571
2673
 
@@ -54,7 +54,7 @@ const STATE_FILE = "messaging.json";
54
54
  /**
55
55
  * Resolve the addressed room to { orgId, roomKey } (one SDK lookup at most).
56
56
  *
57
- * A room is a PAIR, and this function owns the pair. What it no longer owns is
57
+ * A room is a PAIR, and this function owns the pair. It does NOT own
58
58
  * the org half of its own fallback: below the explicit forms, the org comes
59
59
  * from the shared chain in `org-context.mjs` (flag → env → binding → profile
60
60
  * selection → active project) while the room key comes from `--room`, the
@@ -71,8 +71,8 @@ export async function resolveRoom({ org, room, project } = {}) {
71
71
  if (envRoom) {
72
72
  const slash = envRoom.indexOf("/");
73
73
  if (slash <= 0 || slash === envRoom.length - 1) {
74
- // Same class as kychee-com/run402-private#640: a malformed RUN402_ROOM
75
- // is a value we know nothing about, so it must not be echoed raw.
74
+ // A malformed RUN402_ROOM is a value we know nothing about (a leaked
75
+ // secret included), so it must not be echoed raw.
76
76
  fail({
77
77
  code: "BAD_USAGE",
78
78
  message: `${ROOM_ENV} must be "<org_id>/<room_key>".`,
@@ -239,8 +239,8 @@ export async function registerFreshPresence(orgId, roomKey, { name, task } = {})
239
239
  * REGISTER a replacement, and retry ONCE with it. The registration is
240
240
  * explicit on purpose: a bare call cannot mean "me" from the credential
241
241
  * alone, because the server cannot tell one credential's concurrent sessions
242
- * apart — it would either guess (adopting a sibling session's presence,
243
- * run402-private#663) or refuse. Asking — by session identity when we have
242
+ * apart — it would either guess (adopting a sibling session's presence)
243
+ * or refuse. Asking — by session identity when we have
244
244
  * one, by explicit (re-)registration when we don't — is unambiguous.
245
245
  */
246
246
  export async function withPresenceRetry(orgId, roomKey, call, { name, task } = {}) {
@@ -6,8 +6,7 @@
6
6
  * `process.exit(1)`. Preserves specific behaviors:
7
7
  * - `ProjectCredentialNotFound` → preserves
8
8
  * `code: "PROJECT_CREDENTIAL_NOT_FOUND"` and local-cache provenance.
9
- * - HTML / non-JSON error bodies → `body_preview` field (first 500 chars),
10
- * matching GH-84 behavior.
9
+ * - HTML / non-JSON error bodies → `body_preview` field (first 500 chars).
11
10
  * - Network errors → `{status: "error", message: "..."}`.
12
11
  *
13
12
  * For client-side validation failures (missing flags, bad JSON, no-op
package/lib/status.mjs CHANGED
@@ -137,7 +137,7 @@ export async function run(args = []) {
137
137
  const walletName = getActiveProfile();
138
138
  const walletMeta = readMeta(walletName);
139
139
 
140
- // GH-32 follow-up: balances are grouped under one object so the on-chain and
140
+ // Balances are grouped under one object so the on-chain and
141
141
  // prepaid-credit numbers are unambiguous and rail-legible.
142
142
  // - on_chain_usd_micros / on_chain_token: on-chain USDC (x402) or pathUSD
143
143
  // (mpp), null if the RPC read failed
@@ -448,7 +448,7 @@ describe("CLI update notices and scheduler", () => {
448
448
  assert.equal(check.status, "skipped");
449
449
  }));
450
450
 
451
- // ─── the real TTL, and what happens at every edge of it (kychee-com/run402#561) ──
451
+ // ─── the real TTL, and what happens at every edge of it ──
452
452
  //
453
453
  // THE DEFECT (grok dogfood, 4.38.0 installed): `run402 doctor` reported
454
454
  // the latest CLI as 4.17.5 from a cache dated Aug 1 — three minor versions
@@ -133,10 +133,10 @@ export class WalletSelectionError extends Error {
133
133
  }
134
134
 
135
135
  // A rejected name is a value we know nothing about — the most likely
136
- // mistake is a typo, but kychee-com/run402-private#640 was a live
137
- // Base-mainnet private key pasted into RUN402_WALLET (a NAME field, not a
138
- // key field), and the raw value used to be echoed straight into this error
139
- // — printed to a terminal, a log, and a session transcript. Route it
136
+ // mistake is a typo, but a live Base-mainnet private key pasted into
137
+ // RUN402_WALLET (a NAME field, not a key field) is a demonstrated case, and
138
+ // echoing the raw value prints it to a terminal, a log, and a session
139
+ // transcript. Route it
140
140
  // through describeRejectedValue() so a short/plain typo still shows in
141
141
  // full (that's what makes the error useful) while anything long or
142
142
  // hex-shaped enough to be a secret is redacted instead.
@@ -167,11 +167,11 @@ function assertValidName(name, origin) {
167
167
  * Shared verbatim by `resolveWallet` below (the CLI wrapper: `--wallet` flag
168
168
  * layered on top, failures routed through `fail()`) AND by
169
169
  * `git-remote-run402` (no flag layer — the helper has no argv flags at all
170
- * — failures routed through its own protocol-safe error reporting). This is
171
- * THE fix for kychee-com/run402#558: before it, `git-remote-run402` called
172
- * `getSdk()` directly and ran no wallet selection at all, so a `.run402.json`
173
- * binding and even the global `wallets use` default — silently never
174
- * reached it; only the `RUN402_WALLET` env layer worked.
170
+ * — failures routed through its own protocol-safe error reporting). Calling
171
+ * `getSdk()` directly instead would run no wallet selection at all, so a
172
+ * `.run402.json` binding and even the global `wallets use` default
173
+ * would silently never reach the helper; only the `RUN402_WALLET` env layer
174
+ * would work.
175
175
  *
176
176
  * Precedence beneath an optional flag (highest first): `RUN402_WALLET` /
177
177
  * `RUN402_PROFILE` env > nearest `.run402.local.json`/`.run402.json` binding
@@ -197,7 +197,7 @@ export function resolveWalletCore({ walletFlag, env = {}, cwd = process.cwd(), c
197
197
 
198
198
  if (envName && binding && envName !== binding.wallet && !CONFLICT_EXEMPT.has(cmd)) {
199
199
  // This conflict check runs BEFORE assertValidNameCore below, so an
200
- // unvalidated (possibly secret-shaped — kychee-com/run402-private#640)
200
+ // unvalidated (possibly secret-shaped)
201
201
  // RUN402_WALLET reaches here first whenever a directory binding exists
202
202
  // (routine per this repo's own fleet-coordination convention). Redact
203
203
  // the env side the same way the format check would; `binding.wallet`
@@ -175,10 +175,9 @@ describe("resolveWallet — conflict + validation", () => {
175
175
  // The WALLET_SELECTION_CONFLICT check runs BEFORE assertValidNameCore, so a
176
176
  // secret-shaped RUN402_WALLET reaches it unvalidated whenever a directory
177
177
  // binding also exists — routine for any checkout following this repo's own
178
- // fleet-coordination convention (`.run402.json`). This is a stricter variant
179
- // of kychee-com/run402-private#640: no BAD_WALLET_NAME format check ever
180
- // gets a chance to run first, so the redaction has to happen at this site
181
- // too, not just at assertValidNameCore.
178
+ // fleet-coordination convention (`.run402.json`). No BAD_WALLET_NAME
179
+ // format check ever gets a chance to run first, so the redaction has to
180
+ // happen at this site too, not just at assertValidNameCore.
182
181
  it("never echoes a secret-shaped RUN402_WALLET via the conflict path", () => {
183
182
  const dir = bindingDir("client-a"); // a real, differently-named binding
184
183
  const privateKey = "0x" + "22a3f0".repeat(11); // 66 chars
@@ -201,7 +200,7 @@ describe("resolveWallet — conflict + validation", () => {
201
200
  rmSync(dir, { recursive: true, force: true });
202
201
  });
203
202
 
204
- // kychee-com/run402-private#640: a live Base-mainnet private key was pasted
203
+ // a live Base-mainnet private key was pasted
205
204
  // into RUN402_WALLET (a NAME field, not a key field), failed this exact
206
205
  // check, and got echoed verbatim into a terminal, a log, and a session
207
206
  // transcript. A value that fails validation is a value the CLI knows
@@ -254,8 +253,8 @@ describe("enforceWalletExists — fail closed", () => {
254
253
  // A bare (no "0x" prefix) 64-char lowercase-hex private key satisfies the
255
254
  // wallet-name CHARSET (lowercase letters/digits/'_'/'-'), so it sails past
256
255
  // assertValidNameCore's format check and only fails HERE, on existence —
257
- // the second, easy-to-miss half of kychee-com/run402-private#640's blast
258
- // radius. It must still never be echoed.
256
+ // the second, easy-to-miss half of the blast radius. It must still never
257
+ // be echoed.
259
258
  it("redacts a bare-hex value that passes the name format check but not existence", () => {
260
259
  const bareKey = "22a3f0".repeat(10) + "aabb"; // 64 lowercase-hex chars
261
260
  assert.match(bareKey, /^[a-z0-9][a-z0-9_-]{0,63}$/); // sanity: really does pass the name regex
package/lib/wallets.mjs CHANGED
@@ -72,7 +72,7 @@ function out(obj) {
72
72
  console.log(JSON.stringify(obj, null, 2));
73
73
  }
74
74
 
75
- // See core-dist/redact.js's doc comment (kychee-com/run402-private#640): a
75
+ // See core-dist/redact.js's doc comment: a
76
76
  // value that fails this check may be a secret pasted into a name argument
77
77
  // by mistake, so it must never be echoed verbatim — describeRejectedValue()
78
78
  // still shows a short/plain typo in full.
@@ -176,7 +176,7 @@ function cmdUse(args) {
176
176
  if (name !== DEFAULT && !profileExists(name)) {
177
177
  // `name` already passed requireName's charset check, but a bare (no
178
178
  // "0x") 64-hex private key satisfies that charset too — describeRejectedValue()
179
- // is the backstop against echoing it here (kychee-com/run402-private#640).
179
+ // is the backstop against echoing it here.
180
180
  fail({ code: "WALLET_NOT_FOUND", message: `No local wallet named '${describeRejectedValue(name)}'.`, hint: "run402 wallets list", details: { name: describeRejectedValue(name) } });
181
181
  }
182
182
  setDefaultWallet(name);
package/lib/webhooks.mjs CHANGED
@@ -152,7 +152,7 @@ async function update(args) {
152
152
  if (!url && !eventsRaw) {
153
153
  fail({ code: "BAD_USAGE", message: "Provide at least --url or --events" });
154
154
  }
155
- // GH-192: scheme-only local validation. Server-side SSRF defenses are out
155
+ // scheme-only local validation. Server-side SSRF defenses are out
156
156
  // of scope for the CLI (private-IP / DNS rebinding / IMDS belongs on the
157
157
  // gateway). `validateWebhookUrl` is a no-op when `url` is null/undefined,
158
158
  // so partial updates that change only `--events` still work.
@@ -186,7 +186,7 @@ async function register(args) {
186
186
  hint: "run402 email webhooks register --url <url> --events <e1,e2>",
187
187
  });
188
188
  }
189
- // GH-192: validate scheme locally before any network call. Catches
189
+ // validate scheme locally before any network call. Catches
190
190
  // javascript:/file:/http:/data: schemes that the gateway would reject
191
191
  // anyway, but with a friendlier round-trip-free error.
192
192
  validateWebhookUrl(url, "--url");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "4.68.2",
3
+ "version": "4.69.0",
4
4
  "description": "CLI for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -115,7 +115,7 @@ export function buildSIWxAuthHeaders(opts) {
115
115
  * @param path - API path (e.g. "/projects/v1") used to build the SIWE uri field.
116
116
  */
117
117
  export function getAllowanceAuthHeaders(path, allowancePath) {
118
- // GH-194: readAllowance throws on a malformed-shape allowance file. The
118
+ // readAllowance throws on a malformed-shape allowance file. The
119
119
  // CLI's higher-level readAllowance wrapper surfaces this as a structured
120
120
  // BAD_ALLOWANCE_FILE envelope; here we preserve the public contract that
121
121
  // this helper returns SIWxAuthHeaders | null. Re-throw so callers above
@@ -14,7 +14,7 @@ const PRIVATE_KEY_RE = /^0x[a-fA-F0-9]{64}$/;
14
14
  * - the file exists but is not parseable JSON (preserve existing UX —
15
15
  * consumers print "no_allowance" and tell the user to run init)
16
16
  *
17
- * Throws a structured `Error` (GH-194) when the file parses as JSON but the
17
+ * Throws a structured `Error` when the file parses as JSON but the
18
18
  * shape is wrong (missing/wrong-type/wrong-length fields). Without this guard
19
19
  * downstream callers crash with raw stack traces:
20
20
  * - `cli/lib/status.mjs` reaches for `allowance.address.toLowerCase()`
@@ -83,10 +83,10 @@ function assertSafeProfileName(name) {
83
83
  return;
84
84
  if (!isValidProfileName(name)) {
85
85
  // A value that failed this check is a value we know nothing about — the
86
- // most likely mistake is a typo, but kychee-com/run402-private#640 was a
87
- // Base-mainnet private key pasted into RUN402_WALLET (a NAME field), and
88
- // the raw value used to be echoed straight into this message and
89
- // wherever it propagated (terminal, logs, session transcripts).
86
+ // most likely mistake is a typo, but a Base-mainnet private key pasted
87
+ // into RUN402_WALLET (a NAME field) is a demonstrated case, and echoing
88
+ // the raw value propagates it into terminals, logs, and session
89
+ // transcripts.
90
90
  // describeRejectedValue() shows short/plain values in full (useful for
91
91
  // an actual typo) and redacts anything long or hex-shaped enough to be
92
92
  // a secret instead of a name.