run402 4.68.3 → 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 +140 -1
  104. package/sdk/dist/node/gitvault-handoff.d.ts.map +1 -1
  105. package/sdk/dist/node/gitvault-handoff.js +226 -22
  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
package/lib/deploy-v2.mjs CHANGED
@@ -891,7 +891,7 @@ async function mergeAstroReleaseSlice(spec, dirArg) {
891
891
  * (inline bytes). Only `{ path }` entries with no `data` resolve to a disk
892
892
  * file; inline entries are skipped (nothing to scan on disk). Returns a
893
893
  * de-duped list of absolute paths. Existence + extension filtering is the
894
- * caller's job (GH-409). Defensive: tolerates missing/odd shapes silently —
894
+ * caller's job. Defensive: tolerates missing/odd shapes silently —
895
895
  * the manifest normalizer is the authority on shape validity, not this
896
896
  * best-effort extractor.
897
897
  */
@@ -1017,7 +1017,7 @@ async function applyCmd(args) {
1017
1017
  await mergeAstroReleaseSlice(spec, opts.dir);
1018
1018
  }
1019
1019
 
1020
- // GH-232: Reject empty specs client-side. Without this guard,
1020
+ // Reject empty specs client-side. Without this guard,
1021
1021
  // `run402 deploy apply --spec '{}'` (and `--manifest <empty>`) would silently
1022
1022
  // send an empty ReleaseSpec to /apply/v1/plans with no signal that nothing
1023
1023
  // was deployed.
@@ -1117,7 +1117,7 @@ async function applyCmd(args) {
1117
1117
  // the scanner has a false positive that the operator has confirmed
1118
1118
  // is fine. Hits with severity `error` fail the deploy.
1119
1119
  //
1120
- // Scope (GH-409): a `--dir` (Astro SSR build) deploy walks that dir —
1120
+ // Scope: a `--dir` (Astro SSR build) deploy walks that dir —
1121
1121
  // it IS the artifact. A manifest/spec/stdin deploy scans ONLY the
1122
1122
  // on-disk source files the manifest actually references, resolved
1123
1123
  // against the manifest's baseDir. We must NOT walk cwd/src for a
@@ -425,7 +425,7 @@ export function scanSourceTree(srcDir, opts = {}) {
425
425
  /** Scan an explicit list of on-disk file paths — no directory walk.
426
426
  * Used by `run402 deploy apply` for manifest/spec/stdin deploys, where
427
427
  * the artifact is exactly the set of files the manifest references, NOT
428
- * whatever happens to live under cwd/src (GH-409). Files without a
428
+ * whatever happens to live under cwd/src. Files without a
429
429
  * scannable extension are ignored; unreadable files become a WARN
430
430
  * finding (never throw). Returns the combined findings list, sorted by
431
431
  * file + line for stable output, exactly like `scanSourceTree`. */
package/lib/doctor.mjs CHANGED
@@ -27,12 +27,12 @@ import { queueBuzzDoctorTelemetry } from "./diagnostic-telemetry.mjs";
27
27
  import { fail } from "./sdk-errors.mjs";
28
28
  import { normalizeArgv, assertKnownFlags, flagValue } from "./argparse.mjs";
29
29
 
30
- /** Value-taking flags (kychee-com/run402#566 — the flag set doctor actually parses; anything else is BAD_USAGE via assertKnownFlags, never silently ignored). */
30
+ /** Value-taking flags — the flag set doctor actually parses; anything else is BAD_USAGE via assertKnownFlags, never silently ignored. */
31
31
  const DOCTOR_VALUE_FLAGS = ["--scan-dir", "--buzz-agent", "--project", "--only"];
32
32
 
33
33
  /**
34
- * The stable, complete registry of ordinary-mode check names (kychee-com/run402#566,
35
- * the remaining half). One entry per `checks.push({ name: ... })` call below,
34
+ * The stable, complete registry of ordinary-mode check names. One entry
35
+ * per `checks.push({ name: ... })` call below,
36
36
  * in the order each check normally runs. This is the ONE place `--only`
37
37
  * validates its argument against and the ONE place its help text is derived
38
38
  * from, so a check can never be selectable-but-undocumented or
@@ -195,11 +195,10 @@ export async function run(sub, args = []) {
195
195
  console.log(HELP);
196
196
  return;
197
197
  }
198
- // kychee-com/run402#566 (--project half): doctor used to accept ANY flag
199
- // silently an unrecognized one (a typo, or --project before this fix)
200
- // was simply never looked at. Any flag doctor actually parses is listed
201
- // here; anything else is now a structured BAD_USAGE/UNKNOWN_FLAG rejection
202
- // instead of quietly doing nothing.
198
+ // Accepting ANY flag silently means an unrecognized one (a typo) is
199
+ // never looked at. Any flag doctor actually parses is listed here;
200
+ // anything else is a structured BAD_USAGE/UNKNOWN_FLAG rejection instead
201
+ // of quietly doing nothing.
203
202
  assertKnownFlags(all, ["--verbose", "--refresh", "--no-scan", "--buzz", ...DOCTOR_VALUE_FLAGS], DOCTOR_VALUE_FLAGS);
204
203
  const verbose = all.includes("--verbose");
205
204
  const refresh = all.includes("--refresh");
@@ -210,11 +209,10 @@ export async function run(sub, args = []) {
210
209
  // wallet/machine-wide, not per-project.
211
210
  const projectOverride = flagValue(all, "--project");
212
211
 
213
- // kychee-com/run402#566 (the remaining half): --only <check>, repeatable.
214
- // Validated against the stable registry ABOVE the buzz early-return, so an
215
- // unknown name is BAD_USAGE regardless of which mode was also requested —
216
- // the same "every accepted flag must work or BAD_USAGE" bar #569 named for
217
- // doctor's own --human bug.
212
+ // --only <check>, repeatable. Validated against the stable registry
213
+ // ABOVE the buzz early-return, so an unknown name is BAD_USAGE regardless
214
+ // of which mode was also requested — the same "every accepted flag must
215
+ // work or BAD_USAGE" bar as every other doctor flag.
218
216
  const onlyChecks = collectRepeatableFlag(all, "--only");
219
217
  for (const name of onlyChecks) {
220
218
  if (!DOCTOR_CHECK_NAMES.includes(name)) {
@@ -227,9 +225,8 @@ export async function run(sub, args = []) {
227
225
  }
228
226
  }
229
227
  // Buzz mode is a wholly separate, always-complete report shape — an --only
230
- // that named ordinary-mode checks would be silently ignored under --buzz,
231
- // exactly the class of bug #569 flagged for --human. Reject the
232
- // combination instead.
228
+ // that named ordinary-mode checks would be silently ignored under --buzz
229
+ // an accepted-but-inert flag. Reject the combination instead.
233
230
  if (onlyChecks.length > 0 && all.includes("--buzz")) {
234
231
  fail({
235
232
  code: "BAD_USAGE",
@@ -594,8 +591,7 @@ export async function run(sub, args = []) {
594
591
  // deployed is a first-class shape (protocol D183), so its mere absence of a
595
592
  // deploy raises nothing.
596
593
  //
597
- // TARGETING (repo-first-onramp follow-up, kychee-com/run402#559d, extended
598
- // by kychee-com/run402#566's --project half): when cwd is a repository
594
+ // TARGETING: when cwd is a repository
599
595
  // with its own pinned repo id or run402/origin remote, doctor checks THAT
600
596
  // vault, not the profile's active project — the same pin > remote >
601
597
  // RUN402_PROJECT_ID env > active-project order every other gitvault verb
@@ -683,8 +679,30 @@ export async function run(sub, args = []) {
683
679
  // to the durability sentence instead of the terminal-loss claim.
684
680
  covering_recipients: gv.covering_recipients ?? null,
685
681
  daemon: daemonInfo,
682
+ // gitvault-multi-writer (rev 47) task 6.2 — this machine's own
683
+ // standing on the vault's chain-verified writer set. `null` only
684
+ // when there is no vault at all (nothing to be a writer OF).
685
+ // `read_only_vault` takes priority over the caller's own standing
686
+ // — the D228 terminal state blocks EVERY push regardless of who
687
+ // is asking.
688
+ writer: gv.vault === null
689
+ ? null
690
+ : gv.vault.read_only_terminal
691
+ ? "read_only_vault"
692
+ : gv.vault.writer_set?.writers.some((w) => w.writer_key_id === gv.keystore.identity_fingerprint)
693
+ ? "active"
694
+ : gv.vault.pending_writers?.some((p) => p.writer_key_id === gv.keystore.identity_fingerprint)
695
+ ? "pending"
696
+ : "not_admitted",
686
697
  };
687
698
  const gaps = [];
699
+ if (value.writer === "read_only_vault") {
700
+ gaps.push("this vault has lost its last writer (D228 read-only terminal) — it still serves reads, but no push can be admitted until a new writer is admitted through a recovery path");
701
+ } else if (value.writer === "pending") {
702
+ gaps.push("this machine's key is an eligible writer candidate but not yet admitted — run 'run402 repos access sync' if you already hold writer standing on this vault, or ask a current writer to run any gitvault operation");
703
+ } else if (value.writer === "not_admitted") {
704
+ gaps.push("this machine's key is not an active writer on this vault — a push from here is refused GITVAULT_WRITER_NOT_ADMITTED; ask a current writer to admit you (org membership at role developer+ and a published signing key make you eligible)");
705
+ }
688
706
  // The one that actually breaks the next deploy: the project demands a
689
707
  // vaulted capture and THIS machine cannot produce one.
690
708
  if (gv.gitvault_policy === "required" && !gv.keystore.holds_repo_key) {
@@ -698,15 +716,14 @@ export async function run(sub, args = []) {
698
716
  if (gv.pending_overrides > 0) {
699
717
  gaps.push(`${gv.pending_overrides} unvaulted-override journal(s) are still open — run 'run402 repos snapshot' to drain them`);
700
718
  }
701
- // `matches` is a TRI-STATE (kychee-com/run402#562): `false` alone is
719
+ // `matches` is a TRI-STATE: `false` alone is
702
720
  // a real mismatch. `null` (a slug-form remote not yet resolved on
703
- // this machine) is not evidence of anything wrong — `!gv.remote.matches`
704
- // used to treat null the same as false and would have warned here.
721
+ // this machine) is not evidence of anything wrong — a bare
722
+ // `!gv.remote.matches` would treat null the same as false and warn.
705
723
  if (gv.remote && gv.remote.matches === false) {
706
724
  gaps.push(`the '${gv.remote.name}' git remote points at a different project than ${value.project_id} (${gv.remote.url})`);
707
725
  }
708
- // kygit-handoff design D8: the mirror of the OLD `npm i -g @kychee/kygit`
709
- // bug, pointing the other way — a `kygit::` remote with no
726
+ // A `kygit::` remote with no
710
727
  // `git-remote-kygit` helper on PATH means every push/clone/fetch
711
728
  // in this checkout fails inside git with an opaque error.
712
729
  if (gv.remote?.url?.startsWith("kygit::")) {
@@ -786,8 +803,8 @@ export async function run(sub, args = []) {
786
803
  // direct mutation of internal.sessions.authz_version. Hits with severity
787
804
  // `error` block deploy (`run402 deploy` wraps doctor and respects exit
788
805
  // code). Skipped via --no-scan when the user wants config-only checks, and
789
- // by any --only that omits it (kychee-com/run402#566 — this is the check
790
- // that used to bury the gitvault diagnosis under ~1,800 monorepo findings).
806
+ // by any --only that omits it — on a monorepo this check's findings can
807
+ // otherwise bury the gitvault diagnosis under thousands of hits.
791
808
  if (!skipScan && wanted("source_scan")) {
792
809
  try {
793
810
  const scanRoot = scanDirOverride ?? resolveScanRoot(process.cwd());
package/lib/email.mjs CHANGED
@@ -366,9 +366,8 @@ function mailboxIdFromSelector(envelope, selector, flag) {
366
366
  if (/^mbx_/.test(selector)) return selector;
367
367
  const hit = (envelope.mailboxes ?? []).find((m) => m.mailbox_id === selector || m.slug === selector);
368
368
  if (!hit) {
369
- // A selector that matches nothing is a value we know nothing about see
370
- // core-dist/redact.js's doc comment (kychee-com/run402-private#640)
371
- // so it must not be echoed verbatim.
369
+ // A selector that matches nothing is a value we know nothing about (see
370
+ // core-dist/redact.js's doc comment), so it must not be echoed verbatim.
372
371
  fail({
373
372
  code: "MAILBOX_NOT_FOUND",
374
373
  message: `No mailbox matching ${JSON.stringify(describeRejectedValue(selector))} for ${flag}.`,
@@ -11,8 +11,7 @@
11
11
  * long its TTL has silently decayed, so a session that can re-derive its OWN
12
12
  * key independently of any local file needs no cache at all to pick up where
13
13
  * it left off. Two genuinely concurrent sessions must NEVER derive the same
14
- * key — that would be the #663 regression this design exists to avoid (see
15
- * run402-private's agent-presence.ts, resolvePresence's own comment) — so
14
+ * key — that is the regression this design exists to avoid — so
16
15
  * every source below is either a value the harness itself guarantees is
17
16
  * unique per session, or a value generated here and persisted only for THIS
18
17
  * checkout.
package/lib/init.mjs CHANGED
@@ -404,7 +404,7 @@ export async function run(args = []) {
404
404
  const data = await res.json();
405
405
  if (data.result) {
406
406
  // Tempo faucet is "instant" on-chain, but the client RPC read can be
407
- // racy relative to faucet settlement — poll up to 30s (GH-81), mirroring
407
+ // racy relative to faucet settlement — poll up to 30s, mirroring
408
408
  // the x402 path below.
409
409
  for (let i = 0; i < 30; i++) {
410
410
  await new Promise(r => setTimeout(r, 1000));
@@ -70,7 +70,7 @@ export function deployAction() {
70
70
  /**
71
71
  * `repos create` (and `gitvault init`) on an org with no claimed slug: the
72
72
  * response's `address: null` had no pointer to WHY, or to the named-addressing
73
- * feature at all (kychee-com/run402#560). Owner-only. Wording deliberately
73
+ * feature at all. Owner-only. Wording deliberately
74
74
  * omits a price: the first slug per org is free, and renames/re-claims cost
75
75
  * $1 — a gateway change lands the fee separately, so this stays true under
76
76
  * both the old and new pricing rule.
@@ -39,7 +39,7 @@
39
39
  * different organization than the caller named).
40
40
  */
41
41
  import { getSdk } from "./sdk.mjs";
42
- import { flagValue } from "./argparse.mjs";
42
+ import { flagValue, positionalArgs } from "./argparse.mjs";
43
43
  import { findBindingKey } from "./wallet-context.mjs";
44
44
  import { fail } from "./sdk-errors.mjs";
45
45
  import { nextAction } from "./next-actions.mjs";
@@ -69,7 +69,7 @@ const trimmed = (v) => (typeof v === "string" && v.trim() ? v.trim() : null);
69
69
  * NEVER checked here — only that the value could be an org id at all.
70
70
  *
71
71
  * A rejected value is a value we know nothing about — see
72
- * core-dist/redact.js's doc comment (kychee-com/run402-private#640): the
72
+ * core-dist/redact.js's doc comment: the
73
73
  * same class of mistake that put a private key into RUN402_WALLET can put
74
74
  * one into RUN402_ORG / --org, so this must never echo the raw value.
75
75
  */
@@ -290,6 +290,87 @@ export async function resolveOrgId(a, opts = {}) {
290
290
  return resolved ? resolved.orgId : null;
291
291
  }
292
292
 
293
+ /**
294
+ * The org-scoped POSITIONAL contract (cli-org-context, "One Resolver Serves
295
+ * Every Org-Scoped Command").
296
+ *
297
+ * `<org_id>` is optional sugar on every verb that acts on an organization —
298
+ * `org get`, `org member add`, `billing link-wallet`, … A leading positional
299
+ * that IS an org id (a UUID) addresses that org; anything else is the verb's
300
+ * own next positional (a wallet, an email, a principal) and the org comes from
301
+ * the shared chain: `--org`, then `RUN402_ORG`, then the `.run402.json`
302
+ * binding, then `org use`. So inside a bound checkout the two-agent case reads
303
+ * `run402 org member add 0xB… --role developer` with nothing else to know.
304
+ *
305
+ * Naming the org twice with different values (positional AND `--org`) is
306
+ * `AMBIGUOUS_ORG`, never a silent pick — the same refusal the env-vs-binding
307
+ * pair gets. A non-org first positional with no chain answer fails
308
+ * `ORG_REQUIRED` and NAMES the rejected value, so `org get foo` says why `foo`
309
+ * did not count rather than reporting a bare "no organization".
310
+ *
311
+ * Returns the org id with provenance plus the REMAINING positionals; bound
312
+ * those with {@link requireRest} using the verb's own usage line. Enforced
313
+ * mechanically: every `orgScoped` entry in the command manifest is driven
314
+ * through this shape by `cli-conventions-gate.test.mjs`.
315
+ *
316
+ * @param {string[]} a normalized argv
317
+ * @param {string[]} valueFlags flags that take a value (must include "--org")
318
+ * @param {object} [opts] forwarded to {@link resolveOrg} (cmd, env, cwd)
319
+ */
320
+ export async function takeOrgPositional(a, valueFlags = [], opts = {}) {
321
+ const positionals = positionalArgs(a, valueFlags);
322
+ const first = positionals[0];
323
+ if (typeof first === "string" && ORG_ID_RE.test(first)) {
324
+ const flag = trimmed(flagValue(a, "--org"));
325
+ if (flag && flag.toLowerCase() !== first.toLowerCase()) {
326
+ fail({
327
+ code: "AMBIGUOUS_ORG",
328
+ message: `Ambiguous organization: positional ${first} but --org ${flag}.`,
329
+ hint: "Name the organization once — as the leading <org_id> positional or as --org <org_id>, not both.",
330
+ details: {
331
+ candidates: [
332
+ { org_id: first, source: "positional", source_detail: "<org_id>" },
333
+ { org_id: flag, source: "flag", source_detail: "--org" },
334
+ ],
335
+ },
336
+ });
337
+ }
338
+ return { orgId: first, rest: positionals.slice(1), source: "positional", sourceDetail: "<org_id>" };
339
+ }
340
+ const resolved = await resolveOrg(a, { ...opts, optional: true });
341
+ if (resolved) return { orgId: resolved.orgId, rest: positionals, source: resolved.source, sourceDetail: resolved.sourceDetail };
342
+ fail({
343
+ code: "ORG_REQUIRED",
344
+ message: typeof first === "string"
345
+ ? `No organization specified and no current organization set (${JSON.stringify(first)} is not an org_id — an org_id is a UUID).`
346
+ : "No organization specified and no current organization set.",
347
+ hint: `Pass --org <org_id> (or a leading <org_id> positional), set ${ORG_ENV}, bind this directory in .run402.json, or run: run402 org use <org_id>`,
348
+ ...(typeof first === "string" ? { details: { rejected_positional: first } } : {}),
349
+ next_actions: orgRequiredActions(),
350
+ });
351
+ return null; // unreachable — fail() exits
352
+ }
353
+
354
+ /**
355
+ * Bound the positionals LEFT after {@link takeOrgPositional} took the org —
356
+ * the same `BAD_USAGE` shapes `requirePositionalCount` emits, on an array the
357
+ * caller already holds.
358
+ */
359
+ export function requireRest(rest, opts = {}) {
360
+ const { min = 0, max = min, command = "command", missing = "Missing required argument." } = opts;
361
+ if (rest.length < min) {
362
+ fail({ code: "BAD_USAGE", message: missing, hint: command });
363
+ }
364
+ if (rest.length > max) {
365
+ fail({
366
+ code: "BAD_USAGE",
367
+ message: `Unexpected argument for ${command}: ${rest[max]}`,
368
+ hint: `Use \`${command}\`.`,
369
+ });
370
+ }
371
+ return rest;
372
+ }
373
+
293
374
  /** Validate an org id supplied by a human, naming the origin. Throws via fail(). */
294
375
  export function requireOrgIdShape(orgId, origin = "--org") {
295
376
  return assertOrgIdShape(orgId, origin);