run402 4.36.0 → 4.37.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 (43) hide show
  1. package/README.md +15 -0
  2. package/cli.mjs +6 -0
  3. package/git-remote-run402.mjs +69 -3
  4. package/lib/command-manifest.mjs +13 -1
  5. package/lib/functions.mjs +11 -3
  6. package/lib/gitvault-scaffold.mjs +73 -0
  7. package/lib/gitvault.mjs +72 -22
  8. package/lib/init.mjs +8 -3
  9. package/lib/projects.mjs +16 -0
  10. package/lib/repos.mjs +326 -0
  11. package/lib/rooms.mjs +5 -3
  12. package/lib/up.mjs +167 -29
  13. package/package.json +1 -1
  14. package/sdk/dist/errors.d.ts +1 -1
  15. package/sdk/dist/errors.d.ts.map +1 -1
  16. package/sdk/dist/errors.js.map +1 -1
  17. package/sdk/dist/index.d.ts +1 -1
  18. package/sdk/dist/index.d.ts.map +1 -1
  19. package/sdk/dist/index.js +1 -1
  20. package/sdk/dist/index.js.map +1 -1
  21. package/sdk/dist/namespaces/deploy.types.d.ts +9 -0
  22. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  23. package/sdk/dist/namespaces/deploy.types.js.map +1 -1
  24. package/sdk/dist/namespaces/gitvault.d.ts +133 -16
  25. package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
  26. package/sdk/dist/namespaces/gitvault.js +207 -16
  27. package/sdk/dist/namespaces/gitvault.js.map +1 -1
  28. package/sdk/dist/node/gitvault-apply.d.ts +18 -1
  29. package/sdk/dist/node/gitvault-apply.d.ts.map +1 -1
  30. package/sdk/dist/node/gitvault-apply.js +38 -1
  31. package/sdk/dist/node/gitvault-apply.js.map +1 -1
  32. package/sdk/dist/node/gitvault-creation-journal.d.ts +22 -0
  33. package/sdk/dist/node/gitvault-creation-journal.d.ts.map +1 -1
  34. package/sdk/dist/node/gitvault-creation-journal.js +28 -0
  35. package/sdk/dist/node/gitvault-creation-journal.js.map +1 -1
  36. package/sdk/dist/node/gitvault-open-or-create.d.ts +59 -0
  37. package/sdk/dist/node/gitvault-open-or-create.d.ts.map +1 -0
  38. package/sdk/dist/node/gitvault-open-or-create.js +62 -0
  39. package/sdk/dist/node/gitvault-open-or-create.js.map +1 -0
  40. package/sdk/dist/node/index.d.ts +3 -1
  41. package/sdk/dist/node/index.d.ts.map +1 -1
  42. package/sdk/dist/node/index.js +4 -1
  43. package/sdk/dist/node/index.js.map +1 -1
package/README.md CHANGED
@@ -52,6 +52,21 @@ 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)
56
+
57
+ ```bash
58
+ run402 init # once per machine
59
+ git remote add origin run402::<org_id>/<project_id>
60
+ git push -u origin main # allocates the vault on first push, publishes
61
+
62
+ # or, one call: provision + allocate + scaffold, nothing deployed
63
+ run402 repos create my-notes
64
+ run402 repos list --org org_1a2b3c
65
+ run402 repos delete prj_xyz --force # refuses without --force while the vault holds generations
66
+ ```
67
+
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` 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.
69
+
55
70
  ### Allowance
56
71
 
57
72
  ```bash
package/cli.mjs CHANGED
@@ -32,6 +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
36
  deploy Unified deploy operations (requires active tier)
36
37
  apply Alias for deploy apply; supports --rehearse for migration rehearsal
37
38
  functions Manage serverless functions (deploy, invoke, logs, list, delete)
@@ -221,6 +222,11 @@ switch (cmd) {
221
222
  await run(sub, rest);
222
223
  break;
223
224
  }
225
+ case "repos": {
226
+ const { run } = await import("./lib/repos.mjs");
227
+ await run(sub, rest);
228
+ break;
229
+ }
224
230
  case "apply": {
225
231
  const { runDeployV2 } = await import("./lib/deploy-v2.mjs");
226
232
  await runDeployV2("apply", [sub, ...rest].filter(Boolean));
@@ -19,6 +19,15 @@
19
19
  * push [+]<src>:<dst> → publish one atomic ref transaction
20
20
  * option <name> <value> → ok / unsupported, never a silent lie
21
21
  *
22
+ * LAZY ALLOCATION ON FIRST PUSH (design D2). `list` never creates anything —
23
+ * an unallocated vault reports as an empty ref set, exactly what a fresh
24
+ * repository looks like — but `push` does: `git push origin main` against a
25
+ * project whose vault does not exist yet runs the six-stage creation journal
26
+ * inline (`r.gitvault.openOrCreate`, the SDK-owned primitive), prints the
27
+ * one-shot recovery receipt and the keystore path to stderr, and then
28
+ * completes the push. One command, no prior `gitvault init`. `git ls-remote`
29
+ * / `fetch` stay pure reads and allocate nothing.
30
+ *
22
31
  * WHICH REPOSITORY (the fail-closed rule). `process.cwd()` is NOT the
23
32
  * repository. git identifies the repository with `GIT_DIR`, and during
24
33
  * `git clone` cwd is the directory clone was RUN FROM — routinely some other,
@@ -120,7 +129,11 @@ async function main(argv) {
120
129
  return 1;
121
130
  }
122
131
 
123
- const target = { project_id: address.project_id };
132
+ // `org_id` rides in the parsed address (`run402::<org_id>/<project_id>`),
133
+ // so it costs nothing extra to carry — it is exactly what D2's lazy
134
+ // creation needs to allocate an unresolved vault from `runPush` below, with
135
+ // no separate lookup.
136
+ const target = { project_id: address.project_id, org_id: address.org_id };
124
137
  let verbosity = 1;
125
138
 
126
139
  /**
@@ -150,8 +163,61 @@ async function main(argv) {
150
163
  /** Open the vault lazily — `capabilities` and `option` must never touch the network. */
151
164
  const openVault = async (repoDir) => (await getSdk().gitvault.open(repoDir ? { ...target, repo_dir: repoDir } : target)).vault;
152
165
 
166
+ /** A 404/absent-vault refusal — the "nothing here yet" shape, never a genuine failure to mask. */
167
+ function isVaultNotFound(err) {
168
+ return err?.status === 404 || err?.code === "RESOURCE_NOT_FOUND" || err?.code === "ROUTE_NOT_FOUND";
169
+ }
170
+
171
+ /**
172
+ * Open the vault, allocating it first when it does not exist yet (D2). Used
173
+ * ONLY by `runPush` — `list`/`fetch` stay pure reads and never create
174
+ * anything (see `runList`'s own not-found handling below).
175
+ *
176
+ * Prints the one-shot recovery receipt and the keystore path to stderr the
177
+ * moment allocation happens, per the client-surface spec: an agent reads
178
+ * stderr, and the receipt is worth exactly as many copies as get kept.
179
+ */
180
+ async function openOrCreateVault(repoDir) {
181
+ const result = await getSdk().gitvault.openOrCreate({ ...target, repo_dir: repoDir });
182
+ if (!result.found && result.created) {
183
+ note("");
184
+ note(`vault ${result.handle.repo_id} allocated (genesis ${result.created.genesis_sha256}) — one-shot recovery receipt, keep many copies:`);
185
+ note(JSON.stringify(result.created.recovery_receipt));
186
+ try {
187
+ const { getGitvaultKeystoreRoot } = await import("#sdk/node");
188
+ note(`keystore: ${getGitvaultKeystoreRoot()} — back this up; whole-machine or whole-keystore loss is terminal for vault history until human envelopes ship`);
189
+ } catch {
190
+ // Never let a diagnostic line fail a push that already allocated successfully.
191
+ }
192
+ try {
193
+ const { GITVAULT_ALLOCATION_POLICY_ADVISORY } = await import("./lib/gitvault-scaffold.mjs");
194
+ note(GITVAULT_ALLOCATION_POLICY_ADVISORY);
195
+ } catch {
196
+ // Same rule: advisory only, never fatal.
197
+ }
198
+ note("");
199
+ }
200
+ return result.handle.vault;
201
+ }
202
+
153
203
  async function runList() {
154
- const state = await (await openVault()).materialize();
204
+ let state;
205
+ try {
206
+ state = await (await openVault()).materialize();
207
+ } catch (err) {
208
+ // An unallocated vault is not an error here: `list` is the read half of
209
+ // the protocol dance and must never create anything on its own (D2
210
+ // scopes lazy creation to `push`). Reporting it as an EMPTY ref set is
211
+ // exactly what a fresh repository looks like to git, and `push` still
212
+ // runs `list` first either way — this is what lets a first push land in
213
+ // one command instead of `list` failing the whole exchange before
214
+ // `push` ever gets a turn.
215
+ if (isVaultNotFound(err)) {
216
+ endBlock();
217
+ return;
218
+ }
219
+ throw err;
220
+ }
155
221
  const refs = state.refs ?? {};
156
222
  for (const ref of Object.keys(refs).sort()) out(`${refs[ref]} ${ref}`);
157
223
  const head = state.head_target;
@@ -198,7 +264,7 @@ async function main(argv) {
198
264
  ? null
199
265
  : (await hardenedGit(repoDir, ["rev-parse", "--verify", "--end-of-options", spec.src])).text().trim());
200
266
  }
201
- const vault = await openVault(repoDir);
267
+ const vault = await openOrCreateVault(repoDir);
202
268
  const base = await vault.materialize();
203
269
  const updates = [];
204
270
  for (const spec of specs) {
@@ -257,11 +257,23 @@ export const COMMAND_MANIFEST = [
257
257
  // universal `{}` fetch mock or touch the gate's own checkout.
258
258
  { 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" },
259
259
  { path: ["gitvault", "status"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads the local principal keystore and the live vault record" },
260
- { path: ["gitvault", "push"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "captures the cwd git working tree and publishes a signed head" },
260
+ { path: ["gitvault", "snapshot"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "captures the cwd git working tree and publishes a signed head" },
261
+ // D5 (repo-first-onramp task 2.5): `push` is a deprecation-warning alias
262
+ // for `snapshot`, retained for exactly one release — same structural
263
+ // shape, same skip reason, so it stays covered by this gate too.
264
+ { 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" },
261
265
  { 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" },
262
266
  { path: ["gitvault", "compact"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "takes a maintenance lease and builds a checkpoint from the local repository" },
263
267
  { path: ["gitvault", "prune"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "materializes the live vault head to enumerate retention roots" },
264
268
  { path: ["gitvault", "verify"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "walks the live head chain against the keystore's authenticated pin" },
269
+
270
+ // ── repos (vault-only porcelain, repo-first-onramp D8, task 2.6) ────────
271
+ // `create` writes real git state into cwd and allocates a vault; `list`
272
+ // and `delete` read/mutate the live gitvault record — same structural-
273
+ // checks-only rationale as the gitvault family just above.
274
+ { 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" },
275
+ { 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" },
276
+ { path: ["repos", "delete"], positionals: [], projectScoped: true, legacyPositionalProject: true, minimalArgs: [], runStyle: "sub", skipBehavioral: "irreversibly deletes a project after reading its live vault generation count" },
265
277
  { path: ["errors"], positionals: [p("fingerprint_id", { required: false })], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "merged" },
266
278
 
267
279
  // ── jobs ─────────────────────────────────────────────────────────────────
package/lib/functions.mjs CHANGED
@@ -842,6 +842,8 @@ async function rebuild(projectId, args = []) {
842
842
  const positionals = [];
843
843
  for (const arg of args) {
844
844
  if (arg === "--all") { all = true; continue; }
845
+ // Same rule as assertNoExtraPositionals: a flag is not a positional.
846
+ if (typeof arg === "string" && arg.startsWith("-")) continue;
845
847
  positionals.push(arg);
846
848
  }
847
849
  if (positionals.length > 1) {
@@ -957,12 +959,18 @@ function parseDepsFlag(value) {
957
959
  }
958
960
 
959
961
  function assertNoExtraPositionals(args, usage) {
960
- if (args.length > 0) {
962
+ // Only POSITIONALS count. A flag-shaped argument that survived
963
+ // assertKnownFlags is an accepted convention flag (`--json`), not a stray
964
+ // positional — reporting it as one made `functions list --json` fail with
965
+ // "Unexpected argument: --json" while the same command without it worked.
966
+ // Its sibling assertNoUnexpectedPositionals already skips these.
967
+ const extra = args.filter((a) => !(typeof a === "string" && a.startsWith("-")));
968
+ if (extra.length > 0) {
961
969
  fail({
962
970
  code: "BAD_USAGE",
963
- message: `Unexpected argument: ${args[0]}`,
971
+ message: `Unexpected argument: ${extra[0]}`,
964
972
  hint: usage,
965
- details: { argument: args[0] },
973
+ details: { argument: extra[0] },
966
974
  });
967
975
  }
968
976
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Shared git-remote-scaffold fold-in (repo-first-onramp D4, task 2.4).
3
+ *
4
+ * `run402 init`'s 5b block pioneered this pattern; this module is the same
5
+ * additive-only, non-fatal-in-every-branch scaffold factored out so
6
+ * `projects provision`, `run402 up`, and `run402 repos create` all report
7
+ * through the IDENTICAL `gitvault` / `gitvault_skipped` / `gitvault_error`
8
+ * summary keys `init` already uses, instead of three near-copies drifting
9
+ * apart. `init.mjs` itself is left untouched — its own scaffold block is
10
+ * shipped and tested; this module exists for the NEW callers task 2.4 adds.
11
+ *
12
+ * Never creates a repository on its own unless `createRepoIfMissing` is
13
+ * explicit (mirrors `init`'s `--git-remote` opt-in): a directory the caller
14
+ * did not ask to turn into a repository is left alone. `origin` is never
15
+ * touched or reclaimed (D1, via `gitvault.scaffoldRemote` itself). Every
16
+ * branch is non-fatal — a missing git, an unresolvable org, or an
17
+ * unreachable gateway all report and return, never throw.
18
+ */
19
+ import { getSdk } from "./sdk.mjs";
20
+ import { resolveOwningOrgId } from "./org-context.mjs";
21
+
22
+ /**
23
+ * The interim allocation→policy advisory (repo-first-onramp D3, stated as an
24
+ * interim). TODAY the platform sets `gitvault_policy: required` at allocation
25
+ * — the decoupling (allocation leaving the policy unset) is design D3's
26
+ * gateway half and ships in a later release (task 3.1). Until it does, every
27
+ * client-side path that allocates a vault prints this line so the policy flip
28
+ * is never a silent side effect of a push or a create. Faithful over flattering:
29
+ * documenting the designed future as the present is the breach; this line is
30
+ * the truth. DELETE this export (and its four call sites) when the gateway
31
+ * decoupling ships.
32
+ */
33
+ export const GITVAULT_ALLOCATION_POLICY_ADVISORY =
34
+ "note: allocation currently sets gitvault_policy to 'required' — from now on a deploy must present a vaulted capture (produced automatically by `run402 deploy apply` on any machine holding this keystore). Opt out with `run402 gitvault policy grandfathered --reason \"<why>\"`. A later release decouples allocation from the deploy gate (design D3).";
35
+
36
+ /**
37
+ * @param {object} options
38
+ * @param {string} [options.repoDir] Working tree to scaffold. Defaults to `process.cwd()`.
39
+ * @param {string} options.projectId The project the remote should point at.
40
+ * @param {string} [options.orgId] Explicit owning org. Resolved via `resolveOwningOrgId` when omitted.
41
+ * @param {boolean} [options.createRepoIfMissing] Opt into `git init`-ing `repoDir` when it is not a repository yet.
42
+ * @returns {Promise<{gitvault: object|null, gitvault_skipped?: string, gitvault_error?: {code: string, message: string}}>}
43
+ */
44
+ export async function scaffoldGitvaultRemote({ repoDir = process.cwd(), projectId, orgId, createRepoIfMissing = false } = {}) {
45
+ const out = { gitvault: null };
46
+ try {
47
+ const { hardenedGit } = await import("#sdk/node");
48
+ let insideRepo = true;
49
+ try {
50
+ await hardenedGit(repoDir, ["rev-parse", "--git-dir"]);
51
+ } catch {
52
+ insideRepo = false;
53
+ }
54
+ if (!insideRepo && !createRepoIfMissing) {
55
+ out.gitvault_skipped = "not a git repository — re-run with --git-remote to create one and add the remote";
56
+ return out;
57
+ }
58
+ const resolvedOrgId = orgId ?? (await resolveOwningOrgId(projectId));
59
+ if (!resolvedOrgId) {
60
+ out.gitvault_skipped = `could not resolve the owning org for ${projectId} — the run402 remote was not added`;
61
+ return out;
62
+ }
63
+ const remote = await getSdk().gitvault.scaffoldRemote({ repo_dir: repoDir, org_id: resolvedOrgId, project_id: projectId });
64
+ // `allocated: false` is stated, not left to be inferred: this is local
65
+ // git only — no vault exists for the project yet (allocation happens
66
+ // lazily on first push, or explicitly via `run402 gitvault init`).
67
+ out.gitvault = { ...remote, allocated: false };
68
+ } catch (err) {
69
+ out.gitvault = null;
70
+ out.gitvault_error = { code: err?.body?.code ?? err?.code ?? "GITVAULT_SCAFFOLD_FAILED", message: err?.message ?? String(err) };
71
+ }
72
+ return out;
73
+ }
package/lib/gitvault.mjs CHANGED
@@ -19,6 +19,7 @@
19
19
  import { readFileSync } from "node:fs";
20
20
  import { resolveProjectId } from "./config.mjs";
21
21
  import { resolveOwningOrgId } from "./org-context.mjs";
22
+ import { GITVAULT_ALLOCATION_POLICY_ADVISORY } from "./gitvault-scaffold.mjs";
22
23
  import { getSdk } from "./sdk.mjs";
23
24
  import { reportSdkError, fail } from "./sdk-errors.mjs";
24
25
  import {
@@ -37,15 +38,15 @@ const COMMON_VALUE_FLAGS = ["--project", "--repo"];
37
38
  export const HELP = `run402 gitvault — your source, encrypted before it leaves the machine
38
39
 
39
40
  Usage:
40
- run402 gitvault init [--project <id>] [--org <org_id>] [--git-remote] [--no-remote]
41
- run402 gitvault status [--project <id>] [--repo <repo_id>] [--refs]
42
- run402 gitvault push [--project <id>] [--repo <repo_id>] [--message <text>] [--checkpoint]
43
- run402 gitvault policy <required|grandfathered> [--project <id>] [--repo <repo_id>]
44
- [--reason <why>]
45
- run402 gitvault compact [--project <id>] [--repo <repo_id>]
46
- run402 gitvault prune [--project <id>] [--repo <repo_id>]
47
- [--submit --intent-core <path> --verifier-receipt <path> [--wait]]
48
- run402 gitvault verify [--project <id>] [--repo <repo_id>] [--budget <n>]
41
+ run402 gitvault init [--project <id>] [--org <org_id>] [--git-remote] [--no-remote]
42
+ run402 gitvault status [--project <id>] [--repo <repo_id>] [--refs]
43
+ run402 gitvault snapshot [--project <id>] [--repo <repo_id>] [--message <text>] [--checkpoint]
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>]
49
50
 
50
51
  Subcommands:
51
52
  init ALLOCATE the project's vault. This is the one step that mints key
@@ -65,11 +66,20 @@ Subcommands:
65
66
  audited. \`grandfathered\` is the documented way out of a deploy
66
67
  blocked by GITVAULT_CLIENT_UPGRADE_REQUIRED, and leaves a
67
68
  doctor-persistent warning until the project returns to \`required\`.
68
- push Capture the working tree and publish it. This is NOT gated on a
69
- deploy — a vault-only project pushes for months without one.
70
- Before reporting a push as landed the SDK compares finalization
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\`. The one-shot recovery receipt
74
+ and keystore path print to stderr the moment that happens.
75
+ Before reporting a snapshot as landed the SDK compares finalization
71
76
  receipts against the expected manifest and reads the admitted head
72
- back from storage; a 200 alone is never enough.
77
+ back from storage; a 200 alone is never enough. \`push\` is a
78
+ deprecation-warning alias for one release — it will be removed
79
+ next release. Once \`gitvault\` was the only publish verb; \`git
80
+ push\` is now the actual publish path (via the remote helper),
81
+ so \`push\` here was renamed to name what it does: one verb per
82
+ operation.
73
83
  compact Publish a checkpoint covering the canonical refs, every root
74
84
  unexpired at the cutoff, and the HEAD target, under a maintenance
75
85
  lease so a concurrent cycle cannot race it.
@@ -97,9 +107,9 @@ Options:
97
107
  chain and advances the local materialized pin), which is
98
108
  why plain \`status\` — an observation — does not do it.
99
109
  --repo <repo_id> Address the vault directly by id, skipping project lookup
100
- --message <text> push: commit message for the synthetic commit a dirty tree
110
+ --message <text> snapshot: commit message for the synthetic commit a dirty tree
101
111
  produces (a clean tree pushes HEAD itself, no message used)
102
- --checkpoint push: force the checkpoint-bearing form regardless of delta size
112
+ --checkpoint snapshot: force the checkpoint-bearing form regardless of delta size
103
113
  --budget <n> verify: heads to verify in this call. The verified prefix is
104
114
  persisted, so a budget-exceeded run resumes where it stopped
105
115
  instead of restarting.
@@ -190,8 +200,12 @@ function printTerminalLoss(status) {
190
200
  console.error("");
191
201
  }
192
202
 
193
- /** Where the keystore lives — for verbs whose payload is not a `status`. */
194
- async function printKeystoreLocation() {
203
+ /**
204
+ * Where the keystore lives — for verbs whose payload is not a `status`.
205
+ * Exported: `repos create` (repo-first-onramp task 2.6) prints the same
206
+ * line after allocating a vault, and must not restate this logic.
207
+ */
208
+ export async function printKeystoreLocation() {
195
209
  try {
196
210
  const { getGitvaultKeystoreRoot } = await import("#sdk/node");
197
211
  console.error(`keystore: ${getGitvaultKeystoreRoot()} — back this up; whole-keystore loss is terminal for vault history`);
@@ -268,8 +282,9 @@ async function init(args) {
268
282
  ? `vault ${result.repo_id} already existed — nothing was re-allocated and no new key material was minted`
269
283
  : `allocated vault ${result.repo_id} (genesis ${result.genesis_sha256})`,
270
284
  );
271
- if (result.remote) console.error(`remote '${result.remote.name}' -> ${result.remote.url}`);
285
+ if (result.remote) console.error(`remote '${result.remote.name}' -> ${result.remote.url} (${result.remote.reason})`);
272
286
  if (remoteSkipped) console.error(`remote not added: ${remoteSkipped}`);
287
+ if (!result.deduplicated) console.error(GITVAULT_ALLOCATION_POLICY_ADVISORY);
273
288
  // The recovery receipt is integrity data, not a secret, and it is worth
274
289
  // exactly as much as the number of copies you keep. It is persisted into
275
290
  // the keystore automatically; say where, because "keep many copies" is
@@ -376,20 +391,46 @@ async function status(args) {
376
391
  }
377
392
  }
378
393
 
379
- async function push(args) {
394
+ /**
395
+ * D5 (repo-first-onramp task 2.5): one verb per operation. Renamed from
396
+ * `push` — `push` now means exactly one thing everywhere: `git push`. The
397
+ * capture lane keeps its old function name internally to minimize churn;
398
+ * only the dispatched SUBCOMMAND name changed (see `run()` below, where
399
+ * `gitvault push` survives one release as a deprecation-warning alias).
400
+ */
401
+ async function snapshot(args) {
380
402
  const a = normalizeArgv(args);
381
403
  const valueFlags = [...COMMON_VALUE_FLAGS, "--message"];
382
404
  assertKnownFlags(a, [...valueFlags, "--checkpoint", "--help", "-h"], valueFlags);
383
405
  requirePositionalCount(a, valueFlags, {
384
- min: 0, max: 0, command: "run402 gitvault push", missing: "",
406
+ min: 0, max: 0, command: "run402 gitvault snapshot", missing: "",
385
407
  });
386
408
  const message = flagValue(a, "--message");
409
+ const target = vaultTarget(a);
410
+ // D2: lazily allocate the vault on first push when there is a project to
411
+ // resolve the owning org from — the same resolution `gitvault init` uses.
412
+ // `--repo`-only addressing has nothing to create FROM (no project_id), so
413
+ // it is skipped there, matching `open()`'s own precedence.
414
+ const orgId = target.project_id ? await resolveOwningOrgId(target.project_id) : null;
387
415
  const opts = {
388
- ...vaultTarget(a),
416
+ ...target,
417
+ ...(orgId ? { org_id: orgId } : {}),
389
418
  // The gitvault_commit line is progress, not payload: print it the moment
390
419
  // the snapshot exists, well before the publication round-trips finish, so
391
420
  // a human watching a slow push sees what is being pushed.
392
421
  onCommitLine: (line) => console.error(line),
422
+ // Fires synchronously, BEFORE the capture/publish that follows — printed
423
+ // here rather than deferred past `push()`'s return so the receipt is
424
+ // never lost if a later step in the SAME push fails after allocation
425
+ // already landed on the server.
426
+ onVaultCreated: async (created) => {
427
+ console.error("");
428
+ console.error(`vault allocated (genesis ${created.genesis_sha256}) — one-shot recovery receipt, keep many copies:`);
429
+ console.error(JSON.stringify(created.recovery_receipt));
430
+ await printKeystoreLocation();
431
+ console.error(GITVAULT_ALLOCATION_POLICY_ADVISORY);
432
+ console.error("");
433
+ },
393
434
  };
394
435
  // The message rides on `snapshot`, which is what `captureSnapshot` reads —
395
436
  // and, since 5.12b removed the dead top-level `push({ message })` field, is
@@ -558,8 +599,17 @@ export async function run(sub, args) {
558
599
  await status(argv);
559
600
  break;
560
601
  }
602
+ case "snapshot": {
603
+ await snapshot(argv);
604
+ break;
605
+ }
561
606
  case "push": {
562
- await push(argv);
607
+ // D5: one verb per operation — "push" now means exactly one thing,
608
+ // `git push`. Retained as a deprecation-warning alias for ONE release
609
+ // (pre-launch, the benchmark gate prefers the rename now over an
610
+ // alias forever); it will be removed next release.
611
+ console.error("`run402 gitvault push` is deprecated and will be removed in the next release — use `run402 gitvault snapshot` instead.");
612
+ await snapshot(argv);
563
613
  break;
564
614
  }
565
615
  case "compact": {
package/lib/init.mjs CHANGED
@@ -557,7 +557,10 @@ export async function run(args = []) {
557
557
  // Purely LOCAL git. No vault is allocated and no key material is written
558
558
  // here — the spec is explicit that neither exists until first capture, so the
559
559
  // cold-start path gains no prompt and no new failure mode. Allocation happens
560
- // on the first `run402 gitvault push` (or deploy).
560
+ // on the first `git push origin <branch>` or `run402 gitvault snapshot` (D2,
561
+ // repo-first-onramp) — NOT on deploy: `applyWithGitvault` only ever reads an
562
+ // EXISTING vault's policy (D3) and never allocates one that does not exist,
563
+ // so a project with no vault deploys exactly as it always did.
561
564
  //
562
565
  // Adding the remote is the DEFAULT inside a repository that already exists,
563
566
  // because it is pure addition: `origin` is never modified or claimed, no file
@@ -608,11 +611,13 @@ export async function run(args = []) {
608
611
  if (remote.already_present && remote.existing_url !== remote.url) {
609
612
  // Left exactly as it was. Name the URL that is actually in place
610
613
  // rather than implying the remote now points at this project.
611
- line("Gitvault", `remote '${remote.name}' already points at ${remote.existing_url} — left unchanged`);
614
+ line("Gitvault", `remote '${remote.name}' already points at ${remote.existing_url} — left unchanged (${remote.reason})`);
612
615
  } else if (remote.already_present) {
613
616
  line("Gitvault", `remote '${remote.name}' already set (${remote.url})`);
614
617
  } else {
615
- line("Gitvault", `${remote.created_repository ? "initialized a repository and added" : "added"} remote '${remote.name}' -> ${remote.url}`);
618
+ // D1: `origin` when it was free, `run402` when it was already
619
+ // taken by something else — `remote.reason` says which happened.
620
+ line("Gitvault", `${remote.created_repository ? "initialized a repository and added" : "added"} remote '${remote.name}' -> ${remote.url} (${remote.reason})`);
616
621
  }
617
622
  }
618
623
  }
package/lib/projects.mjs CHANGED
@@ -6,6 +6,7 @@ import { withAutoApprove } from "./operator.mjs";
6
6
  import { getSdk } from "./sdk.mjs";
7
7
  import { reportSdkError, fail, parseFlagJson } from "./sdk-errors.mjs";
8
8
  import { stampOrgFromProject } from "./org-context.mjs";
9
+ import { scaffoldGitvaultRemote } from "./gitvault-scaffold.mjs";
9
10
  import { assertKnownFlags, failBadProjectId, flagValue, hasHelp, normalizeArgv, positionalArgs, resolveProjectSelector, validateRegularFile, failUnknownSubcommand } from "./argparse.mjs";
10
11
 
11
12
  const HELP = `run402 projects — Manage your deployed Run402 projects
@@ -299,6 +300,20 @@ async function quote() {
299
300
  }
300
301
  }
301
302
 
303
+ /**
304
+ * D4 (repo-first-onramp task 2.4): `projects provision` already knows the
305
+ * project and (usually) the org, and just set the active project —
306
+ * scaffolding the git remote here is a pure Anticipatory fold, zero extra
307
+ * round-trips beyond the one org lookup provision needed anyway when `--org`
308
+ * was not given explicitly. `provision` has no `--git-remote` opt-in, so a
309
+ * directory that is not already a repository is left untouched, exactly like
310
+ * `run402 init`'s own scaffold (shared via `gitvault-scaffold.mjs`, which
311
+ * emits the SAME `gitvault` / `gitvault_skipped` / `gitvault_error` keys).
312
+ */
313
+ async function foldGitvaultScaffold(out, projectId, explicitOrgId) {
314
+ Object.assign(out, await scaffoldGitvaultRemote({ repoDir: process.cwd(), projectId, orgId: explicitOrgId }));
315
+ }
316
+
302
317
  async function provision(args) {
303
318
  const opts = { tier: "prototype", name: undefined, orgId: undefined, idempotencyKey: undefined };
304
319
  for (let i = 0; i < args.length; i++) {
@@ -373,6 +388,7 @@ async function provision(args) {
373
388
  out.note = `active project changed: ${activeBefore} -> ${activeAfter}`;
374
389
  out.previous_active_project_id = activeBefore;
375
390
  }
391
+ await foldGitvaultScaffold(out, data.project_id, opts.orgId);
376
392
  console.log(JSON.stringify(out, null, 2));
377
393
  } catch (err) {
378
394
  reportSdkError(err);