run402 4.39.1 → 4.41.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 (59) hide show
  1. package/README.md +7 -4
  2. package/cli.mjs +10 -2
  3. package/lib/assets.mjs +19 -26
  4. package/lib/cdn.mjs +4 -6
  5. package/lib/command-manifest.mjs +56 -25
  6. package/lib/config.mjs +30 -0
  7. package/lib/deploy-v2.mjs +15 -0
  8. package/lib/doctor.mjs +36 -3
  9. package/lib/gitvault-scaffold.mjs +1 -1
  10. package/lib/gitvault.mjs +121 -819
  11. package/lib/init.mjs +2 -2
  12. package/lib/repos.mjs +1237 -248
  13. package/package.json +1 -1
  14. package/sdk/dist/errors.d.ts +5 -2
  15. package/sdk/dist/errors.d.ts.map +1 -1
  16. package/sdk/dist/errors.js.map +1 -1
  17. package/sdk/dist/namespaces/gitvault.crypto.d.ts +10 -0
  18. package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -1
  19. package/sdk/dist/namespaces/gitvault.crypto.js +10 -0
  20. package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -1
  21. package/sdk/dist/namespaces/gitvault.d.ts +285 -5
  22. package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
  23. package/sdk/dist/namespaces/gitvault.js +369 -8
  24. package/sdk/dist/namespaces/gitvault.js.map +1 -1
  25. package/sdk/dist/node/gitvault-apply.d.ts +1 -1
  26. package/sdk/dist/node/gitvault-apply.d.ts.map +1 -1
  27. package/sdk/dist/node/gitvault-apply.js +6 -6
  28. package/sdk/dist/node/gitvault-apply.js.map +1 -1
  29. package/sdk/dist/node/gitvault-creation-journal.js +1 -1
  30. package/sdk/dist/node/gitvault-creation-journal.js.map +1 -1
  31. package/sdk/dist/node/gitvault-deploy.js +2 -2
  32. package/sdk/dist/node/gitvault-deploy.js.map +1 -1
  33. package/sdk/dist/node/gitvault-keystore.d.ts +11 -1
  34. package/sdk/dist/node/gitvault-keystore.d.ts.map +1 -1
  35. package/sdk/dist/node/gitvault-keystore.js.map +1 -1
  36. package/sdk/dist/node/gitvault-mirror-backend.d.ts +88 -0
  37. package/sdk/dist/node/gitvault-mirror-backend.d.ts.map +1 -0
  38. package/sdk/dist/node/gitvault-mirror-backend.js +374 -0
  39. package/sdk/dist/node/gitvault-mirror-backend.js.map +1 -0
  40. package/sdk/dist/node/gitvault-mirror-config.d.ts +53 -0
  41. package/sdk/dist/node/gitvault-mirror-config.d.ts.map +1 -0
  42. package/sdk/dist/node/gitvault-mirror-config.js +112 -0
  43. package/sdk/dist/node/gitvault-mirror-config.js.map +1 -0
  44. package/sdk/dist/node/gitvault-mirror.d.ts +120 -0
  45. package/sdk/dist/node/gitvault-mirror.d.ts.map +1 -0
  46. package/sdk/dist/node/gitvault-mirror.js +464 -0
  47. package/sdk/dist/node/gitvault-mirror.js.map +1 -0
  48. package/sdk/dist/node/gitvault-publication.d.ts +153 -3
  49. package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
  50. package/sdk/dist/node/gitvault-publication.js +182 -13
  51. package/sdk/dist/node/gitvault-publication.js.map +1 -1
  52. package/sdk/dist/node/gitvault-recover.d.ts +136 -0
  53. package/sdk/dist/node/gitvault-recover.d.ts.map +1 -0
  54. package/sdk/dist/node/gitvault-recover.js +412 -0
  55. package/sdk/dist/node/gitvault-recover.js.map +1 -0
  56. package/sdk/dist/node/gitvault-snapshot.d.ts +8 -0
  57. package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -1
  58. package/sdk/dist/node/gitvault-snapshot.js +11 -0
  59. package/sdk/dist/node/gitvault-snapshot.js.map +1 -1
package/lib/gitvault.mjs CHANGED
@@ -1,815 +1,119 @@
1
1
  /**
2
- * `run402 gitvault` — the host-blind encrypted Git remote (wire tag `r402s/v0`).
2
+ * `run402 gitvault` — RETIRED (repo-surface-consolidation, design D7).
3
3
  *
4
- * ARCHITECTURAL LAW (gitvault-client-surface, "All protocol logic lives in the
5
- * SDK"): every piece of vault protocol behaviour crypto core, keystore,
6
- * creation journal, snapshot + capture, publication state machines, ref
7
- * transactions, verification budget, token exchange, repair lives ONCE in
8
- * `@run402/sdk` under `r.gitvault`. This module is a THIN ADAPTER: argument
9
- * parsing, TTY output, exit codes, local file I/O. It adds zero protocol
10
- * behaviour, and imports only the SDK (via `./sdk.mjs`) plus the CLI's own
11
- * argument/error helpers never a crypto, HTTP, or git library.
4
+ * The 19-command dispatcher this module used to hold is gone. Every old
5
+ * spelling now answers a proper stderr JSON error envelope: typed
6
+ * `next_actions`, non-zero exit, EMPTY stdout never silence, never new
7
+ * behavior. Renamed verbs answer `COMMAND_MOVED` naming their `repos`
8
+ * successor; verbs with no behavioral successor answer `COMMAND_REMOVED`
9
+ * (never a `COMMAND_MOVED` that would lie about equivalence design
10
+ * D7/D10). The freed spellings are RESERVED for one release, never
11
+ * re-pointed at different behavior the `rooms`→`messages` precedent.
12
12
  *
13
- * Pipe contract (docs/style.md): the payload is JSON on stdout; every human
14
- * line progress, the terminal-loss statement, advisories goes to stderr, so
15
- * `run402 gitvault status | jq` stays clean.
16
- *
17
- * Run these from inside the git working tree: `repo_dir` is `process.cwd()`.
13
+ * `gitvault` itself survives only as protocol/infrastructure language: the
14
+ * SDK keeps `r.gitvault.*` (design D1 "gitvault is what the thing IS;
15
+ * repos is what the user HAS"). This module's one surviving export,
16
+ * `printKeystoreLocation`, is a shared print helper `cli/lib/repos.mjs`
17
+ * still composes moving it there instead would make repos.mjs restate
18
+ * logic this file already owns for zero benefit.
18
19
  */
19
- import { readFileSync } from "node:fs";
20
- import { resolveProjectId } from "./config.mjs";
21
- import { resolveOwningOrgId } from "./org-context.mjs";
22
- import { resolveGitvaultTarget } from "./gitvault-target.mjs";
23
- import { getSdk } from "./sdk.mjs";
24
- import { reportSdkError, fail } from "./sdk-errors.mjs";
25
- import {
26
- normalizeArgv,
27
- hasHelp,
28
- assertKnownFlags,
29
- parseIntegerFlag,
30
- flagValue,
31
- requirePositionalCount,
32
- failUnknownSubcommand,
33
- } from "./argparse.mjs";
34
-
35
- /** Value-taking flags every gitvault subcommand accepts. */
36
- const COMMON_VALUE_FLAGS = ["--project", "--repo"];
20
+ import { fail } from "./sdk-errors.mjs";
21
+ import { hasHelp } from "./argparse.mjs";
37
22
 
38
- export const HELP = `run402 gitvault — your source, encrypted before it leaves the machine
23
+ export const HELP = `run402 gitvault — RETIRED (repo-surface-consolidation)
39
24
 
40
25
  Usage:
41
- run402 gitvault init [--project <id>] [--org <org_id>] [--git-remote] [--no-remote]
42
- run402 gitvault status [--project <id>] [--repo <repo_id>] [--refs] [--human]
43
- run402 gitvault snapshot [--project <id>] [--repo <repo_id>] [--message <text>] [--checkpoint] [--dry-run]
44
- run402 gitvault policy <required|grandfathered> [--project <id>] [--repo <repo_id>]
45
- [--reason <why>]
46
- run402 gitvault compact [--project <id>] [--repo <repo_id>]
47
- run402 gitvault prune [--project <id>] [--repo <repo_id>]
48
- [--submit --intent-core <path> --verifier-receipt <path> [--wait]]
49
- run402 gitvault verify [--project <id>] [--repo <repo_id>] [--budget <n>]
50
-
51
- Subcommands:
52
- init ALLOCATE the project's vault. This is the one step that mints key
53
- material on this machine and emits the one-shot recovery receipt,
54
- so it is explicit rather than a side effect of \`run402 init\` (which
55
- only adds the git remote). Idempotent: an existing vault is
56
- reported with \`deduplicated: true\` and nothing is re-minted. Adds
57
- the \`run402\` remote too when the current directory is already a
58
- repository.
59
- status What this machine and the control plane each believe about the
60
- vault: allocation, policy, whether this keystore can sign, the
61
- authenticated and materialized pins, any pending
62
- unvaulted-override journals, and where the keystore lives. Never
63
- reports key material.
64
- policy Set the activation policy — \`required\` (a deploy must present a
65
- vaulted capture) or \`grandfathered\` (it need not). Owner + step-up,
66
- audited. \`grandfathered\` is the documented way out of a deploy
67
- blocked by GITVAULT_CLIENT_UPGRADE_REQUIRED, and leaves a
68
- doctor-persistent warning until the project returns to \`required\`.
69
- snapshot Capture the working tree and publish it. This is NOT gated on a
70
- deploy — a vault-only project snapshots for months without one.
71
- Against a project with no vault yet, this ALLOCATES one inline
72
- (the six-stage creation, same as \`init\`) before publishing — one
73
- command, no prior \`gitvault init\`. When --repo/--project are
74
- both omitted and the local run402/origin remote is a slug-form
75
- address (run402::<org-slug>/<name>), PUSH-TO-CREATES through it
76
- instead (design D6) — same as pushing that name with \`git\`. The
77
- one-shot recovery receipt and keystore path print to stderr the
78
- moment that happens.
79
- Before reporting a snapshot as landed the SDK compares finalization
80
- receipts against the expected manifest and reads the admitted head
81
- back from storage; a 200 alone is never enough. \`push\` is a
82
- deprecation-warning alias for one release — it will be removed
83
- next release. Once \`gitvault\` was the only publish verb; \`git
84
- push\` is now the actual publish path (via the remote helper),
85
- so \`push\` here was renamed to name what it does: one verb per
86
- operation. \`--dry-run\` (kychee-com/run402#565) previews it
87
- instead: the same real local pipeline, publishing nothing and
88
- never allocating.
89
- compact Publish a checkpoint covering the canonical refs, every root
90
- unexpired at the cutoff, and the HEAD target, under a maintenance
91
- lease so a concurrent cycle cannot race it.
92
- prune Plan a prune, and — with both verifier receipts — submit it.
93
- Two phases, because the protocol is two-phase; see below.
94
- verify Verify the head chain from the authenticated pin up to the newest
95
- listed generation. Fails closed on a regression, a gap, or a
96
- transition descriptor this client cannot validate.
97
-
98
- Options:
99
- --project <id> Project whose vault to act on (defaults to the active project)
100
- --org <org_id> init: the owning organization (resolved from the project
101
- when omitted)
102
- --git-remote init: 'git init' the current directory when it is not a
103
- repository yet, so the run402 remote can be added there.
104
- Opt-in: creating a repository where you did not ask for one
105
- is a bad surprise, so without it a non-repository directory
106
- allocates the vault and adds no remote.
107
- --no-remote init: allocate the vault only; touch no git configuration
108
- --reason <why> policy: why the policy is changing — recorded in the audit
109
- event. REQUIRED for \`grandfathered\`, which is a deliberate
110
- weakening of the activation guarantee.
111
- --refs status: also materialize and report the vault's ref map and
112
- HEAD target. This is a VERIFICATION (it walks the head
113
- chain and advances the local materialized pin), which is
114
- why plain \`status\` — an observation — does not do it.
115
- --human status: a five/six-line human summary on stdout instead of
116
- the JSON dump (kychee-com/run402#569; explicit opt-in per
117
- the cli-output-contract). Address, remote; HEAD + ref count
118
- (needs --refs too — otherwise the line names the omission);
119
- generations in decimal; storage bytes/object count (from
120
- this SAME status() call — no extra network read); whether
121
- THIS machine can decrypt, and the policy; standing warnings,
122
- if any, verbatim (a live terminal-loss risk may be the sixth
123
- line). Rejected together with --json. No effect on plain
124
- \`status\`'s own output, which is unchanged.
125
- --repo <repo_id> Address the vault directly by id, skipping project lookup
126
- --message <text> snapshot: commit message for the synthetic commit a dirty tree
127
- produces (a clean tree pushes HEAD itself, no message used)
128
- --checkpoint snapshot: force the checkpoint-bearing form regardless of delta size
129
- --dry-run snapshot: a REAL preview (kychee-com/run402#565) — runs the actual
130
- local pipeline (capture, pack building, encryption sizing) and
131
- reports objects, encrypted bytes, refs, and the generation it
132
- would admit as. Publishes NOTHING, and never allocates a vault
133
- that does not exist yet (reports allocation_needed instead).
134
- --budget <n> verify: heads to verify in this call. The verified prefix is
135
- persisted, so a budget-exceeded run resumes where it stopped
136
- instead of restarting.
137
- --submit prune: submit the planned intent. Requires --intent-core and
138
- --verifier-receipt.
139
- --intent-core <path>
140
- prune: the plan's \`intent_core\`, saved verbatim from a prior
141
- planning run. A rebuilt core carries a different nonce, so
142
- the r402s-verify receipt would no longer bind to it.
143
- --verifier-receipt <path>
144
- prune: r402s-verify's \`verifier_receipt\` over that core.
145
- --wait prune: poll the submitted intent until the control-plane-
146
- signed completion appears, instead of returning immediately.
147
- --json No-op: stdout is already JSON.
148
-
149
- prune is TWO PHASES, because the protocol is:
150
- 1. \`run402 gitvault prune\` plans. It walks the verified chain, computes the GC
151
- root set, subtracts it, and prints a SIGNED \`intent_core\` plus its
152
- \`intent_core_sha256\`. Nothing is submitted and nothing is deleted.
153
- 2. Run \`r402s-verify\` against that core, then re-run with
154
- \`--submit --intent-core <core.json> --verifier-receipt <receipt.json>\`.
155
- The intent carries TWO receipts over the same core, one per implementation:
156
- this CLI produces the \`run402-cli\` half by restoring the latest checkpoint
157
- and recomputing its commitments, and \`r402s-verify\` produces the other.
158
- A second receipt from this lineage would prove nothing, so it is never
159
- synthesized here.
160
- Only the control-plane-signed completion says what was deleted, and only its
161
- \`deleted\` result means the bytes are gone — \`present_after_attempt\` is a
162
- FAILED deletion, never counted as a success. There is deliberately no purge
163
- verb in V0 at all. Retention is an operational promise of the platform, not a
164
- cryptographic guarantee against it.
165
-
166
- Expiry is permissive, by design:
167
- A retention root whose \`effective_admitted_at\` this client cannot resolve is
168
- RETAINED, and a compact that cannot obtain a retention-cutoff ticket keeps
169
- every root. That costs storage, never history.
170
-
171
- Terminal loss (protocol §0):
172
- In V0-A, whole-machine or whole-keystore loss is terminal for vault history
173
- until human envelopes ship. \`status\` prints the full statement verbatim on
174
- stderr and carries it in its JSON — read it before you rely on this.
175
-
176
- Examples:
177
- run402 gitvault init
178
- run402 gitvault status --refs
179
- run402 gitvault status --human
180
- run402 gitvault snapshot --message "wip: refactor the parser"
181
- run402 gitvault snapshot --dry-run
182
- run402 gitvault policy grandfathered --reason "migrating CI to a vaulted client"
183
- run402 gitvault verify --budget 500
184
- run402 gitvault prune --project prj_1a2b3c
185
- # \`gitvault push\` still works as a deprecation-warning alias for \`snapshot\`
186
- # for one release; it will be removed next release.
26
+ run402 gitvault <anything> answers a structured error naming its repos/git successor; see below.
27
+
28
+ Every "run402 gitvault <verb>" spelling has moved to "run402 repos <verb>",
29
+ except "reconcile" and "push", which are REMOVED outright (see below). Each
30
+ one answers a structured COMMAND_MOVED/COMMAND_REMOVED error for exactly one
31
+ release; after that the spelling is reserved and answers nothing at all.
32
+
33
+ Moved:
34
+ gitvault init -> repos create --project <id> (or repos create <name> for a NEW project)
35
+ gitvault status -> repos view
36
+ gitvault snapshot -> repos snapshot
37
+ gitvault policy -> repos policy
38
+ gitvault compact -> repos gc
39
+ gitvault prune -> repos gc
40
+ gitvault verify -> repos fsck
41
+ gitvault mirror -> repos mirror
42
+ gitvault recover -> repos recover
43
+
44
+ Removed, no successor:
45
+ gitvault push -> git push (its one-release alias window is over); repos snapshot is the capture lane
46
+ gitvault reconcile -> repos access (read-only inspection; the workaround it approximated is gone, not renamed)
47
+
48
+ Run \`run402 repos --help\` for the current surface.
187
49
  `;
188
50
 
189
- /**
190
- * Resolve which vault to act on, plus the local git tree.
191
- *
192
- * `--repo` addresses the vault directly (the cold-restart path: an agent that
193
- * knows its repo_id needs no project lookup). Otherwise the project targets,
194
- * highest first: `--project` > the repo's own pin/remote > RUN402_PROJECT_ID
195
- * > the active project (repo-first-onramp follow-up, kychee-com/run402#559
196
- * see `gitvault-target.mjs`'s module doc for the full targeting order and
197
- * why it exists: a stale active-project pointer used to silently outrank the
198
- * repository this command is actually standing in).
199
- */
200
- async function vaultTarget(a) {
201
- const repoId = flagValue(a, "--repo");
202
- const project = flagValue(a, "--project");
203
- const repoDir = process.cwd();
204
- const resolved = await resolveGitvaultTarget({
205
- repoDir,
206
- explicitProjectId: project ?? undefined,
207
- explicitRepoId: repoId ?? undefined,
51
+ /** Renamed verbs: `gitvault <key>` -> the exact `repos` command that means the same thing now. */
52
+ const MOVED = {
53
+ init: { command: "run402 repos create --project <id>", why: "repos create absorbs allocation — pass --project to adopt an existing project, or a name to provision a new one." },
54
+ status: { command: "run402 repos view", why: "repos view is the side-effect-free repo inspection command; it never materializes refs the way status --refs used to." },
55
+ snapshot: { command: "run402 repos snapshot", why: "same verb, new noun." },
56
+ policy: { command: "run402 repos policy", why: "same verb, new noun." },
57
+ compact: { command: "run402 repos gc", why: "gc is git gc's own two halves checkpoint publication and prune planning — in one verb." },
58
+ prune: { command: "run402 repos gc", why: "gc is git gc's own two halves checkpoint publication and prune planning — in one verb." },
59
+ verify: { command: "run402 repos fsck", why: "fsck is git fsck's own job: walk the object graph and fail closed on corruption." },
60
+ mirror: { command: "run402 repos mirror", why: "one flag-driven verb replaces the five mirror subcommands (set/remove/status/sync/verify)." },
61
+ recover: { command: "run402 repos recover", why: "same verb, new noun." },
62
+ };
63
+
64
+ /** Verbs with NO behavioral successor — a lying COMMAND_MOVED would be worse than an honest COMMAND_REMOVED. */
65
+ const REMOVED = {
66
+ push: {
67
+ message: "`run402 gitvault push` was a deprecation-warning alias for exactly one release, and that release is over.",
68
+ hint: "`push` now means exactly one thing everywhere: `git push`. The capture lane is `run402 repos snapshot`.",
69
+ next_actions: [
70
+ { type: "use_moved_command", command: "git push", why: "Publish your branches and tags the ordinary way." },
71
+ { type: "use_moved_command", command: "run402 repos snapshot", why: "The capture lane: publish the protocol-owned deploy ref outside a deploy." },
72
+ ],
73
+ },
74
+ reconcile: {
75
+ message: "`run402 gitvault reconcile` is removed. It was a workaround — its own help text said so — a newly-wrapped member got the vault's entire history under a single fixed epoch, not real epoch rotation, and a temporary mechanism does not get a permanent verb.",
76
+ hint: "`run402 repos access` reports what the read surface has today (recipients, coverage, this machine's local TOFU pins). `repos access repair` will replace the mutating half once real epoch rotation ships — it does not exist yet either.",
77
+ next_actions: [{ type: "use_moved_command", command: "run402 repos access", why: "Inspect recipients and coverage — there is no equivalent mutating successor yet." }],
78
+ },
79
+ };
80
+
81
+ function movedResponse(sub) {
82
+ const m = MOVED[sub];
83
+ fail({
84
+ code: "COMMAND_MOVED",
85
+ message: `run402 gitvault ${sub} moved to ${m.command}.`,
86
+ hint: m.why,
87
+ details: { was: `gitvault ${sub}`, now: m.command },
88
+ next_actions: [{ type: "use_moved_command", command: m.command, why: m.why }],
208
89
  });
209
- const target = { repo_dir: repoDir };
210
- if (repoId != null) target.repo_id = repoId;
211
- // Only demand a project when one is actually needed: `--repo` alone is a
212
- // complete address, and requiring one on top of it would make the
213
- // cold-restart path fail for no reason.
214
- if (repoId == null || project != null) {
215
- if ("repo_id" in resolved && project == null) target.repo_id = resolved.repo_id;
216
- // `resolveGitvaultTarget` reports its last (env/active) tier
217
- // non-throwingly (`run402 doctor`'s call site needs that) — this call
218
- // site is the one that historically failed closed with PROJECT_REQUIRED
219
- // when nothing resolves anywhere, and still does: `resolveProjectId`
220
- // re-derives the exact same env/active check and throws.
221
- if ("project_id" in resolved) target.project_id = resolved.project_id ?? resolveProjectId(project);
222
- }
223
- return target;
224
90
  }
225
91
 
226
- /**
227
- * Print the protocol §0 terminal-loss statement.
228
- *
229
- * NORMATIVE COPY, printed verbatim straight from the SDK's own constants and
230
- * never paraphrased, summarized, or reassembled here. Both lines also ride in
231
- * the JSON payload on stdout.
232
- *
233
- * The PATH is printed with it. "Whole-keystore loss is terminal" appeared three
234
- * times across this surface while the directory to back up appeared nowhere
235
- * (dogfood #1, finding D2) — a warning nobody can act on.
236
- */
237
- function printTerminalLoss(status) {
238
- console.error("");
239
- console.error(status.terminal_loss_statement);
240
- console.error(status.terminal_loss_detail);
241
- console.error(`Back up this directory: ${status.keystore.root}`);
242
- console.error("");
92
+ function removedResponse(sub) {
93
+ const r = REMOVED[sub];
94
+ fail({
95
+ code: "COMMAND_REMOVED",
96
+ message: r.message,
97
+ hint: r.hint,
98
+ details: { was: `gitvault ${sub}` },
99
+ next_actions: r.next_actions,
100
+ });
243
101
  }
244
102
 
245
103
  /**
246
- * Where the keystore lives — for verbs whose payload is not a `status`.
247
- * Exported: `repos create` (repo-first-onramp task 2.6) prints the same
248
- * line after allocating a vault, and must not restate this logic.
104
+ * Where the keystore lives — for verbs whose payload is not a `view`.
105
+ * Exported: `repos create` and `repos snapshot` print the same line after
106
+ * allocating/publishing, and must not restate this logic.
249
107
  */
250
108
  export async function printKeystoreLocation() {
251
109
  try {
252
110
  const { getGitvaultKeystoreRoot } = await import("#sdk/node");
253
- console.error(`keystore: ${getGitvaultKeystoreRoot()} — back this up; whole-keystore loss is terminal for vault history`);
111
+ console.error(`keystore: ${getGitvaultKeystoreRoot()} — back this up; whole-keystore loss is terminal for repo history`);
254
112
  } catch {
255
113
  // Never let a diagnostic line fail a command that already succeeded.
256
114
  }
257
115
  }
258
116
 
259
- /**
260
- * `run402 gitvault init` — allocate the project's vault.
261
- *
262
- * WHY THIS EXISTS AS ITS OWN VERB (dogfood #1, finding A). Until it did, the
263
- * only way to allocate was `sdk.gitvault.init()` through the vendored SDK:
264
- * `gitvault status` pointed at `run402 init`, which scaffolds the remote and
265
- * says so in a comment; `gitvault push` and `git push run402` both 404'd and
266
- * handed the user a raw `POST /gitvault/v1/vaults`. A published CLI that can
267
- * do everything except start is not a usable product.
268
- *
269
- * It stays SEPARATE from `run402 init` on purpose: this is the step that mints
270
- * key material on this machine and emits a one-shot recovery receipt, and
271
- * whole-keystore loss is terminal for vault history. That belongs to a command
272
- * the user typed, not to a setup command's side effects.
273
- */
274
- async function init(args) {
275
- const a = normalizeArgv(args);
276
- const valueFlags = ["--project", "--org"];
277
- assertKnownFlags(a, [...valueFlags, "--git-remote", "--no-remote", "--help", "-h"], valueFlags);
278
- requirePositionalCount(a, valueFlags, {
279
- min: 0, max: 0, command: "run402 gitvault init", missing: "",
280
- });
281
- if (a.includes("--git-remote") && a.includes("--no-remote")) {
282
- fail({
283
- code: "BAD_USAGE",
284
- message: "--git-remote and --no-remote contradict each other.",
285
- hint: "--git-remote creates a repository to add the remote to; --no-remote touches no git configuration at all.",
286
- });
287
- }
288
- const projectId = resolveProjectId(flagValue(a, "--project"));
289
- const orgId = flagValue(a, "--org") ?? await resolveOwningOrgId(projectId);
290
- if (!orgId) {
291
- fail({
292
- code: "ORG_UNRESOLVED",
293
- message: `Could not resolve the organization that owns ${projectId}.`,
294
- hint: "Pass --org <org_id>, or check that this wallet can see the project (`run402 projects list`).",
295
- details: { project_id: projectId },
296
- });
297
- }
298
-
299
- // Whether to touch git at all. Mirrors `run402 init`: adding a remote inside
300
- // an EXISTING repository is pure addition and is the default; CREATING a
301
- // repository is opt-in, because a vault can be allocated from anywhere and
302
- // `git init`-ing whatever directory you happened to be in is a bad surprise.
303
- let scaffold = !a.includes("--no-remote");
304
- let remoteSkipped = null;
305
- if (scaffold && !a.includes("--git-remote")) {
306
- const { hardenedGit } = await import("#sdk/node");
307
- try {
308
- await hardenedGit(process.cwd(), ["rev-parse", "--git-dir"]);
309
- } catch {
310
- scaffold = false;
311
- remoteSkipped = "not a git repository — the vault was allocated; re-run with --git-remote to create one and add the remote";
312
- }
313
- }
314
-
315
- try {
316
- const result = await getSdk().gitvault.init({
317
- org_id: orgId,
318
- project_id: projectId,
319
- ...(scaffold ? { repo_dir: process.cwd() } : { scaffold_git: false }),
320
- });
321
- console.log(JSON.stringify(remoteSkipped ? { ...result, remote_skipped: remoteSkipped } : result, null, 2));
322
- console.error(
323
- result.deduplicated
324
- ? `vault ${result.repo_id} already existed — nothing was re-allocated and no new key material was minted`
325
- : `allocated vault ${result.repo_id} (genesis ${result.genesis_sha256})`,
326
- );
327
- if (result.remote) console.error(`remote '${result.remote.name}' -> ${result.remote.url} (${result.remote.reason})`);
328
- if (remoteSkipped) console.error(`remote not added: ${remoteSkipped}`);
329
- // The recovery receipt is integrity data, not a secret, and it is worth
330
- // exactly as much as the number of copies you keep. It is persisted into
331
- // the keystore automatically; say where, because "keep many copies" is
332
- // advice nobody can act on without a path.
333
- console.error("");
334
- console.error(result.terminal_loss_statement);
335
- await printKeystoreLocation();
336
- console.error("");
337
- } catch (err) {
338
- reportSdkError(err);
339
- }
340
- }
341
-
342
- /**
343
- * `run402 gitvault policy <required|grandfathered>` — the activation gate.
344
- *
345
- * The gateway's own `GITVAULT_CLIENT_UPGRADE_REQUIRED` envelope names
346
- * `run402 gitvault policy grandfathered --reason <why>` as the second way out
347
- * of a blocked deploy. Until this verb existed, running exactly what the
348
- * platform told you to run returned UNKNOWN_SUBCOMMAND, so a user could
349
- * allocate themselves into a blocked-deploy state with no way back.
350
- */
351
- async function policy(args) {
352
- const a = normalizeArgv(args);
353
- const valueFlags = [...COMMON_VALUE_FLAGS, "--reason"];
354
- assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
355
- const [requested] = requirePositionalCount(a, valueFlags, {
356
- min: 1, max: 1, command: "run402 gitvault policy <required|grandfathered>",
357
- missing: "Missing <policy>. Expected `required` or `grandfathered`.",
358
- });
359
- if (requested !== "required" && requested !== "grandfathered") {
360
- fail({
361
- code: "BAD_USAGE",
362
- message: `Unknown policy: ${requested}.`,
363
- hint: "Expected `required` (a deploy must present a vaulted capture) or `grandfathered` (it need not).",
364
- details: { policy: requested, known_policies: ["required", "grandfathered"] },
365
- });
366
- }
367
- const reason = flagValue(a, "--reason");
368
- // Required only for the weakening direction. Returning to `required` is
369
- // restoring the default and needs no justification; leaving it does.
370
- if (requested === "grandfathered" && (reason == null || reason.trim() === "")) {
371
- fail({
372
- code: "BAD_USAGE",
373
- message: "`grandfathered` needs --reason <why>.",
374
- hint: "It weakens the activation guarantee for this project and is recorded in the audit event. Say why, e.g. --reason \"migrating CI to a vaulted client\".",
375
- details: { policy: requested },
376
- });
377
- }
378
-
379
- const target = await vaultTarget(a);
380
- try {
381
- const sdk = getSdk();
382
- const repoId = target.repo_id ?? (await sdk.gitvault.forProject(target.project_id)).repo_id;
383
- const result = await sdk.gitvault.setPolicy(repoId, {
384
- gitvault_policy: requested,
385
- ...(reason != null ? { reason } : {}),
386
- });
387
- console.log(JSON.stringify({ repo_id: repoId, ...result }, null, 2));
388
- console.error(
389
- result.changed
390
- ? `gitvault_policy is now ${result.gitvault_policy} (version ${result.gitvault_policy_version})`
391
- : `gitvault_policy was already ${result.gitvault_policy} — nothing changed`,
392
- );
393
- for (const w of result.warnings ?? []) console.error(`warning (${w.kind}): ${w.message}`);
394
- } catch (err) {
395
- reportSdkError(err);
396
- }
397
- }
398
-
399
- /**
400
- * The vault's address in the form a human would actually type it: named
401
- * (`run402::<org-slug>/<name>`) when this checkout's local pin resolved from
402
- * one — an id-form pin buys nothing and is never written (see
403
- * `gitvault-address.ts`'s own doc comment), so a non-null `s.pinned` always
404
- * carries `resolved_from` — else id-form (`run402::<org_id>/<project_id>`),
405
- * falling back to whichever of project_id/repo_id is known when the vault
406
- * record itself is unavailable.
407
- */
408
- function formatGitvaultAddress(s) {
409
- if (s.pinned?.resolved_from) {
410
- return `run402::${s.pinned.resolved_from.org_slug}/${s.pinned.resolved_from.repo_name}`;
411
- }
412
- const orgId = s.vault?.org_id ?? null;
413
- const projectId = s.project_id ?? s.vault?.project_id ?? null;
414
- if (orgId && projectId) return `run402::${orgId}/${projectId}`;
415
- if (projectId) return projectId;
416
- if (s.repo_id) return `repo ${s.repo_id}`;
417
- return "(unresolved)";
418
- }
419
-
420
- /**
421
- * `run402 gitvault status --human` (kychee-com/run402#569) — the five-liner:
422
- * "status --refs is an admission-debugging protocol dump; the human question
423
- * is five lines — remote URL, branch/HEAD, generation, bytes,
424
- * can-this-machine-decrypt." Renders from `s` alone — the SAME status() call
425
- * the JSON path already made, so `--human` costs no extra network read.
426
- *
427
- * Generations render DECIMAL, not the wire's 16-hex-digit form — a hex
428
- * generation is a protocol detail, not something a human reads at a glance.
429
- *
430
- * The HEAD/ref-count line needs the vault's OWN ref map, which `status`
431
- * fetches only when `--refs` is ALSO passed (materializing is a verification
432
- * that advances local state — `status` alone stays a pure observation, see
433
- * that option's own doc comment). Composing `--human --refs` gets the full
434
- * line; `--human` alone names the omission rather than guessing from the
435
- * local git checkout, which could easily disagree with what the vault holds.
436
- *
437
- * Warnings — including the progressive terminal-loss risk warning — are
438
- * echoed EXACTLY as the SDK reported them (never reworded) and become an
439
- * optional sixth line, present only when `s.warnings` is non-empty. Without
440
- * `--human`, `run402 gitvault status` is unchanged: it always prints the
441
- * FULL terminal-loss statement verbatim on stderr regardless of warnings;
442
- * this compact view surfaces it only when it is actually live advice.
443
- */
444
- async function formatGitvaultHuman(s) {
445
- const lines = [];
446
- const remotePart = s.remote
447
- ? ` (remote '${s.remote.name}'${s.remote.matches ? "" : " — points at a DIFFERENT project"})`
448
- : " (no local remote)";
449
- lines.push(`Address: ${formatGitvaultAddress(s)}${remotePart}`);
450
-
451
- if (!s.vault) {
452
- // A normal shape (protocol D183) — no vault allocated for this project
453
- // yet. Nothing below this line is knowable, so it is not fabricated.
454
- lines.push("Vault: not allocated yet for this project — run 'run402 gitvault init' to allocate one.");
455
- if (s.warnings.length > 0) lines.push(`Warnings: ${s.warnings.map((w) => w.message).join(" ")}`);
456
- return lines.join("\n");
457
- }
458
-
459
- if (s.refs) {
460
- const count = Object.keys(s.refs).length;
461
- const head = !s.head_target
462
- ? "(none yet)"
463
- : s.head_target.kind === "symref"
464
- ? s.head_target.ref
465
- : `detached @ ${s.head_target.oid}`;
466
- lines.push(`HEAD: ${head} (${count} ref${count === 1 ? "" : "s"})`);
467
- } else {
468
- lines.push("HEAD: (not materialized — pass --refs to see HEAD/ref count)");
469
- }
470
-
471
- const { generationToBigInt } = await import("#sdk/node");
472
- const decimal = (g) => (g ? generationToBigInt(g).toString() : "none");
473
- lines.push(`Generations: authenticated ${decimal(s.pins.highest_authenticated)}, materialized ${decimal(s.pins.highest_materialized)}`);
474
-
475
- // Bytes + object count — pulled from the vault record `status()` ALREADY
476
- // fetched (no new network read, per the ask). `objects` is per-object-kind
477
- // counts; summed for one number a human can glance at.
478
- const storage = s.vault.storage;
479
- const objectCount = storage?.objects ? Object.values(storage.objects).reduce((sum, n) => sum + Number(n), 0) : null;
480
- lines.push(storage ? `Storage: ${storage.source_bytes} byte(s)${objectCount != null ? ` across ${objectCount} object(s)` : ""}` : "Storage: unknown");
481
-
482
- const decryptPart = !s.keystore.holds_repo_key
483
- ? "CANNOT decrypt (no key in this machine's keystore)"
484
- : s.keystore.can_sign
485
- ? "can decrypt and publish"
486
- : "can decrypt (read-only — no signing key)";
487
- lines.push(`This machine: ${decryptPart}. Policy: ${s.gitvault_policy ?? "(none)"}`);
488
-
489
- if (s.warnings.length > 0) lines.push(`Warnings: ${s.warnings.map((w) => w.message).join(" ")}`);
490
-
491
- return lines.join("\n");
492
- }
493
-
494
- async function status(args) {
495
- const a = normalizeArgv(args);
496
- assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--refs", "--human", "--help", "-h"], COMMON_VALUE_FLAGS);
497
- requirePositionalCount(a, COMMON_VALUE_FLAGS, {
498
- min: 0, max: 0, command: "run402 gitvault status", missing: "",
499
- });
500
- // kychee-com/run402#569 — an explicit opt-in per the cli-output-contract
501
- // (openspec/specs/cli-output-contract/spec.md: raw/human stdout REQUIRES
502
- // one), the same shape `run402 up`'s own `--human` already uses. Without
503
- // it, behavior is byte-identical to before this flag existed.
504
- const human = a.includes("--human");
505
- if (human && a.includes("--json")) {
506
- fail({
507
- code: "BAD_USAGE",
508
- message: "--human cannot be combined with --json.",
509
- details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") },
510
- });
511
- }
512
- const target = await vaultTarget(a);
513
- if (a.includes("--refs")) target.refs = true;
514
- try {
515
- const s = await getSdk().gitvault.status(target);
516
- if (human) {
517
- // The human view REPLACES the JSON dump — it is the sanctioned
518
- // exception the CLI-wide `--json` no-op convention already carves out
519
- // for a command's OWN `--human` flag (see argparse.mjs's header
520
- // comment). No new network read: everything below is already present
521
- // on `s`, the SAME status() call the JSON path made.
522
- console.log(await formatGitvaultHuman(s));
523
- return;
524
- }
525
- console.log(JSON.stringify(s, null, 2));
526
- printTerminalLoss(s);
527
- // Two facts the user otherwise has to leave the CLI for: which vault this
528
- // checkout is wired to, and what the control plane says is in it.
529
- if (s.remote) {
530
- // `matches` is a TRI-STATE (kychee-com/run402#562): `false` is a real
531
- // mismatch; `null` only means a slug-form remote has not resolved on
532
- // this machine yet — that is NOT evidence of anything wrong, so it
533
- // gets a neutral note, never the mismatch warning.
534
- const suffix =
535
- s.remote.matches === false ? " ← points at a DIFFERENT project than this status"
536
- : s.remote.matches === null ? ` (${s.remote.reason})`
537
- : "";
538
- console.error(`remote '${s.remote.name}': ${s.remote.url}${suffix}`);
539
- }
540
- // The id-pinning state (design D6, task 4.5): a slug-form remote pins
541
- // repo_id in local git state the first time it resolves; id-form pins
542
- // nothing (it needs no pin — see resolveGitvaultAddress's doc comment).
543
- if (s.pinned) {
544
- console.error(
545
- `pinned: repo_id ${s.pinned.repo_id}` +
546
- (s.pinned.resolved_from ? ` (resolved from run402::${s.pinned.resolved_from.org_slug}/${s.pinned.resolved_from.repo_name})` : ""),
547
- );
548
- }
549
- if (s.refs) {
550
- const names = Object.keys(s.refs).sort();
551
- console.error(names.length === 0 ? "refs: (none yet)" : `refs (${names.length}):`);
552
- for (const ref of names) console.error(` ${s.refs[ref]} ${ref}`);
553
- if (s.head_target) {
554
- console.error(s.head_target.kind === "symref" ? ` HEAD -> ${s.head_target.ref}` : ` HEAD ${s.head_target.oid} (detached)`);
555
- }
556
- }
557
- // Advisories are echoed EXACTLY as the SDK reported them. Nothing is
558
- // synthesized here — in particular a project that has never deployed gets
559
- // no deploy-related warning, because a vault-only project is a first-class
560
- // shape (protocol D183), not a half-configured deploy.
561
- for (const w of s.warnings) console.error(`warning (${w.kind}): ${w.message}`);
562
- for (const n of s.next_actions) console.error(`next: ${n.action}${n.command ? ` — ${n.command}` : ""}`);
563
- } catch (err) {
564
- reportSdkError(err);
565
- }
566
- }
567
-
568
- /**
569
- * D5 (repo-first-onramp task 2.5): one verb per operation. Renamed from
570
- * `push` — `push` now means exactly one thing everywhere: `git push`. The
571
- * capture lane keeps its old function name internally to minimize churn;
572
- * only the dispatched SUBCOMMAND name changed (see `run()` below, where
573
- * `gitvault push` survives one release as a deprecation-warning alias).
574
- */
575
- /**
576
- * D6 (repo-first-onramp task 4): when neither `--repo` nor `--project` was
577
- * given explicitly, look at the local `run402`/`origin` remote (in that
578
- * order, mirroring `scaffoldRemote`'s own naming) and, if it is a SLUG-form
579
- * address (`run402::<org-slug>/<name>`), return the parsed address so
580
- * `snapshot` can push-to-create through it — the same address-form
581
- * resolution `git push` drives via the remote helper. `null` for an
582
- * id-form remote, no remote at all, or an explicit `--repo`/`--project`.
583
- */
584
- async function detectSlugFormRemote(a, repoDir) {
585
- if (flagValue(a, "--repo") != null || flagValue(a, "--project") != null) return null;
586
- const { hardenedGit } = await import("#sdk/node");
587
- const { parseGitvaultRemoteUrl, gitvaultRemoteAddressForm } = await import("#sdk");
588
- for (const name of ["run402", "origin"]) {
589
- let url;
590
- try {
591
- url = (await hardenedGit(repoDir, ["remote", "get-url", name])).text().trim();
592
- } catch {
593
- continue;
594
- }
595
- if (!url) continue;
596
- const address = parseGitvaultRemoteUrl(url);
597
- if (address && gitvaultRemoteAddressForm(address) === "slug") return address;
598
- }
599
- return null;
600
- }
601
-
602
- async function snapshot(args) {
603
- const a = normalizeArgv(args);
604
- const valueFlags = [...COMMON_VALUE_FLAGS, "--message"];
605
- assertKnownFlags(a, [...valueFlags, "--checkpoint", "--dry-run", "--help", "-h"], valueFlags);
606
- requirePositionalCount(a, valueFlags, {
607
- min: 0, max: 0, command: "run402 gitvault snapshot", missing: "",
608
- });
609
- const dryRun = a.includes("--dry-run");
610
- const message = flagValue(a, "--message");
611
- const repoDir = process.cwd();
612
- const address = await detectSlugFormRemote(a, repoDir);
613
- // D2: lazily allocate the vault on first push when there is a project to
614
- // resolve the owning org from — the same resolution `gitvault init` uses.
615
- // `--repo`-only addressing has nothing to create FROM (no project_id), so
616
- // it is skipped there, matching `open()`'s own precedence. Skipped
617
- // entirely for a slug-form remote (`address` above) — that resolves
618
- // through the address, not a project_id, and needs no separate org_id.
619
- //
620
- // Skipped ENTIRELY for --dry-run (kychee-com/run402#565): org resolution
621
- // exists only to feed lazy allocation, and a dry run never allocates — the
622
- // read would cost a network round-trip for a fact `planPush` never uses.
623
- const target = address ? { repo_dir: repoDir } : await vaultTarget(a);
624
- const orgId = !address && !dryRun && target.project_id ? await resolveOwningOrgId(target.project_id) : null;
625
- const opts = {
626
- ...target,
627
- ...(address ? { address } : {}),
628
- ...(orgId ? { org_id: orgId } : {}),
629
- // The gitvault_commit line is progress, not payload: print it the moment
630
- // the snapshot exists, well before the publication round-trips finish, so
631
- // a human watching a slow push sees what is being pushed. Fires for a
632
- // dry run too — the capture itself is real, local work.
633
- onCommitLine: (line) => console.error(line),
634
- // Fires synchronously, BEFORE the capture/publish that follows — printed
635
- // here rather than deferred past `push()`'s return so the receipt is
636
- // never lost if a later step in the SAME push fails after allocation
637
- // already landed on the server. Never fires for --dry-run: `planPush`
638
- // never allocates, so this callback is simply unused there.
639
- onVaultCreated: async (created) => {
640
- console.error("");
641
- console.error(`vault allocated (genesis ${created.genesis_sha256}) — one-shot recovery receipt, keep many copies:`);
642
- console.error(JSON.stringify(created.recovery_receipt));
643
- await printKeystoreLocation();
644
- console.error("");
645
- },
646
- };
647
- // The message rides on `snapshot`, which is what `captureSnapshot` reads —
648
- // and, since 5.12b removed the dead top-level `push({ message })` field, is
649
- // the ONE place it can ride.
650
- if (message != null) opts.snapshot = { message };
651
- if (a.includes("--checkpoint")) opts.checkpoint = true;
652
- try {
653
- if (dryRun) {
654
- // kychee-com/run402#565: a REAL dry run — the same local pipeline
655
- // `push` runs (capture, pack building, encryption sizing), stopping
656
- // before the two network mutations. Nothing is published; the JSON
657
- // report is the entire contract, so it goes on stdout like every other
658
- // gitvault verb's payload.
659
- const plan = await getSdk().gitvault.planPush(opts);
660
- console.log(JSON.stringify(plan, null, 2));
661
- if (plan.allocation_needed) {
662
- console.error("dry-run: no vault allocated for this project yet — a real snapshot would allocate one first; object/byte sizing is not knowable until then");
663
- } else {
664
- console.error(
665
- `dry-run: would publish generation ${plan.would_admit_generation} (${plan.would_admit_generation_decimal}, ${plan.form}) — ` +
666
- `${plan.object_count} object(s), ${plan.encrypted_bytes} encrypted byte(s) (${plan.raw_bytes} raw)`,
667
- );
668
- }
669
- return;
670
- }
671
- const result = await getSdk().gitvault.push(opts);
672
- console.log(JSON.stringify(result, null, 2));
673
- console.error(`published generation ${result.generation} (${result.form})`);
674
- } catch (err) {
675
- reportSdkError(err);
676
- }
677
- }
678
-
679
- async function compact(args) {
680
- const a = normalizeArgv(args);
681
- assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
682
- requirePositionalCount(a, COMMON_VALUE_FLAGS, {
683
- min: 0, max: 0, command: "run402 gitvault compact", missing: "",
684
- });
685
- try {
686
- const result = await getSdk().gitvault.compact(await vaultTarget(a));
687
- console.log(JSON.stringify(result, null, 2));
688
- console.error(
689
- `checkpoint published at generation ${result.generation}: ` +
690
- `${result.covered_refs} ref(s), ${result.covered_roots} retention root(s).`,
691
- );
692
- if (!result.cutoff_bound) {
693
- // Say what actually happened rather than reporting a clean compaction:
694
- // without a ticket no root can leave the map, so this run reclaimed
695
- // nothing from expiry.
696
- console.error(
697
- "no retention-cutoff ticket was obtained, so roots were RETAINED — expiry is permissive. " +
698
- "The checkpoint published, but no expired root left the map; re-run compact once the ticket route answers.",
699
- );
700
- }
701
- } catch (err) {
702
- reportSdkError(err);
703
- }
704
- }
705
-
706
- /** Read a protocol object a prior planning run (or r402s-verify) wrote to disk. */
707
- function readJsonFile(flag, path) {
708
- let text;
709
- try {
710
- text = readFileSync(path, "utf8");
711
- } catch (err) {
712
- fail({
713
- code: "BAD_USAGE",
714
- message: `${flag} ${path} could not be read: ${err?.message ?? String(err)}`,
715
- hint: "Point it at the file a prior `run402 gitvault prune` (or r402s-verify) wrote.",
716
- });
717
- }
718
- try {
719
- return JSON.parse(text);
720
- } catch (err) {
721
- fail({
722
- code: "BAD_USAGE",
723
- message: `${flag} ${path} is not valid JSON: ${err?.message ?? String(err)}`,
724
- hint: "Pass the file verbatim; do not reformat or re-serialize it.",
725
- });
726
- }
727
- }
728
-
729
- async function prune(args) {
730
- const a = normalizeArgv(args);
731
- const valueFlags = [...COMMON_VALUE_FLAGS, "--intent-core", "--verifier-receipt"];
732
- assertKnownFlags(a, [...valueFlags, "--submit", "--wait", "--help", "-h"], valueFlags);
733
- requirePositionalCount(a, valueFlags, {
734
- min: 0, max: 0, command: "run402 gitvault prune", missing: "",
735
- });
736
- const submitting = a.includes("--submit");
737
- const corePath = flagValue(a, "--intent-core");
738
- const receiptPath = flagValue(a, "--verifier-receipt");
739
- // Refuse the half-specified submit here rather than planning and silently
740
- // discarding the flags — an agent that typed --submit meant to submit.
741
- if (submitting && (corePath == null || receiptPath == null)) {
742
- fail({
743
- code: "BAD_USAGE",
744
- message: "run402 gitvault prune --submit needs both --intent-core and --verifier-receipt.",
745
- hint: "Plan first (`run402 gitvault prune`), save its `intent_core`, run r402s-verify against it, then submit both.",
746
- });
747
- }
748
- if (!submitting && (corePath != null || receiptPath != null)) {
749
- fail({
750
- code: "BAD_USAGE",
751
- message: "--intent-core / --verifier-receipt only apply with --submit.",
752
- hint: "Add --submit, or drop the flags to plan.",
753
- });
754
- }
755
- const opts = await vaultTarget(a);
756
- if (submitting) {
757
- opts.submit = {
758
- core: readJsonFile("--intent-core", corePath),
759
- verifier_receipt: readJsonFile("--verifier-receipt", receiptPath),
760
- };
761
- if (a.includes("--wait")) opts.submit.wait = {};
762
- }
763
- try {
764
- const result = await getSdk().gitvault.prune(opts);
765
- console.log(JSON.stringify(result, null, 2));
766
- // Never imply a deletion. State what actually happened, then reproduce the
767
- // SDK's own note verbatim rather than summarizing it.
768
- if (!result.submitted) {
769
- console.error(
770
- result.blocked_reason
771
- ? `planned — nothing to submit: ${result.blocked_reason}`
772
- : `planned — nothing submitted. ${result.object_candidates.length} object(s) proposed for deletion` +
773
- `${result.deferred_object_count > 0 ? ` (${result.deferred_object_count} more deferred to a later intent)` : ""}` +
774
- `; ${result.eligible_count} retention root(s) past their window, ${result.retained_count} retained.`,
775
- );
776
- if (result.intent_core_sha256) {
777
- console.error(`intent_core_sha256: ${result.intent_core_sha256} — run r402s-verify against this core, then re-run with --submit.`);
778
- }
779
- } else if (result.confirmation?.outcome) {
780
- console.error(
781
- `submitted — the signed completion reports ${result.confirmation.deleted.length} deleted, ` +
782
- `${result.confirmation.present.length} still present` +
783
- `${result.confirmation.unadjudicated.length > 0 ? `, ${result.confirmation.unadjudicated.length} unadjudicated` : ""}.`,
784
- );
785
- } else {
786
- console.error("submitted — no completion yet. Nothing is deleted until the control-plane-signed completion says so; re-run with --wait or poll the intent.");
787
- }
788
- console.error(result.note);
789
- } catch (err) {
790
- reportSdkError(err);
791
- }
792
- }
793
-
794
- async function verify(args) {
795
- const a = normalizeArgv(args);
796
- const valueFlags = [...COMMON_VALUE_FLAGS, "--budget"];
797
- assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
798
- requirePositionalCount(a, valueFlags, {
799
- min: 0, max: 0, command: "run402 gitvault verify", missing: "",
800
- });
801
- const target = await vaultTarget(a);
802
- const budget = flagValue(a, "--budget");
803
- if (budget != null) target.verification_budget = parseIntegerFlag("--budget", budget, { min: 1 });
804
- try {
805
- const state = await getSdk().gitvault.verify(target);
806
- console.log(JSON.stringify(state, null, 2));
807
- console.error(`verified through generation ${state.generation}`);
808
- } catch (err) {
809
- reportSdkError(err);
810
- }
811
- }
812
-
813
117
  export async function run(sub, args) {
814
118
  const argv = Array.isArray(args) ? args : [];
815
119
  if (!sub || hasHelp([sub, ...argv])) {
@@ -817,46 +121,44 @@ export async function run(sub, args) {
817
121
  process.exit(0);
818
122
  }
819
123
  switch (sub) {
820
- case "init": {
821
- await init(argv);
124
+ case "init":
125
+ movedResponse("init");
126
+ break;
127
+ case "status":
128
+ movedResponse("status");
129
+ break;
130
+ case "snapshot":
131
+ movedResponse("snapshot");
132
+ break;
133
+ case "policy":
134
+ movedResponse("policy");
822
135
  break;
823
- }
824
- case "policy": {
825
- await policy(argv);
136
+ case "compact":
137
+ movedResponse("compact");
826
138
  break;
827
- }
828
- case "status": {
829
- await status(argv);
139
+ case "prune":
140
+ movedResponse("prune");
830
141
  break;
831
- }
832
- case "snapshot": {
833
- await snapshot(argv);
142
+ case "verify":
143
+ movedResponse("verify");
834
144
  break;
835
- }
836
- case "push": {
837
- // D5: one verb per operation — "push" now means exactly one thing,
838
- // `git push`. Retained as a deprecation-warning alias for ONE release
839
- // (pre-launch, the benchmark gate prefers the rename now over an
840
- // alias forever); it will be removed next release.
841
- console.error("`run402 gitvault push` is deprecated and will be removed in the next release — use `run402 gitvault snapshot` instead.");
842
- await snapshot(argv);
145
+ case "mirror":
146
+ movedResponse("mirror");
843
147
  break;
844
- }
845
- case "compact": {
846
- await compact(argv);
148
+ case "recover":
149
+ movedResponse("recover");
847
150
  break;
848
- }
849
- case "prune": {
850
- await prune(argv);
151
+ case "push":
152
+ removedResponse("push");
851
153
  break;
852
- }
853
- case "verify": {
854
- await verify(argv);
154
+ case "reconcile":
155
+ removedResponse("reconcile");
855
156
  break;
856
- }
857
157
  default:
858
- failUnknownSubcommand("gitvault", sub, {
859
- hint: "Run `run402 gitvault --help` for usage.",
158
+ fail({
159
+ code: "UNKNOWN_SUBCOMMAND",
160
+ message: `run402 gitvault ${sub}: unknown, and gitvault itself is retired.`,
161
+ hint: "Run `run402 repos --help` for the current surface.",
860
162
  });
861
163
  }
862
164
  }