run402 4.97.0 → 4.98.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 (163) hide show
  1. package/README.md +1 -1
  2. package/core-dist/binding-file.js +28 -1
  3. package/core-dist/wallet.js +3 -3
  4. package/lib/auth.mjs +1 -2
  5. package/lib/buzz-doctor.mjs +21 -806
  6. package/lib/buzz-doctor.test.mjs +7 -6
  7. package/lib/buzz.test.mjs +13 -2
  8. package/lib/doctor.mjs +72 -811
  9. package/lib/init.mjs +35 -576
  10. package/lib/org-context.mjs +39 -376
  11. package/lib/orgs.mjs +22 -106
  12. package/lib/projects.mjs +1 -2
  13. package/lib/repos.mjs +6 -6
  14. package/lib/rooms.mjs +1 -2
  15. package/lib/sdk-errors.mjs +26 -0
  16. package/lib/status.mjs +14 -154
  17. package/lib/up.mjs +1 -2
  18. package/lib/vault-scaffold.mjs +2 -3
  19. package/lib/vault-target.mjs +14 -141
  20. package/lib/wallet-context.mjs +61 -227
  21. package/lib/wallets.mjs +47 -310
  22. package/package.json +1 -2
  23. package/sdk/core-dist/binding-file.js +28 -1
  24. package/sdk/core-dist/wallet.js +3 -3
  25. package/sdk/dist/errors.d.ts +24 -2
  26. package/sdk/dist/errors.d.ts.map +1 -1
  27. package/sdk/dist/errors.js +44 -2
  28. package/sdk/dist/errors.js.map +1 -1
  29. package/sdk/dist/index.d.ts +18 -3
  30. package/sdk/dist/index.d.ts.map +1 -1
  31. package/sdk/dist/index.js +12 -1
  32. package/sdk/dist/index.js.map +1 -1
  33. package/sdk/dist/kernel.d.ts +34 -0
  34. package/sdk/dist/kernel.d.ts.map +1 -1
  35. package/sdk/dist/kernel.js +15 -1
  36. package/sdk/dist/kernel.js.map +1 -1
  37. package/sdk/dist/namespaces/admin.d.ts.map +1 -1
  38. package/sdk/dist/namespaces/admin.js +8 -0
  39. package/sdk/dist/namespaces/admin.js.map +1 -1
  40. package/sdk/dist/namespaces/agent.d.ts +2 -2
  41. package/sdk/dist/namespaces/agent.d.ts.map +1 -1
  42. package/sdk/dist/namespaces/agent.js +4 -0
  43. package/sdk/dist/namespaces/agent.js.map +1 -1
  44. package/sdk/dist/namespaces/agent.types.d.ts +5 -0
  45. package/sdk/dist/namespaces/agent.types.d.ts.map +1 -1
  46. package/sdk/dist/namespaces/apps.d.ts.map +1 -1
  47. package/sdk/dist/namespaces/apps.js +2 -0
  48. package/sdk/dist/namespaces/apps.js.map +1 -1
  49. package/sdk/dist/namespaces/auth.d.ts.map +1 -1
  50. package/sdk/dist/namespaces/auth.js +9 -0
  51. package/sdk/dist/namespaces/auth.js.map +1 -1
  52. package/sdk/dist/namespaces/branches.d.ts.map +1 -1
  53. package/sdk/dist/namespaces/branches.js +2 -0
  54. package/sdk/dist/namespaces/branches.js.map +1 -1
  55. package/sdk/dist/namespaces/ci.d.ts.map +1 -1
  56. package/sdk/dist/namespaces/ci.js +2 -0
  57. package/sdk/dist/namespaces/ci.js.map +1 -1
  58. package/sdk/dist/namespaces/credentials.d.ts +0 -23
  59. package/sdk/dist/namespaces/credentials.d.ts.map +1 -1
  60. package/sdk/dist/namespaces/credentials.js +29 -0
  61. package/sdk/dist/namespaces/credentials.js.map +1 -1
  62. package/sdk/dist/namespaces/escalations.d.ts.map +1 -1
  63. package/sdk/dist/namespaces/escalations.js +2 -0
  64. package/sdk/dist/namespaces/escalations.js.map +1 -1
  65. package/sdk/dist/namespaces/grants.d.ts.map +1 -1
  66. package/sdk/dist/namespaces/grants.js +4 -0
  67. package/sdk/dist/namespaces/grants.js.map +1 -1
  68. package/sdk/dist/namespaces/live.d.ts +0 -16
  69. package/sdk/dist/namespaces/live.d.ts.map +1 -1
  70. package/sdk/dist/namespaces/live.js +19 -0
  71. package/sdk/dist/namespaces/live.js.map +1 -1
  72. package/sdk/dist/namespaces/me.d.ts.map +1 -1
  73. package/sdk/dist/namespaces/me.js +3 -0
  74. package/sdk/dist/namespaces/me.js.map +1 -1
  75. package/sdk/dist/namespaces/org.d.ts.map +1 -1
  76. package/sdk/dist/namespaces/org.js +3 -0
  77. package/sdk/dist/namespaces/org.js.map +1 -1
  78. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  79. package/sdk/dist/namespaces/projects.js +5 -0
  80. package/sdk/dist/namespaces/projects.js.map +1 -1
  81. package/sdk/dist/namespaces/repos.d.ts.map +1 -1
  82. package/sdk/dist/namespaces/repos.js +5 -0
  83. package/sdk/dist/namespaces/repos.js.map +1 -1
  84. package/sdk/dist/namespaces/rooms.d.ts.map +1 -1
  85. package/sdk/dist/namespaces/rooms.js +3 -0
  86. package/sdk/dist/namespaces/rooms.js.map +1 -1
  87. package/sdk/dist/namespaces/session.d.ts.map +1 -1
  88. package/sdk/dist/namespaces/session.js +18 -0
  89. package/sdk/dist/namespaces/session.js.map +1 -1
  90. package/sdk/dist/namespaces/transfers.d.ts.map +1 -1
  91. package/sdk/dist/namespaces/transfers.js +3 -0
  92. package/sdk/dist/namespaces/transfers.js.map +1 -1
  93. package/sdk/dist/namespaces/wallets.d.ts.map +1 -1
  94. package/sdk/dist/namespaces/wallets.js +2 -0
  95. package/sdk/dist/namespaces/wallets.js.map +1 -1
  96. package/sdk/dist/namespaces/write-approval.d.ts.map +1 -1
  97. package/sdk/dist/namespaces/write-approval.js +3 -0
  98. package/sdk/dist/namespaces/write-approval.js.map +1 -1
  99. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  100. package/sdk/dist/node/actions-node.js +3 -0
  101. package/sdk/dist/node/actions-node.js.map +1 -1
  102. package/sdk/dist/node/buzz-doctor-contract.d.ts +155 -0
  103. package/sdk/dist/node/buzz-doctor-contract.d.ts.map +1 -0
  104. package/sdk/dist/node/buzz-doctor-contract.js +385 -0
  105. package/sdk/dist/node/buzz-doctor-contract.js.map +1 -0
  106. package/sdk/dist/node/buzz-doctor.d.ts +155 -0
  107. package/sdk/dist/node/buzz-doctor.d.ts.map +1 -0
  108. package/sdk/dist/node/buzz-doctor.js +810 -0
  109. package/sdk/dist/node/buzz-doctor.js.map +1 -0
  110. package/sdk/dist/node/credentials.d.ts +9 -4
  111. package/sdk/dist/node/credentials.d.ts.map +1 -1
  112. package/sdk/dist/node/credentials.js +1 -1
  113. package/sdk/dist/node/credentials.js.map +1 -1
  114. package/sdk/dist/node/diagnostics.d.ts +45 -0
  115. package/sdk/dist/node/diagnostics.d.ts.map +1 -0
  116. package/sdk/dist/node/diagnostics.js +67 -0
  117. package/sdk/dist/node/diagnostics.js.map +1 -0
  118. package/sdk/dist/node/doctor.d.ts +101 -0
  119. package/sdk/dist/node/doctor.d.ts.map +1 -0
  120. package/sdk/dist/node/doctor.js +618 -0
  121. package/sdk/dist/node/doctor.js.map +1 -0
  122. package/sdk/dist/node/index.d.ts +57 -5
  123. package/sdk/dist/node/index.d.ts.map +1 -1
  124. package/sdk/dist/node/index.js +36 -1
  125. package/sdk/dist/node/index.js.map +1 -1
  126. package/sdk/dist/node/init.d.ts +137 -0
  127. package/sdk/dist/node/init.d.ts.map +1 -0
  128. package/sdk/dist/node/init.js +507 -0
  129. package/sdk/dist/node/init.js.map +1 -0
  130. package/sdk/dist/node/lightning-wallet.d.ts +61 -0
  131. package/sdk/dist/node/lightning-wallet.d.ts.map +1 -0
  132. package/sdk/dist/node/lightning-wallet.js +126 -0
  133. package/sdk/dist/node/lightning-wallet.js.map +1 -0
  134. package/sdk/dist/node/local-actions.d.ts +23 -0
  135. package/sdk/dist/node/local-actions.d.ts.map +1 -0
  136. package/sdk/dist/node/local-actions.js +38 -0
  137. package/sdk/dist/node/local-actions.js.map +1 -0
  138. package/sdk/dist/node/org-context.d.ts +169 -0
  139. package/sdk/dist/node/org-context.d.ts.map +1 -0
  140. package/sdk/dist/node/org-context.js +403 -0
  141. package/sdk/dist/node/org-context.js.map +1 -0
  142. package/sdk/dist/node/status.d.ts +62 -0
  143. package/sdk/dist/node/status.d.ts.map +1 -0
  144. package/sdk/dist/node/status.js +132 -0
  145. package/sdk/dist/node/status.js.map +1 -0
  146. package/sdk/dist/node/vault-target.d.ts +49 -0
  147. package/sdk/dist/node/vault-target.d.ts.map +1 -0
  148. package/sdk/dist/node/vault-target.js +109 -0
  149. package/sdk/dist/node/vault-target.js.map +1 -0
  150. package/sdk/dist/node/wallets.d.ts +242 -0
  151. package/sdk/dist/node/wallets.d.ts.map +1 -0
  152. package/sdk/dist/node/wallets.js +484 -0
  153. package/sdk/dist/node/wallets.js.map +1 -0
  154. package/sdk/dist/secret-gate.d.ts +377 -0
  155. package/sdk/dist/secret-gate.d.ts.map +1 -0
  156. package/sdk/dist/secret-gate.js +273 -0
  157. package/sdk/dist/secret-gate.js.map +1 -0
  158. package/vault-surface.json +1 -1
  159. package/fixtures/buzz-v0.5.2-cli-capabilities.json +0 -92
  160. package/fixtures/run402-buzz-doctor-v1-contract.json +0 -140
  161. package/lib/buzz-doctor-contract.mjs +0 -97
  162. package/lib/buzz-doctor-contract.test.mjs +0 -134
  163. package/lib/lightning-wallet.mjs +0 -115
@@ -1,182 +1,42 @@
1
1
  /**
2
- * Shared organization resolution for org-scoped CLI families
3
- * (attention-architecture Wave E — one resolver, not one per family;
4
- * extended by add-cli-current-org with the local-context classes).
5
- *
6
- * The org is the ownership root — it owns projects, holds memberships, and is
7
- * the scope for rooms, escalations, members, grants, and audit. This is the one
8
- * place org precedence is expressed.
9
- *
10
- * Precedence is four INTENT CLASSES, highest first, and inside each class an
11
- * organization named directly outranks one derived from a project named in that
12
- * same class:
13
- *
14
- * 1. flag --org <org_id> else --project <project_id> -> its org
15
- * 2. environment RUN402_ORG, else the org else RUN402_PROJECT_ID -> its org
16
- * half of RUN402_ROOM
17
- * 3. binding the `org` key of the nearest .run402(.local).json
18
- * 4. profile state the profile's selected org else its active project -> org
19
- *
20
- * The class rule is what makes `--project X` behave: naming a project IS naming
21
- * its organization, so a stale profile selection must not outrank it. Classes 3
22
- * and 4 are what let an org that owns NO project be addressed at all — every
23
- * earlier rung needed an argument, a variable, or a deployed project.
2
+ * Organization resolution for org-scoped CLI families: the CLI edge of
3
+ * `r.orgs.resolve()` (`@run402/sdk/node`), which owns the precedence chain —
4
+ * flag (`--org`, then `--project`), environment (`RUN402_ORG`, the org half of
5
+ * `RUN402_ROOM`, then `RUN402_PROJECT_ID`), the `.run402.json` binding, this
6
+ * checkout's vault pin, then profile state (`orgs use`, then the active
7
+ * project). This module adds only what is CLI-shaped: reading the flags out of
8
+ * argv, the leading `<org_id>` positional contract, the per-command conflict
9
+ * exemption, and printing a refusal as the CLI's error envelope.
24
10
  *
25
11
  * `rooms`/`claims` resolve a PAIR ({orgId, roomKey}) with their own
26
- * `RUN402_ROOM` env form — see `rooms-context.mjs`. They are not forced through
27
- * this single-value resolver: the pair keeps its own shape and its own
28
- * precedence, and only the ORG HALF of its fallback delegates here. One
29
- * resolver per shape, composed for the half they share.
30
- *
31
- * Two things this deliberately does NOT do:
32
- * - Infer the org from the caller's memberships, at any count. Membership is
33
- * server state that changes without the caller acting, so a heuristic that is
34
- * right today silently changes meaning the day they are invited elsewhere.
35
- * - Validate membership locally. A well-formed id goes to the server and the
36
- * server's answer is surfaced as returned — never rewritten into a local
37
- * not-found (that would invent the existence oracle the gateway refuses to
38
- * provide) and never retried against a lower class (that would act on a
39
- * different organization than the caller named).
12
+ * `RUN402_ROOM` form — see `rooms-context.mjs`; only the org half of their
13
+ * fallback delegates here.
40
14
  */
41
15
  import { getSdk } from "./sdk.mjs";
42
16
  import { flagValue, positionalArgs } from "./argparse.mjs";
43
- import { findBindingKey } from "./wallet-context.mjs";
44
- import { fail } from "./sdk-errors.mjs";
45
- import { nextAction } from "./next-actions.mjs";
46
- import { describeRejectedValue } from "../core-dist/redact.js";
47
- import {
48
- getActiveOrgId as coreGetActiveOrgId,
49
- setActiveOrgId as coreSetActiveOrgId,
50
- clearActiveOrgId as coreClearActiveOrgId,
51
- } from "../core-dist/profile-state.js";
52
- import { getActiveProjectId } from "../core-dist/keystore.js";
53
- import { getProject, resolveProjectId, updateProject } from "./config.mjs";
17
+ import { fail, failLocal } from "./sdk-errors.mjs";
18
+ import { ORG_ID_RE, assertOrgIdShape, orgProvenance, orgRequiredError } from "#sdk/node";
54
19
 
55
- export const ORG_ENV = "RUN402_ORG";
56
- export const ROOM_ENV = "RUN402_ROOM";
57
- export const PROJECT_ENV = "RUN402_PROJECT_ID";
20
+ export { ORG_ENV, ROOM_ENV, PROJECT_ENV, orgProvenance } from "#sdk/node";
58
21
 
59
22
  /** Command groups that must stay usable while selection is ambiguous. */
60
23
  const CONFLICT_EXEMPT = new Set(["orgs", "wallets", "doctor"]);
61
24
 
62
- /** `org_id` is a UUID at every API boundary (`uuidParam` on every route). */
63
- const ORG_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
25
+ /** The refusals the resolver itself raises; anything else is the server's answer. */
26
+ const RESOLVER_CODES = new Set(["BAD_ORG_ID", "AMBIGUOUS_ORG", "ORG_REQUIRED"]);
64
27
 
65
28
  const trimmed = (v) => (typeof v === "string" && v.trim() ? v.trim() : null);
66
29
 
67
- /**
68
- * Shape-validate an organization id supplied by a local source. Membership is
69
- * NEVER checked here — only that the value could be an org id at all.
70
- *
71
- * A rejected value is a value we know nothing about — see
72
- * core-dist/redact.js's doc comment: the
73
- * same class of mistake that put a private key into RUN402_WALLET can put
74
- * one into RUN402_ORG / --org, so this must never echo the raw value.
75
- */
76
- function assertOrgIdShape(orgId, origin) {
77
- if (ORG_ID_RE.test(orgId)) return orgId;
78
- fail({
79
- code: "BAD_ORG_ID",
80
- message: `Invalid organization id ${JSON.stringify(describeRejectedValue(orgId))} (from ${origin}).`,
81
- hint: "An org_id is a UUID. Run 'run402 orgs list' to see the organizations you belong to.",
82
- details: { org_id: describeRejectedValue(orgId), origin },
83
- next_actions: [listOrgsAction()],
84
- });
85
- }
86
-
87
- /** The org half of `RUN402_ROOM=<org_id>/<room_key>`, or null. */
88
- function orgFromRoomEnv(env) {
89
- const raw = trimmed(env[ROOM_ENV]);
90
- if (!raw) return null;
91
- const slash = raw.indexOf("/");
92
- if (slash <= 0 || slash === raw.length - 1) return null; // rooms-context reports the malformed form
93
- return raw.slice(0, slash);
94
- }
95
-
96
- /**
97
- * `r402.orgId` from `cwd`'s LOCAL git config (kygit-handoff design D10) —
98
- * best-effort, gracefully degrading like `findBindingKey`: no repository,
99
- * no pin, or a shape-invalid value all answer `null` rather than throwing,
100
- * so a bare directory or a checkout with no vault remote costs nothing.
101
- * Checks a real vault checkout's pin first (gated on `r402.repoId`), then
102
- * falls back to the bare org/room pin a room-only `rooms join <key>` writes
103
- * in a directory with no vault at all (add-room-invite design D10).
104
- */
105
- async function readVaultPinnedOrgId(cwd) {
106
- try {
107
- const { readPinnedVaultRepo, readPinnedRoomBinding } = await import("#sdk/node");
108
- const pinned = await readPinnedVaultRepo(cwd);
109
- const orgId = trimmed(pinned?.org_id);
110
- if (orgId && ORG_ID_RE.test(orgId)) return orgId;
111
- const bare = await readPinnedRoomBinding(cwd);
112
- const bareOrgId = trimmed(bare?.org_id);
113
- return bareOrgId && ORG_ID_RE.test(bareOrgId) ? bareOrgId : null;
114
- } catch {
115
- return null;
116
- }
117
- }
118
-
119
- function listOrgsAction() {
120
- return nextAction("edit_request", {
121
- command: "run402 orgs list",
122
- why: "List the organizations this wallet belongs to.",
123
- });
124
- }
125
-
126
- function orgRequiredActions() {
127
- return [
128
- nextAction("edit_request", {
129
- command: "run402 orgs use <org_id>",
130
- why: "Select a current organization for this wallet profile.",
131
- }),
132
- listOrgsAction(),
133
- nextAction("edit_request", {
134
- command: "run402 <command> --org <org_id>",
135
- why: "Name the organization on this one call.",
136
- }),
137
- nextAction("edit_request", {
138
- command: "export RUN402_ORG=<org_id>",
139
- why: "Name the organization for every call in this shell (harness wiring).",
140
- }),
141
- nextAction("edit_request", {
142
- command: `echo '{"org":"<org_id>"}' > .run402.json`,
143
- why: "Bind this checkout to an organization so every agent in it inherits the same one.",
144
- }),
145
- nextAction("edit_request", {
146
- command: "run402 projects use <project_id>",
147
- why: "Select a project; its owning organization becomes the current one.",
148
- }),
149
- ];
150
- }
151
-
152
- /** Resolve a project's owning org (one GET). Returns null when unresolvable. */
153
- async function orgOfProject(projectId, { required }) {
154
- // Upstream's per-project cache answers the same question for free when it is
155
- // warm. It is NOT used as the whole implementation: it returns null on
156
- // failure, and an explicitly named project must be able to HARD STOP rather
157
- // than fall through to a lower class.
158
- const cached = getProject(projectId)?.org_id;
159
- if (typeof cached === "string" && cached.length > 0) return cached;
160
- try {
161
- const scoped = await getSdk().rooms.forProject(projectId);
162
- return scoped.orgId;
163
- } catch (err) {
164
- // An EXPLICITLY named project that cannot be resolved is a hard stop — the
165
- // caller named it, so falling to a lower class would act on a different
166
- // organization than the one they asked for. An implicitly selected project
167
- // (profile state) may simply be stale; skip the class.
168
- if (!required) return null;
169
- throw err;
170
- }
30
+ /** Print a resolver refusal as the CLI envelope; rethrow a server or network error. */
31
+ function failResolver(err) {
32
+ if (err?.kind === "local_error" && RESOLVER_CODES.has(err?.code)) failLocal(err);
33
+ throw err;
171
34
  }
172
35
 
173
36
  /**
174
37
  * Resolve the organization an org-scoped command acts on.
175
38
  *
176
- * Accepts either normalized argv (the Wave E call shape, still used by
177
- * `escalations`) or an options object for callers that already hold values.
178
- *
179
- * @param {string[]|object} input normalized argv, or { org, project }
39
+ * @param {string[]|object} input normalized argv (`--org`, `--project` read from it), or { org, project }
180
40
  * @param {object} [opts]
181
41
  * @param {string} [opts.cmd] command group, for the conflict exemption
182
42
  * @param {object} [opts.env] environment (injectable for tests)
@@ -188,136 +48,30 @@ export async function resolveOrg(input = {}, opts = {}) {
188
48
  const { cmd, env = process.env, cwd = process.cwd(), optional = false } = opts;
189
49
  const org = Array.isArray(input) ? flagValue(input, "--org") : input.org;
190
50
  const project = Array.isArray(input) ? flagValue(input, "--project") : input.project;
191
-
192
- // --- Class 1: flag -------------------------------------------------------
193
- const orgFlag = trimmed(org);
194
- if (orgFlag) {
195
- return { orgId: assertOrgIdShape(orgFlag, "--org"), source: "flag", sourceDetail: "--org" };
196
- }
197
- const projectFlag = trimmed(project);
198
- if (projectFlag) {
199
- const orgId = await orgOfProject(projectFlag, { required: true });
200
- if (orgId) return { orgId, source: "flag", sourceDetail: "--project" };
201
- }
202
-
203
- // --- Class 2 and 3: environment vs binding (the conflict pair) ------------
204
- const bindingHit = findBindingKey(cwd, "org");
205
- const bindingOrg = bindingHit ? assertOrgIdShape(bindingHit.value, bindingHit.file) : null;
206
-
207
- const envDirect = trimmed(env[ORG_ENV]);
208
- const envRoomOrg = envDirect ? null : orgFromRoomEnv(env);
209
- const envProject = envDirect || envRoomOrg ? null : trimmed(env[PROJECT_ENV]);
210
-
211
- let envOrg = null;
212
- let envDetail = null;
213
- if (envDirect) {
214
- envOrg = assertOrgIdShape(envDirect, ORG_ENV);
215
- envDetail = ORG_ENV;
216
- } else if (envRoomOrg) {
217
- envOrg = assertOrgIdShape(envRoomOrg, ROOM_ENV);
218
- envDetail = ROOM_ENV;
219
- } else if (envProject && bindingOrg) {
220
- // Only worth a lookup when a binding exists to disagree with; otherwise the
221
- // env class wins uncontested and the lookup happens once, below.
222
- envOrg = await orgOfProject(envProject, { required: false });
223
- envDetail = PROJECT_ENV;
224
- }
225
-
226
- if (envOrg && bindingOrg && envOrg !== bindingOrg && !CONFLICT_EXEMPT.has(cmd)) {
227
- // The wallet tier's rule, scoped to the same pair. A binding deliberately
228
- // declared by a checkout outranks the profile's selection SILENTLY — that
229
- // is what a binding is for — but an ambient env var disagreeing with a
230
- // committed file is exactly the surprise worth stopping on.
231
- fail({
232
- code: "AMBIGUOUS_ORG",
233
- message: `Ambiguous organization: ${envDetail}=${envOrg} but ${bindingHit.file} binds ${bindingOrg}.`,
234
- hint: `Resolve with one of: pass --org <org_id>, unset ${envDetail}, or edit the binding file.`,
235
- details: {
236
- candidates: [
237
- { org_id: envOrg, source: "env", source_detail: envDetail },
238
- { org_id: bindingOrg, source: "binding", source_detail: bindingHit.file },
239
- ],
240
- },
241
- next_actions: [
242
- nextAction("edit_request", {
243
- command: "run402 <command> --org <org_id>",
244
- why: "The flag resolves the conflict for this call.",
245
- }),
246
- ],
247
- });
248
- }
249
-
250
- if (envOrg) return { orgId: envOrg, source: "env", sourceDetail: envDetail };
251
- if (envProject) {
252
- const orgId = await orgOfProject(envProject, { required: false });
253
- if (orgId) return { orgId, source: "env", sourceDetail: PROJECT_ENV };
254
- }
255
- if (bindingOrg) return { orgId: bindingOrg, source: "binding", sourceDetail: bindingHit.file };
256
-
257
- // --- Class 3.5: vault local pin (kygit-handoff design D10) ------------
258
- // `r402.orgId` in this checkout's LOCAL git config — written by every
259
- // vault pin site (`repos create`, `resume`, address resolution), a
260
- // rung below env/binding and above profile state: a checkout that IS a
261
- // resumed/pinned vault should resolve its own org with zero configuration,
262
- // but an explicit env var or a committed `.run402.json` binding still wins.
263
- const pinnedOrg = await readVaultPinnedOrgId(cwd);
264
- if (pinnedOrg) return { orgId: pinnedOrg, source: "vault_pin", sourceDetail: "r402.orgId (local git config)" };
265
-
266
- // --- Class 4: profile state ----------------------------------------------
267
- const selected = trimmed(coreGetActiveOrgId());
268
- if (selected) {
269
- return { orgId: assertOrgIdShape(selected, "orgs use"), source: "profile", sourceDetail: "orgs use" };
270
- }
271
- const activeProject = trimmed(getActiveProjectId());
272
- if (activeProject) {
273
- const orgId = await orgOfProject(activeProject, { required: false });
274
- if (orgId) return { orgId, source: "profile", sourceDetail: "projects use" };
51
+ try {
52
+ return await getSdk().orgs.resolve({ org, project }, { env, cwd, optional, allowConflict: CONFLICT_EXEMPT.has(cmd) });
53
+ } catch (err) {
54
+ return failResolver(err);
275
55
  }
276
-
277
- if (optional) return null;
278
- fail({
279
- code: "ORG_REQUIRED",
280
- message: "No organization specified and no current organization set.",
281
- hint: `Pass --org <org_id>, set ${ORG_ENV}, bind this directory in .run402.json, or run: run402 orgs use <org_id>`,
282
- next_actions: orgRequiredActions(),
283
- });
284
- return null; // unreachable — fail() exits
285
56
  }
286
57
 
287
- /**
288
- * Resolve the addressed organization id from normalized argv.
289
- *
290
- * The Wave E signature, preserved: callers that only need the id keep passing
291
- * argv and getting a string back. Use {@link resolveOrg} when the provenance
292
- * pair is wanted too.
293
- */
58
+ /** The addressed organization id from normalized argv (see {@link resolveOrg}). */
294
59
  export async function resolveOrgId(a, opts = {}) {
295
60
  const resolved = await resolveOrg(a, opts);
296
61
  return resolved ? resolved.orgId : null;
297
62
  }
298
63
 
299
64
  /**
300
- * The org-scoped POSITIONAL contract (cli-org-context, "One Resolver Serves
301
- * Every Org-Scoped Command").
302
- *
303
- * `<org_id>` is optional sugar on every verb that acts on an organization —
304
- * `orgs get`, `orgs members add`, `billing link-wallet`, … A leading positional
305
- * that IS an org id (a UUID) addresses that org; anything else is the verb's
306
- * own next positional (a wallet, an email, a principal) and the org comes from
307
- * the shared chain: `--org`, then `RUN402_ORG`, then the `.run402.json`
308
- * binding, then `orgs use`. So inside a bound checkout the two-agent case reads
309
- * `run402 orgs members add 0xB… --role developer` with nothing else to know.
310
- *
311
- * Naming the org twice with different values (positional AND `--org`) is
312
- * `AMBIGUOUS_ORG`, never a silent pick — the same refusal the env-vs-binding
313
- * pair gets. A non-org first positional with no chain answer fails
314
- * `ORG_REQUIRED` and NAMES the rejected value, so `orgs get foo` says why `foo`
315
- * did not count rather than reporting a bare "no organization".
65
+ * The org-scoped POSITIONAL contract: `<org_id>` is optional sugar on every
66
+ * verb that acts on an organization. A leading positional that IS an org id
67
+ * addresses that org; anything else is the verb's own next positional and the
68
+ * org comes from the chain. Naming the org twice with different values
69
+ * (positional AND `--org`) is `AMBIGUOUS_ORG`; a non-org first positional with
70
+ * no chain answer fails `ORG_REQUIRED` naming the rejected value.
316
71
  *
317
72
  * Returns the org id with provenance plus the REMAINING positionals; bound
318
- * those with {@link requireRest} using the verb's own usage line. Enforced
319
- * mechanically: every `orgScoped` entry in the command manifest is driven
320
- * through this shape by `cli-conventions-gate.test.mjs`.
73
+ * those with {@link requireRest}. Every `orgScoped` entry in the command
74
+ * manifest is driven through this shape by `cli-conventions-gate.test.mjs`.
321
75
  *
322
76
  * @param {string[]} a normalized argv
323
77
  * @param {string[]} valueFlags flags that take a value (must include "--org")
@@ -345,16 +99,8 @@ export async function takeOrgPositional(a, valueFlags = [], opts = {}) {
345
99
  }
346
100
  const resolved = await resolveOrg(a, { ...opts, optional: true });
347
101
  if (resolved) return { orgId: resolved.orgId, rest: positionals, source: resolved.source, sourceDetail: resolved.sourceDetail };
348
- fail({
349
- code: "ORG_REQUIRED",
350
- message: typeof first === "string"
351
- ? `No organization specified and no current organization set (${JSON.stringify(first)} is not an org_id — an org_id is a UUID).`
352
- : "No organization specified and no current organization set.",
353
- hint: `Pass --org <org_id> (or a leading <org_id> positional), set ${ORG_ENV}, bind this directory in .run402.json, or run: run402 orgs use <org_id>`,
354
- ...(typeof first === "string" ? { details: { rejected_positional: first } } : {}),
355
- next_actions: orgRequiredActions(),
356
- });
357
- return null; // unreachable — fail() exits
102
+ failLocal(orgRequiredError({ ...(typeof first === "string" ? { rejectedPositional: first } : {}), positionalHint: true }));
103
+ return null; // unreachable — failLocal() exits
358
104
  }
359
105
 
360
106
  /**
@@ -377,94 +123,11 @@ export function requireRest(rest, opts = {}) {
377
123
  return rest;
378
124
  }
379
125
 
380
- /** Validate an org id supplied by a human, naming the origin. Throws via fail(). */
126
+ /** Validate an org id supplied by a human, naming the origin. Exits on a bad shape. */
381
127
  export function requireOrgIdShape(orgId, origin = "--org") {
382
- return assertOrgIdShape(orgId, origin);
383
- }
384
-
385
- /** The profile's selected organization, without running the chain. */
386
- export function getSelectedOrgId() {
387
- return trimmed(coreGetActiveOrgId());
388
- }
389
-
390
- /** Record the profile's selected organization. */
391
- export function setSelectedOrgId(orgId, origin = "orgs use") {
392
- coreSetActiveOrgId(assertOrgIdShape(orgId, origin));
393
- }
394
-
395
- /** Clear the profile's selected organization. */
396
- export function clearSelectedOrgId() {
397
- coreClearActiveOrgId();
398
- }
399
-
400
- /**
401
- * Stamp a project's owning organization as the profile selection.
402
- *
403
- * Called by `projects use`: a project determines its organization
404
- * unambiguously, so selecting one keeps the two lowest classes from ever
405
- * disagreeing in practice — and gives an existing user a current org without
406
- * reading a changelog. Best-effort: a failure here must never fail the project
407
- * selection the caller actually asked for.
408
- */
409
- export async function stampOrgFromProject(projectId) {
410
- try {
411
- const orgId = await orgOfProject(projectId, { required: false });
412
- if (orgId) {
413
- coreSetActiveOrgId(orgId);
414
- return orgId;
415
- }
416
- } catch {
417
- /* best-effort */
418
- }
419
- return null;
420
- }
421
-
422
- /** Provenance pair for command output: bounded, never a resolution trace. */
423
- export function orgProvenance(resolved) {
424
- return resolved
425
- ? { org_id: resolved.orgId, org_source: resolved.source, org_source_detail: resolved.sourceDetail }
426
- : { org_id: null, org_source: null, org_source_detail: null };
427
- }
428
-
429
- /**
430
- * The organization that OWNS a specific project, or `null`.
431
- *
432
- * Distinct from {@link resolveOrgId}: that one answers "which org is this
433
- * command addressed at" (flag → env → active project); this one answers "who
434
- * owns THIS project id", which is what a project-scoped scaffold needs and
435
- * where an `--org` override would be a mis-binding, not a convenience.
436
- *
437
- * WHY IT CACHES (task 5.12c): the vault scaffold is the one part of
438
- * `run402 init` the client-surface spec says adds no network dependency to the
439
- * cold-start path, and resolving the org through `projects.list()` quietly made
440
- * that untrue. `org_id` is a non-secret routing identifier the control plane
441
- * already hands back with every listing, so the honest fix is to remember it —
442
- * the FIRST call on a machine still asks, every returning one reads it locally.
443
- *
444
- * Cached in the local project entry after the first lookup, and an EXACT id
445
- * match is required: a near-miss must never bind a vault or a git remote to
446
- * somebody else's project. `updateProject` is a no-op for a project this
447
- * machine holds no credentials for, so such a project asks again next time
448
- * rather than being silently mis-cached. Returns `null` rather than throwing —
449
- * every caller has something better to say than a stack trace.
450
- */
451
- export async function resolveOwningOrgId(projectId) {
452
- const cached = getProject(projectId)?.org_id;
453
- if (typeof cached === "string" && cached.length > 0) return cached;
454
128
  try {
455
- const listed = await getSdk().projects.list();
456
- const rows = Array.isArray(listed?.projects) ? listed.projects : [];
457
- const row = rows.find((p) => (p?.id ?? p?.project_id) === projectId);
458
- const orgId = row?.org_id;
459
- if (typeof orgId !== "string" || orgId.length === 0) return null;
460
- try {
461
- updateProject(projectId, { org_id: orgId });
462
- } catch {
463
- // Caching is an optimization; a read-only or contended keystore costs a
464
- // round trip next time and must never fail the caller.
465
- }
466
- return orgId;
467
- } catch {
468
- return null;
129
+ return assertOrgIdShape(orgId, origin);
130
+ } catch (err) {
131
+ return failResolver(err);
469
132
  }
470
133
  }
package/lib/orgs.mjs CHANGED
@@ -1,17 +1,6 @@
1
1
  import { getSdk } from "./sdk.mjs";
2
- import { reportSdkError, fail } from "./sdk-errors.mjs";
3
- import { readBindingFile, updateBindingFile } from "./wallet-context.mjs";
4
- import { nextAction } from "./next-actions.mjs";
5
- import {
6
- requireOrgIdShape,
7
- resolveOrg,
8
- takeOrgPositional,
9
- requireRest,
10
- orgProvenance,
11
- getSelectedOrgId,
12
- setSelectedOrgId,
13
- clearSelectedOrgId,
14
- } from "./org-context.mjs";
2
+ import { reportSdkError, fail, failLocal } from "./sdk-errors.mjs";
3
+ import { resolveOrg, takeOrgPositional, requireRest } from "./org-context.mjs";
15
4
  import {
16
5
  normalizeArgv,
17
6
  assertKnownFlags,
@@ -370,130 +359,57 @@ async function use(args) {
370
359
  requirePositionalCount(a, [], {
371
360
  min: 1, max: 1, command: "run402 orgs use <org_id>", missing: "<org_id>",
372
361
  });
373
- const orgId = positionalArgs(a, [])[0];
374
- setSelectedOrgId(orgId);
375
- console.log(JSON.stringify({ org_id: orgId, selected: true, scope: "wallet_profile" }, null, 2));
362
+ await printOrgContext((orgs) => orgs.use(positionalArgs(a, [])[0]));
376
363
  }
377
364
 
378
365
  async function clear(args) {
379
366
  const a = normalizeArgv(args);
380
367
  assertKnownFlags(a, ["--help", "-h"]);
381
368
  requirePositionalCount(a, [], { min: 0, max: 0, command: "run402 orgs clear" });
382
- const previous = getSelectedOrgId();
383
- clearSelectedOrgId();
384
- console.log(JSON.stringify({ org_id: null, selected: false, previous_org_id: previous ?? null }, null, 2));
369
+ await printOrgContext((orgs) => orgs.clear());
385
370
  }
386
371
 
387
372
  /**
388
- * Slugify a directory name into a legal room key.
389
- * Room keys match /^[a-z0-9][a-z0-9._-]{0,63}$/ (the DB CHECK on every
390
- * agent-messaging table), so the repo's own name has to be coerced, not trusted.
391
- */
392
- function roomKeyFromDir(dir) {
393
- const base = dir.split("/").filter(Boolean).pop() ?? "";
394
- const slug = base.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+/, "").slice(0, 64);
395
- return /^[a-z0-9]/.test(slug) ? slug : null;
396
- }
397
-
398
- /**
399
- * Write this checkout's org (and room) into `.run402.json`.
400
- *
401
- * WHY THIS PICKS FOR YOU WHEN YOU OWN EXACTLY ONE ORG, while the resolution
402
- * chain never does: they are different acts. The chain runs on EVERY command
403
- * and must not change meaning the day you are invited to a second org — so it
404
- * refuses to infer. This runs ONCE, because you asked it to, and it WRITES THE
405
- * ANSWER DOWN. Nothing is inferred afterwards; the file is read verbatim
406
- * forever after. An explicit act with a recorded result is not a heuristic.
407
- *
408
- * With two or more orgs there is nothing to pick, so it lists them and stops.
373
+ * Write this checkout's org (and room) into `.run402.json`. With no `--org`,
374
+ * the caller's sole membership is written down; the resolution chain itself
375
+ * never infers one.
409
376
  */
410
377
  async function bind(args) {
411
378
  const a = normalizeArgv(args);
412
379
  const valueFlags = ["--org", "--room"];
413
380
  assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
414
381
  requirePositionalCount(a, valueFlags, { min: 0, max: 0, command: "run402 orgs bind [--org <org_id>] [--room <key>]" });
415
-
416
- let orgId = flagValue(a, "--org");
417
- let picked = "flag";
418
- if (!orgId) {
419
- let orgs;
420
- try {
421
- orgs = await getSdk().orgs.list();
422
- } catch (err) {
423
- reportSdkError(err);
424
- return;
425
- }
426
- const rows = Array.isArray(orgs) ? orgs : (orgs?.orgs ?? []);
427
- if (rows.length === 0) {
428
- fail({
429
- code: "NO_ORGS",
430
- message: "This wallet is a member of no organization yet.",
431
- hint: "Run 'run402 init' to provision one, or ask an owner to add you with 'run402 orgs members add'.",
432
- next_actions: [nextAction("initialize_wallet", { command: "run402 init", why: "Provision this wallet's organization, then retry." })],
433
- });
434
- }
435
- if (rows.length > 1) {
436
- fail({
437
- code: "AMBIGUOUS_ORG",
438
- message: `This wallet belongs to ${rows.length} organizations — name the one to bind.`,
439
- hint: "run402 orgs bind --org <org_id>",
440
- details: { orgs: rows.map((o) => ({ org_id: o.org_id, display_name: o.display_name ?? null, role: o.role ?? null })) },
441
- next_actions: [nextAction("edit_request", { command: "run402 orgs bind --org <org_id>", why: "Name which organization this checkout coordinates in." })],
442
- });
443
- }
444
- orgId = rows[0].org_id;
445
- picked = "sole_membership";
446
- }
447
-
448
- const room = flagValue(a, "--room") ?? roomKeyFromDir(process.cwd());
449
- const { contents, file } = updateBindingFile(process.cwd(), {
450
- org: requireOrgIdShape(orgId, picked === "flag" ? "--org" : "orgs list"),
451
- ...(room ? { room } : {}),
452
- });
453
- console.log(JSON.stringify({
454
- org_id: orgId,
455
- room_key: room ?? null,
456
- org_source: picked,
457
- file: ".run402.json",
458
- path: file,
459
- bound: true,
460
- safe_to_commit: true,
461
- note: "Safe to commit — an org id is an identifier, not a credential; authorization stays server-side.",
462
- binding: contents,
463
- }, null, 2));
382
+ await printOrgContext((orgs) => orgs.bind(flagValue(a, "--org"), { room: flagValue(a, "--room") }));
464
383
  }
465
384
 
466
385
  async function unbind(args) {
467
386
  const a = normalizeArgv(args);
468
387
  assertKnownFlags(a, ["--help", "-h"]);
469
388
  requirePositionalCount(a, [], { min: 0, max: 0, command: "run402 orgs unbind" });
470
- const previous = readBindingFile(process.cwd());
471
- const { contents, removed } = updateBindingFile(process.cwd(), { org: null, room: null });
472
- console.log(JSON.stringify({
473
- file: ".run402.json",
474
- unbound: previous.org !== undefined || previous.room !== undefined,
475
- removed,
476
- binding: contents,
477
- }, null, 2));
389
+ await printOrgContext((orgs) => orgs.unbind());
478
390
  }
479
391
 
480
392
  async function current(args) {
481
393
  const a = normalizeArgv(args);
482
394
  assertKnownFlags(a, ["--help", "-h"]);
483
395
  requirePositionalCount(a, [], { min: 0, max: 0, command: "run402 orgs current" });
396
+ await printOrgContext((orgs) => orgs.current());
397
+ }
398
+
399
+ /** The organization-context refusals `r.orgs` raises locally; anything else is the server's answer. */
400
+ const ORG_CONTEXT_CODES = new Set(["BAD_ORG_ID", "AMBIGUOUS_ORG", "ORG_REQUIRED", "NO_ORGS"]);
401
+
402
+ /** Run one `r.orgs` context verb and print its result, or the refusal envelope. */
403
+ async function printOrgContext(fn) {
404
+ let result;
484
405
  try {
485
- // `cmd: "orgs"` exempts this from the ambiguity error on purpose: the
486
- // command that reports the selection must stay usable while it is ambiguous.
487
- // `optional` keeps an empty selection an explicit null state rather than a
488
- // failure — reporting is not acting.
489
- const resolved = await resolveOrg([], { cmd: "orgs", optional: true });
490
- console.log(JSON.stringify({
491
- ...orgProvenance(resolved),
492
- selected_org_id: getSelectedOrgId() ?? null,
493
- }, null, 2));
406
+ result = await fn(getSdk().orgs);
494
407
  } catch (err) {
408
+ if (err?.kind === "local_error" && ORG_CONTEXT_CODES.has(err?.code)) failLocal(err);
495
409
  reportSdkError(err);
410
+ return;
496
411
  }
412
+ console.log(JSON.stringify(result, null, 2));
497
413
  }
498
414
 
499
415
  async function get(args) {
package/lib/projects.mjs CHANGED
@@ -4,7 +4,6 @@ import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.
4
4
  import { withAutoApprove } from "./sign-in.mjs";
5
5
  import { getSdk } from "./sdk.mjs";
6
6
  import { reportSdkError, fail, parseFlagJson } from "./sdk-errors.mjs";
7
- import { stampOrgFromProject } from "./org-context.mjs";
8
7
  import { assertKnownFlags, failBadProjectId, flagValue, hasHelp, normalizeArgv, positionalArgs, resolveProjectSelector, validateRegularFile, failUnknownSubcommand } from "./argparse.mjs";
9
8
 
10
9
  const HELP = `run402 projects — Manage your deployed Run402 projects
@@ -805,7 +804,7 @@ async function use(projectId) {
805
804
  // A project determines its org unambiguously, so selecting one also makes
806
805
  // that org current (add-cli-current-org D4). Best-effort by construction:
807
806
  // the project selection the caller asked for must not fail on this.
808
- const orgId = await stampOrgFromProject(projectId);
807
+ const orgId = await getSdk().orgs.selectFromProject(projectId);
809
808
  console.log(JSON.stringify({ active_project_id: projectId, set: true, org_id: orgId ?? null }));
810
809
  } catch (err) {
811
810
  reportSdkError(err);