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/README.md CHANGED
@@ -52,7 +52,9 @@ For app manifests with `verify.http[]`, `up` runs HTTP checks after deploy. Fres
52
52
 
53
53
  For typed `run402.deploy.ts` configs, pass `--manifest` explicitly because TypeScript/JavaScript configs execute local code. Use `--check` for local-only import/normalize/file validation, `--print-spec` to inspect the normalized `ReleaseSpec`, `--plan` for a gateway-reviewed non-deploying plan, and `--require-plan <plan_id>` to apply only that reviewed intent. Warning flags are not used with `--require-plan`; the reviewed plan binds the exact warning/destructive set. Run402 Core skips Cloud allowance/tier prerequisites and fails closed when no Core project is selected.
54
54
 
55
- ### Vault-only repos (zero deploy ceremony)
55
+ ### repos — host-blind encrypted git repos (zero deploy ceremony)
56
+
57
+ One noun, twelve verbs (repo-surface-consolidation; `repo` singular resolves identically). `run402 gitvault <verb>` is RETIRED — every old spelling now answers `COMMAND_MOVED`/`COMMAND_REMOVED`.
56
58
 
57
59
  ```bash
58
60
  run402 init # once per machine
@@ -61,13 +63,14 @@ git push -u origin main # allocates the vault on first pus
61
63
 
62
64
  # or, one call: provision + allocate + scaffold, nothing deployed
63
65
  run402 repos create my-notes
66
+ run402 repos view --human
64
67
  run402 repos list --org org_1a2b3c
65
- run402 repos delete prj_xyz --force # refuses without --force while the vault holds generations
68
+ run402 repos delete --project prj_xyz --force # refuses without --force while the vault holds generations
66
69
  ```
67
70
 
68
- A hosted git remote, encrypted before it leaves the machine — no deploy, no manifest, no app. `origin` is claimed additively — an existing `origin` is never touched, the run402 remote falls back to `run402` instead. `repos create|list|delete|name` and `gitvault`'s mutating verbs (`init`, `snapshot`, `policy`, `compact`, `prune`) are CLI/OpenClaw-only by design — no MCP tool exists or will exist for them (one-shot recovery receipts, immutable generations, irreversible delete). See `run402 gitvault --help` and `run402 repos --help` for the full surface, and the CLI reference's `gitvault` / `repos` sections for the terminal-loss statement and the progressive backup warning.
71
+ A hosted git remote, encrypted before it leaves the machine — no deploy, no manifest, no app. `origin` is claimed additively — an existing `origin` is never touched, the run402 remote falls back to `run402` instead. Every mutating `repos` verb (`create`, `rename`, `delete`, `snapshot`, `policy`, `mirror`, `gc`) is CLI/OpenClaw-only by design — no MCP tool exists or will exist for them (one-shot recovery receipts, immutable generations, irreversible delete). Three READ-ONLY tools do exist — `repos_view`, `repos_list_heads`, `repos_fsck` teaching only `repos` spellings. See `run402 repos --help` for the full tiered surface (common: `create`/`view`/`list`; occasional: `snapshot`/`mirror`/`recover`; lifecycle: `rename`/`delete`; maintenance: `fsck`/`gc`/`access`/`policy`), and the CLI reference's `repos` section for the terminal-loss statement and the progressive backup warning.
69
72
 
70
- **Named addressing (design D6).** `run402 org slug <slug>` (owner-only, small one-time fee) claims an org's globally-unique, address-form slug, after which `run402::<slug>/<name>` addresses any repo under it — `git push` to a name that doesn't exist yet push-to-creates it. `run402 repos name <name> [--project <id>]` claims the per-org-unique `<name>` half explicitly (no fee); `repos create` claims one automatically, best-effort, when the org already has a slug. Also CLI/SDK-only — no MCP tool.
73
+ **Named addressing (design D6).** `run402 org slug <slug>` (owner-only, small one-time fee) claims an org's globally-unique, address-form slug, after which `run402::<slug>/<name>` addresses any repo under it — `git push` to a name that doesn't exist yet push-to-creates it. `run402 repos rename <name> [--project <id>]` claims the per-org-unique `<name>` half explicitly (no fee, absorbs the old `repos name`); `repos create` claims one automatically, best-effort, when the org already has a slug. Also CLI/SDK-only — no MCP tool.
71
74
 
72
75
  ### Allowance
73
76
 
package/cli.mjs CHANGED
@@ -32,7 +32,7 @@ SET UP — get an agent funded and entitled
32
32
  BUILD & SHIP — the app and everything it serves
33
33
  up Provision/link/deploy the current app with SDK orchestration
34
34
  projects Manage projects (provision, list, get, sql, delete)
35
- repos Vault-only hosted encrypted repos, zero deploy ceremony (create, list, delete)
35
+ repos Host-blind encrypted git repos: create/view/list, snapshot/mirror/recover, fsck/gc/access/policy (repo aliases repos)
36
36
  deploy Unified deploy operations (requires active tier)
37
37
  apply Alias for deploy apply; supports --rehearse for migration rehearsal
38
38
  functions Manage serverless functions (deploy, invoke, logs, list, delete)
@@ -84,7 +84,7 @@ PLATFORM — everything else, and the things still finding a home
84
84
  transfer Two-party project transfer (init, preview, list, accept, cancel)
85
85
  cloud Cloud portability archive export (archives create/download/status)
86
86
  archives Inspect and verify portable project archives locally
87
- gitvault Host-blind encrypted Git remote (init/status/push/policy/compact/prune/verify)
87
+ gitvault RETIRED — moved to repos (repo-surface-consolidation); every spelling answers COMMAND_MOVED/COMMAND_REMOVED
88
88
  buzz Buzz human/community/agent control-plane workflows
89
89
  apps Browse and manage the app marketplace
90
90
  ai AI translation and moderation tools
@@ -227,6 +227,14 @@ switch (cmd) {
227
227
  await run(sub, rest);
228
228
  break;
229
229
  }
230
+ // `repo` singular resolves identically to `repos` (design D1 — agents type
231
+ // what `gh repo` already taught them). Its own case block, not a
232
+ // fall-through, so cli-conventions-gate's per-line family scanner sees it.
233
+ case "repo": {
234
+ const { run } = await import("./lib/repos.mjs");
235
+ await run(sub, rest);
236
+ break;
237
+ }
230
238
  case "apply": {
231
239
  const { runDeployV2 } = await import("./lib/deploy-v2.mjs");
232
240
  await runDeployV2("apply", [sub, ...rest].filter(Boolean));
package/lib/assets.mjs CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  import { basename, dirname, resolve as resolvePath } from "node:path";
29
29
  import { pipeline } from "node:stream/promises";
30
30
 
31
- import { resolveProjectId } from "./config.mjs";
31
+ import { resolveProjectIdAllowingLegacyEnv } from "./config.mjs";
32
32
  import { getSdk } from "./sdk.mjs";
33
33
  import { reportSdkError, fail } from "./sdk-errors.mjs";
34
34
  import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag, failUnknownSubcommand } from "./argparse.mjs";
@@ -472,10 +472,9 @@ function computeDestKey(filePath, keyOpt) {
472
472
  return keyOpt;
473
473
  }
474
474
 
475
- async function put(projectId, argv) {
475
+ async function put(argv) {
476
476
  const opts = parseArgs(argv);
477
- opts.project = opts.project || projectId;
478
- const resolvedId = resolveProjectId(opts.project);
477
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
479
478
 
480
479
  if (opts.positional.length === 0) die("At least one file path is required");
481
480
  if (opts.positional.length > 1 && opts.key && !opts.key.endsWith("/")) {
@@ -499,10 +498,9 @@ async function put(projectId, argv) {
499
498
  // get
500
499
  // ---------------------------------------------------------------------------
501
500
 
502
- async function get(projectId, argv) {
501
+ async function get(argv) {
503
502
  const opts = parseArgs(argv);
504
- opts.project = opts.project || projectId;
505
- const resolvedId = resolveProjectId(opts.project);
503
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
506
504
  if (opts.positional.length === 0) die("Key required");
507
505
  if (opts.positional.length > 1) die("blob get expects exactly one key");
508
506
  if (!opts.output) die("--output <file> required");
@@ -526,10 +524,9 @@ async function get(projectId, argv) {
526
524
  // ls
527
525
  // ---------------------------------------------------------------------------
528
526
 
529
- async function ls(projectId, argv) {
527
+ async function ls(argv) {
530
528
  const opts = parseArgs(argv);
531
- opts.project = opts.project || projectId;
532
- const resolvedId = resolveProjectId(opts.project);
529
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
533
530
 
534
531
  try {
535
532
  const data = await getSdk().assets.ls(resolvedId, {
@@ -552,10 +549,9 @@ async function ls(projectId, argv) {
552
549
  // rm
553
550
  // ---------------------------------------------------------------------------
554
551
 
555
- async function rm(projectId, argv) {
552
+ async function rm(argv) {
556
553
  const opts = parseArgs(argv);
557
- opts.project = opts.project || projectId;
558
- const resolvedId = resolveProjectId(opts.project);
554
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
559
555
  if (opts.positional.length === 0) die("Key required");
560
556
  if (opts.positional.length > 1) die("blob rm expects exactly one key");
561
557
  const key = opts.positional[0];
@@ -572,10 +568,9 @@ async function rm(projectId, argv) {
572
568
  // sign
573
569
  // ---------------------------------------------------------------------------
574
570
 
575
- async function diagnose(projectId, argv) {
571
+ async function diagnose(argv) {
576
572
  const opts = parseArgs(argv);
577
- opts.project = opts.project || projectId;
578
- const resolvedId = resolveProjectId(opts.project);
573
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
579
574
  if (opts.positional.length === 0) die("URL required");
580
575
  if (opts.positional.length > 1) die("blob diagnose expects exactly one URL");
581
576
  const url = opts.positional[0];
@@ -623,10 +618,9 @@ function toCliDiagnoseEnvelope(env) {
623
618
  };
624
619
  }
625
620
 
626
- async function sign(projectId, argv) {
621
+ async function sign(argv) {
627
622
  const opts = parseArgs(argv);
628
- opts.project = opts.project || projectId;
629
- const resolvedId = resolveProjectId(opts.project);
623
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
630
624
  if (opts.positional.length === 0) die("Key required");
631
625
  if (opts.positional.length > 1) die("blob sign expects exactly one key");
632
626
  const key = opts.positional[0];
@@ -676,14 +670,13 @@ export async function run(sub, args) {
676
670
  console.log(SUB_HELP[sub] || HELP);
677
671
  process.exit(0);
678
672
  }
679
- const defaultProject = process.env.RUN402_PROJECT ?? null;
680
673
  switch (sub) {
681
- case "put": await put(defaultProject, args); break;
682
- case "get": await get(defaultProject, args); break;
683
- case "ls": await ls(defaultProject, args); break;
684
- case "rm": await rm(defaultProject, args); break;
685
- case "sign": await sign(defaultProject, args); break;
686
- case "diagnose": await diagnose(defaultProject, args); break;
674
+ case "put": await put(args); break;
675
+ case "get": await get(args); break;
676
+ case "ls": await ls(args); break;
677
+ case "rm": await rm(args); break;
678
+ case "sign": await sign(args); break;
679
+ case "diagnose": await diagnose(args); break;
687
680
  default:
688
681
  failUnknownSubcommand("assets", sub);
689
682
  }
package/lib/cdn.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
  * are bound to a SHA at upload time and never previously cached.
14
14
  */
15
15
 
16
- import { resolveProjectId } from "./config.mjs";
16
+ import { resolveProjectIdAllowingLegacyEnv } from "./config.mjs";
17
17
  import { getSdk } from "./sdk.mjs";
18
18
  import { reportSdkError, fail } from "./sdk-errors.mjs";
19
19
  import { assertKnownFlags, flagValue, normalizeArgv, parseIntegerFlag, positionalArgs, failUnknownSubcommand } from "./argparse.mjs";
@@ -91,10 +91,9 @@ function parseArgs(args) {
91
91
  return opts;
92
92
  }
93
93
 
94
- async function waitFresh(projectId, argv) {
94
+ async function waitFresh(argv) {
95
95
  const opts = parseArgs(argv);
96
- opts.project = opts.project || projectId;
97
- const resolvedId = resolveProjectId(opts.project);
96
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
98
97
  if (opts.positional.length === 0) die("URL required");
99
98
  const url = opts.positional[0];
100
99
  if (!opts.sha) die("--sha is required");
@@ -136,10 +135,9 @@ export async function run(sub, args) {
136
135
  console.log(SUB_HELP[sub] || HELP);
137
136
  process.exit(0);
138
137
  }
139
- const defaultProject = process.env.RUN402_PROJECT ?? null;
140
138
  switch (sub) {
141
139
  case "wait-fresh":
142
- await waitFresh(defaultProject, args);
140
+ await waitFresh(args);
143
141
  break;
144
142
  default:
145
143
  failUnknownSubcommand("cdn", sub);
@@ -255,32 +255,38 @@ export const COMMAND_MANIFEST = [
255
255
  { path: ["claims", "list"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub" },
256
256
  { path: ["claims", "release"], positionals: [p("claim_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["clm_1"], runStyle: "sub" },
257
257
 
258
- // ── gitvault (host-blind encrypted Git remote) ───────────────────────────
259
- // Every verb needs a real principal keystore, an allocated vault, and (for
260
- // all but `status`) a local git working tree, so the gate runs structural
261
- // checks only an in-process behavioral run would either no-op against the
258
+ // ── gitvault — RETIRED (repo-surface-consolidation, design D7) ──────────
259
+ // No manifest entries: `cli/lib/gitvault.mjs`'s dispatcher retired, and
260
+ // every `gitvault <verb>` now answers a structural COMMAND_MOVED (or, for
261
+ // `push`/`reconcile`, COMMAND_REMOVED) redirect that dispatches nothing
262
+ // see RESERVED_SUBCOMMANDS below, and "gitvault" in SKIPPED_FAMILIES.
263
+
264
+ // ── repos (repo-surface-consolidation — the consolidated 12-verb family) ─
265
+ // Every verb needs a real principal keystore and, for most, an allocated
266
+ // repo and a local git working tree, so the gate runs structural checks
267
+ // only — an in-process behavioral run would either no-op against the
262
268
  // universal `{}` fetch mock or touch the gate's own checkout.
263
- { path: ["gitvault", "init"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "allocates a vault: mints key material on this machine and runs the six-stage creation journal" },
264
- { path: ["gitvault", "status"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads the local principal keystore and the live vault record" },
265
- { path: ["gitvault", "snapshot"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "captures the cwd git working tree and publishes a signed head" },
266
- // D5 (repo-first-onramp task 2.5): `push` is a deprecation-warning alias
267
- // for `snapshot`, retained for exactly one release — same structural
268
- // shape, same skip reason, so it stays covered by this gate too.
269
- { path: ["gitvault", "push"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "deprecated alias for `snapshot` captures the cwd git working tree and publishes a signed head" },
270
- { path: ["gitvault", "policy"], positionals: [p("gitvault_policy")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["required"], runStyle: "sub", skipBehavioral: "owner + step-up mutation of the live project's activation policy" },
271
- { path: ["gitvault", "compact"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "takes a maintenance lease and builds a checkpoint from the local repository" },
272
- { path: ["gitvault", "prune"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "materializes the live vault head to enumerate retention roots" },
273
- { path: ["gitvault", "verify"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "walks the live head chain against the keystore's authenticated pin" },
274
-
275
- // ── repos (vault-only porcelain, repo-first-onramp D8, task 2.6) ────────
276
- // `create` writes real git state into cwd and allocates a vault; `list`
277
- // and `delete` read/mutate the live gitvault record same structural-
278
- // checks-only rationale as the gitvault family just above.
279
- { path: ["repos", "create"], positionals: [p("name")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["my-notes"], runStyle: "sub", skipBehavioral: "provisions a project, allocates a vault, and scaffolds a real git remote into cwd" },
280
- { path: ["repos", "list"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "cross-references live projects against their live gitvault status, one call per project" },
281
- { path: ["repos", "delete"], positionals: [], projectScoped: true, legacyPositionalProject: true, minimalArgs: [], runStyle: "sub", skipBehavioral: "irreversibly deletes a project after reading its live vault generation count" },
282
- // repo-first-onramp task 4.2 (design D6): explicit address-form name claim.
283
- { path: ["repos", "name"], positionals: [p("name")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["my-notes"], runStyle: "sub", skipBehavioral: "claims a per-org-unique repo name against a live project" },
269
+ { path: ["repos", "create"], positionals: [p("name", { required: false })], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["my-notes"], runStyle: "sub", skipBehavioral: "provisions a project, allocates a repo, and scaffolds a real git remote into cwd" },
270
+ { path: ["repos", "list"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads the live bulk vaults-by-org route, or falls back to a live per-project walk" },
271
+ { path: ["repos", "view"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads the local principal keystore and the live repo record" },
272
+ // repo-surface-consolidation D2 (`gh repo rename`): absorbs the old
273
+ // `repos name` same claim/rename, `--repo`/`--project` addressing added.
274
+ { path: ["repos", "rename"], positionals: [p("new_name")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["my-notes"], runStyle: "sub", skipBehavioral: "claims a per-org-unique repo name against a live project" },
275
+ { path: ["repos", "delete"], positionals: [], projectScoped: true, legacyPositionalProject: true, minimalArgs: [], runStyle: "sub", skipBehavioral: "irreversibly deletes a project after reading its live non-repo-resource state and vault generation count (design D9)" },
276
+ { path: ["repos", "snapshot"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "captures the cwd git working tree and publishes a signed head" },
277
+ { path: ["repos", "policy"], positionals: [p("repos_policy")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["required"], runStyle: "sub", skipBehavioral: "owner + step-up mutation of the live project's activation policy" },
278
+ // design D4 ONE flag-driven verb; `<destination>` is a real attribute
279
+ // (not a sub-verb literal like the old `mirror set/remove/...`), so this
280
+ // stays a single manifest entry.
281
+ { path: ["repos", "mirror"], positionals: [p("destination", { required: false })], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads/writes mirror destination config beside the keystore and may move real bytes into a customer-owned bucket" },
282
+ { path: ["repos", "fsck"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "walks the live head chain and materializes the ref map against the keystore's local pins" },
283
+ { path: ["repos", "gc"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "publishes a checkpoint under a maintenance lease and materializes the live vault head to enumerate retention roots" },
284
+ // `access repair` is a nested sub-verb literal (same shape the old
285
+ // `gitvault mirror set/remove/...` used) two manifest entries, one
286
+ // dispatched `run("access", ...)` case in repos.mjs, matching precedent.
287
+ { path: ["repos", "access"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads the live org encryption-key directory + vault envelope recipients" },
288
+ { path: ["repos", "access", "repair"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "always refuses — gated on gitvault-human-envelopes' epoch-rotation work, not shipped yet" },
289
+ { path: ["repos", "recover"], positionals: [p("source")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["s3://example-mirror-bucket", "--out", "__SCRATCH_DIR__/recover-out"], runStyle: "sub", skipBehavioral: "materializes a git repository from a mirror source, offline, with no server call" },
284
290
  { path: ["errors"], positionals: [p("fingerprint_id", { required: false })], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "merged" },
285
291
 
286
292
  // ── jobs ─────────────────────────────────────────────────────────────────
@@ -455,6 +461,15 @@ export const SKIPPED_FAMILIES = {
455
461
  // Split into `deliveries` / `contacts` / `subscriptions`; every subcommand
456
462
  // answers COMMAND_REMOVED naming its successor.
457
463
  "notifications": "reserved group; split by legible-cli-surface",
464
+ // repo-surface-consolidation D7: the dispatcher retired. Every
465
+ // `gitvault <verb>` answers COMMAND_MOVED/COMMAND_REMOVED (see
466
+ // RESERVED_SUBCOMMANDS below for the per-verb list) — the family stays
467
+ // dispatched in cli.mjs (so the redirect fires instead of UNKNOWN_COMMAND)
468
+ // but has zero manifest entries, since a redirect dispatches nothing.
469
+ "gitvault": "retired; every subcommand answers COMMAND_MOVED/COMMAND_REMOVED naming its `repos`/git successor",
470
+ // `repo` singular resolves identically to `repos` (design D1) — same
471
+ // module, same case block in cli.mjs, so it needs no manifest of its own.
472
+ "repo": "alias for `repos`, resolves identically (design D1)",
458
473
  };
459
474
 
460
475
  /**
@@ -480,4 +495,20 @@ export const RESERVED_SUBCOMMANDS = {
480
495
  "rooms:get": "moved to `messages get` — the verb acts on a message",
481
496
  "rooms:ack": "moved to `messages ack` — the verb acts on a message",
482
497
  "escalations:contacts": "merged into `contacts` — the ladder and Telegram channels are one question",
498
+ // repo-surface-consolidation D7: every `gitvault <verb>` spelling. Nine
499
+ // answer COMMAND_MOVED naming their `repos` successor; `push` and
500
+ // `reconcile` answer COMMAND_REMOVED (no equivalent successor for either —
501
+ // `push`'s one-release alias window is over, `reconcile` was a workaround
502
+ // with no permanent replacement, only a read at `repos access`).
503
+ "gitvault:init": "moved to `repos create --project <id>`",
504
+ "gitvault:status": "moved to `repos view`",
505
+ "gitvault:snapshot": "moved to `repos snapshot`",
506
+ "gitvault:policy": "moved to `repos policy`",
507
+ "gitvault:compact": "moved to `repos gc`",
508
+ "gitvault:prune": "moved to `repos gc`",
509
+ "gitvault:verify": "moved to `repos fsck`",
510
+ "gitvault:mirror": "moved to `repos mirror`",
511
+ "gitvault:recover": "moved to `repos recover`",
512
+ "gitvault:push": "removed — its one-release deprecation-alias window is over; `git push` / `repos snapshot`",
513
+ "gitvault:reconcile": "removed — a workaround with no permanent successor; read `repos access` instead",
483
514
  };
package/lib/config.mjs CHANGED
@@ -139,6 +139,36 @@ export function resolveProjectId(id) {
139
139
  return projectId;
140
140
  }
141
141
 
142
+ /**
143
+ * `resolveProjectId`, but also honors the deprecated `RUN402_PROJECT` alias
144
+ * as a last-resort fallback.
145
+ *
146
+ * `RUN402_PROJECT_ID` is the canonical env var everywhere in the CLI — it's
147
+ * what `resolveProjectId`, `run402 doctor`, `org-context.mjs`, and every
148
+ * other project-scoped command read. Two commands (`cdn wait-fresh` and
149
+ * every `assets` subcommand) historically read the DIFFERENT, undocumented
150
+ * `RUN402_PROJECT` instead, so exporting the canonical `RUN402_PROJECT_ID`
151
+ * and running one of those two commands was a silent no-op: the export did
152
+ * nothing, and resolution quietly fell through to the active project.
153
+ *
154
+ * Precedence, highest first: an explicit `id` (e.g. `--project`) >
155
+ * `RUN402_PROJECT_ID` > the deprecated `RUN402_PROJECT` alias > the active
156
+ * project. The alias is only ever consulted when `RUN402_PROJECT_ID` is
157
+ * unset AND no explicit id was given — so it never silently overrides the
158
+ * canonical var or a flag. When the alias is what actually resolves the
159
+ * project, exactly one deprecation line goes to stderr (never stdout — the
160
+ * pipe contract keeps stdout pure JSON).
161
+ */
162
+ export function resolveProjectIdAllowingLegacyEnv(id) {
163
+ if (!id && !process.env.RUN402_PROJECT_ID && process.env.RUN402_PROJECT) {
164
+ process.stderr.write(
165
+ "warning: RUN402_PROJECT is deprecated and will be removed; set RUN402_PROJECT_ID instead.\n",
166
+ );
167
+ return resolveProjectId(process.env.RUN402_PROJECT);
168
+ }
169
+ return resolveProjectId(id);
170
+ }
171
+
142
172
  // Re-export core keystore functions for direct use
143
173
  export {
144
174
  configureApiBase,
package/lib/deploy-v2.mjs CHANGED
@@ -1281,9 +1281,24 @@ async function applyCmd(args) {
1281
1281
  ...("generation" in vaulted ? { generation: vaulted.generation } : {}),
1282
1282
  ...("push_error" in vaulted ? { push_error: vaulted.push_error } : {}),
1283
1283
  ...("deploy_error" in vaulted ? { deploy_error: vaulted.deploy_error } : {}),
1284
+ // Design D5/D6 (gitvault-human-envelopes task 4.1 + gitvault-mirror):
1285
+ // present only when this deploy landed a new generation — the SDK
1286
+ // omits both fields entirely on the other three outcomes rather than
1287
+ // faking a `skipped_*` value for something that never had a chance to
1288
+ // run. See `Gitvault.deploy`'s doc comment (sdk/src/namespaces/gitvault.ts).
1289
+ ...("mirror_push" in vaulted ? { mirror_push: vaulted.mirror_push } : {}),
1290
+ ...("reconcile_recipients" in vaulted ? { reconcile_recipients: vaulted.reconcile_recipients } : {}),
1284
1291
  next_actions: vaulted.next_actions,
1285
1292
  deploy: outcome.deploy,
1286
1293
  }, null, 2));
1294
+ // Design D6: the mirror result is reported BESIDE the vault outcome
1295
+ // above, on its own stderr line — a mirror failure never blocks the
1296
+ // deploy (mirrors `run402 gitvault snapshot`'s reporting).
1297
+ if (vaulted.mirror_push?.outcome === "pushed") {
1298
+ console.error(`mirror: pushed generation ${vaulted.generation} (${vaulted.mirror_push.summary?.objects_copied ?? 0} object(s) copied)`);
1299
+ } else if (vaulted.mirror_push?.outcome === "failed") {
1300
+ console.error(`mirror: dual-push FAILED (deploy is unaffected) — ${vaulted.mirror_push.error ?? "see mirror_push.summary.errors"}`);
1301
+ }
1287
1302
  // A non-activating outcome is a failed deploy even though it resolved
1288
1303
  // rather than threw: the five outcomes are a result type, not an error
1289
1304
  // channel, so the exit code has to carry the verdict.
package/lib/doctor.mjs CHANGED
@@ -575,13 +575,13 @@ export async function run(sub, args = []) {
575
575
  if (gv.gitvault_policy === "required" && !gv.keystore.holds_repo_key) {
576
576
  gaps.push(
577
577
  "gitvault_policy is 'required' but this machine holds no key for the vault — a deploy from here is refused with GITVAULT_CLIENT_UPGRADE_REQUIRED. " +
578
- "Run 'run402 gitvault init' (idempotent; resolves to the existing vault), or 'run402 gitvault policy grandfathered --reason <why>' to un-gate the project.",
578
+ "Run 'run402 repos create --project <id>' (idempotent; resolves to the existing repo), or 'run402 repos policy grandfathered --reason <why>' to un-gate the project.",
579
579
  );
580
580
  } else if (gv.gitvault_policy === "required" && !gv.keystore.can_sign) {
581
581
  gaps.push("gitvault_policy is 'required' and this keystore is read-only (no signing key) — it can verify but cannot publish the capture a deploy needs");
582
582
  }
583
583
  if (gv.pending_overrides > 0) {
584
- gaps.push(`${gv.pending_overrides} unvaulted-override journal(s) are still open — run 'run402 gitvault push' to drain them`);
584
+ gaps.push(`${gv.pending_overrides} unvaulted-override journal(s) are still open — run 'run402 repos snapshot' to drain them`);
585
585
  }
586
586
  // `matches` is a TRI-STATE (kychee-com/run402#562): `false` alone is
587
587
  // a real mismatch. `null` (a slug-form remote not yet resolved on
@@ -593,12 +593,45 @@ export async function run(sub, args = []) {
593
593
  // Echoed exactly as the SDK reported them — including the
594
594
  // doctor-persistent `grandfathered` advisory it owns.
595
595
  for (const w of gv.warnings ?? []) gaps.push(`${w.kind}: ${w.message}`);
596
+
597
+ // gitvault-mirror-and-recover task 4.3: mirror currency, reported
598
+ // ALONGSIDE (never in place of) the deploy-related gaps above, and
599
+ // never blocking `run402 deploy`'s own gate — the vault lane's
600
+ // outcome is unaffected regardless of mirror state (design D6).
601
+ // `mirror_currency` mirrors `mirror status`'s own tri-state:
602
+ // `current` / `stale` / `unknown` (mirror unreachable or vault
603
+ // unread) only STALE is actionable enough to become a warning; NO
604
+ // mirror configured is a purely informational, ungated advisory —
605
+ // most vaults have never opted in, and that is a normal shape.
606
+ if (gv.vault !== null && value.repo_id) {
607
+ try {
608
+ const mirrorStatus = await getSdk().gitvault.mirrorStatus({ repo_id: value.repo_id });
609
+ value.gitvault_mirror = {
610
+ configured: mirrorStatus.configured,
611
+ destination: mirrorStatus.destination,
612
+ mirrored_generation: mirrorStatus.mirrored_generation,
613
+ newest_generation: mirrorStatus.newest_generation,
614
+ is_current: mirrorStatus.is_current,
615
+ validity_not_freshness: mirrorStatus.validity_not_freshness,
616
+ keystore_still_required: mirrorStatus.keystore_still_required,
617
+ };
618
+ if (!mirrorStatus.configured) {
619
+ value.gitvault_mirror.advisory = "no ciphertext mirror is configured for this vault — the exit ramp is opt-in; 'run402 repos mirror <destination>' to configure one.";
620
+ } else if (mirrorStatus.is_current === false) {
621
+ gaps.push(`the ciphertext mirror at ${mirrorStatus.destination} is STALE (mirrored generation ${mirrorStatus.mirrored_generation ?? "(none)"}, vault newest ${mirrorStatus.newest_generation ?? "(none)"}) — ${mirrorStatus.closing_command}`);
622
+ }
623
+ } catch {
624
+ // Best-effort: a mirror status read failing is never a doctor
625
+ // failure, and never touches the deploy-related gaps above.
626
+ }
627
+ }
628
+
596
629
  checks.push({
597
630
  name: "gitvault",
598
631
  status: gaps.length > 0 ? "warning" : "ok",
599
632
  value: gaps.length > 0 ? { ...value, gaps } : value,
600
633
  hint: gv.vault === null
601
- ? `No vault for this project (that is a normal shape). Allocate one with 'run402 gitvault init'. Keystore: ${gv.keystore.root}`
634
+ ? `No vault for this project (that is a normal shape). Allocate one with 'run402 repos create --project <id>'. Keystore: ${gv.keystore.root}`
602
635
  : `Back up ${gv.keystore.root} — whole-machine or whole-keystore loss is terminal for vault history.`,
603
636
  });
604
637
  } catch (err) {
@@ -49,7 +49,7 @@ export async function scaffoldGitvaultRemote({ repoDir = process.cwd(), projectI
49
49
  const remote = await getSdk().gitvault.scaffoldRemote({ repo_dir: repoDir, org_id: resolvedOrgId, project_id: projectId });
50
50
  // `allocated: false` is stated, not left to be inferred: this is local
51
51
  // git only — no vault exists for the project yet (allocation happens
52
- // lazily on first push, or explicitly via `run402 gitvault init`).
52
+ // lazily on first push, or explicitly via `run402 repos create --project <id>`).
53
53
  out.gitvault = { ...remote, allocated: false };
54
54
  } catch (err) {
55
55
  out.gitvault = null;