@vibe-commit/capture 0.10.0 → 0.11.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 (81) hide show
  1. package/README.md +24 -0
  2. package/dist/agent_hooks.js +2 -2
  3. package/dist/cli.d.ts +35 -0
  4. package/dist/cli.js +412 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/backfill.js +19 -2
  7. package/dist/commands/backfill.js.map +1 -1
  8. package/dist/commands/finalize.d.ts +37 -1
  9. package/dist/commands/finalize.js +129 -29
  10. package/dist/commands/finalize.js.map +1 -1
  11. package/dist/commands/status.js +7 -0
  12. package/dist/commands/status.js.map +1 -1
  13. package/dist/commands/status_machine.js +5 -0
  14. package/dist/commands/status_machine.js.map +1 -1
  15. package/dist/commands/upload_worker.d.ts +63 -0
  16. package/dist/commands/upload_worker.js +336 -0
  17. package/dist/commands/upload_worker.js.map +1 -0
  18. package/dist/copy/strings.d.ts +23 -7
  19. package/dist/copy/strings.js +40 -7
  20. package/dist/copy/strings.js.map +1 -1
  21. package/dist/drain.d.ts +68 -0
  22. package/dist/drain.js +234 -0
  23. package/dist/drain.js.map +1 -0
  24. package/dist/firing.d.ts +169 -0
  25. package/dist/firing.js +334 -0
  26. package/dist/firing.js.map +1 -0
  27. package/dist/hooks/entry.d.ts +171 -26
  28. package/dist/hooks/entry.js +333 -507
  29. package/dist/hooks/entry.js.map +1 -1
  30. package/dist/hooks/fire_lock.d.ts +36 -87
  31. package/dist/hooks/fire_lock.js +40 -99
  32. package/dist/hooks/fire_lock.js.map +1 -1
  33. package/dist/hooks/post_commit.d.ts +2 -0
  34. package/dist/hooks/post_commit.js +7 -3
  35. package/dist/hooks/post_commit.js.map +1 -1
  36. package/dist/identity.d.ts +32 -0
  37. package/dist/identity.js +38 -0
  38. package/dist/identity.js.map +1 -0
  39. package/dist/index.d.ts +3 -17
  40. package/dist/index.js +59 -450
  41. package/dist/index.js.map +1 -1
  42. package/dist/paths.d.ts +18 -0
  43. package/dist/paths.js +31 -0
  44. package/dist/paths.js.map +1 -1
  45. package/dist/pending_prune.d.ts +2 -2
  46. package/dist/pending_prune.js +2 -2
  47. package/dist/policy.d.ts +17 -0
  48. package/dist/policy.js +7 -0
  49. package/dist/policy.js.map +1 -1
  50. package/dist/post.d.ts +52 -4
  51. package/dist/post.js +59 -21
  52. package/dist/post.js.map +1 -1
  53. package/dist/replay.d.ts +167 -0
  54. package/dist/replay.js +301 -0
  55. package/dist/replay.js.map +1 -0
  56. package/dist/session_targets.d.ts +12 -0
  57. package/dist/session_targets.js +6 -1
  58. package/dist/session_targets.js.map +1 -1
  59. package/dist/spool.d.ts +44 -16
  60. package/dist/spool.js +174 -33
  61. package/dist/spool.js.map +1 -1
  62. package/dist/spool_union.d.ts +18 -2
  63. package/dist/spool_union.js +23 -2
  64. package/dist/spool_union.js.map +1 -1
  65. package/dist/state.js +21 -14
  66. package/dist/state.js.map +1 -1
  67. package/dist/stdin.d.ts +2 -0
  68. package/dist/stdin.js +13 -0
  69. package/dist/stdin.js.map +1 -0
  70. package/dist/uploader_status.d.ts +34 -0
  71. package/dist/uploader_status.js +25 -0
  72. package/dist/uploader_status.js.map +1 -0
  73. package/dist/uploader_view.d.ts +39 -0
  74. package/dist/uploader_view.js +106 -0
  75. package/dist/uploader_view.js.map +1 -0
  76. package/dist/verbs.d.ts +11 -0
  77. package/dist/verbs.js +13 -0
  78. package/dist/verbs.js.map +1 -0
  79. package/dist/version.d.ts +1 -1
  80. package/dist/version.js +1 -1
  81. package/package.json +2 -2
package/README.md CHANGED
@@ -356,6 +356,16 @@ request; a waiting revoke row is still retried.
356
356
  *Files.* `grants.json` gains `pending` and `synced_at` and stays written only by the commands, never by a hook; a 0.7.0
357
357
  command that rewrites it keeps both. `grants-account.json` is new, and the hooks' file. `pending-revoke.json` is new and
358
358
  holds a bearer token while a revoke is waiting.
359
+ ### Known limits of the background uploader
360
+
361
+ These are understood and left as they are for now; each fails in the safe direction.
362
+
363
+ - *One repository's persistent failure holds the session's other repositories.* A session that goes to several repositories is delivered in order; if the server keeps failing for one of them the others wait behind it. The server is shared, so this is rare.
364
+ - *`finalize` treats turns recorded for another server or credential as done* and sends the rest itself; the background process for that other server or credential sends its own.
365
+ - *The lock's stale-reclaim has a narrow race* (two processes judging the same dead holder at once). It predates the background process and is unchanged.
366
+ - *A session whose newest recorded turn belongs to another server or credential is not finished by the idle drain.* A session takes its identity from its last recorded turn.
367
+ - *A turn with more than 200 sub-agent files is sent as its files are when the background process gets to it.* Each turn's upload normally sends a sub-agent file only up to the size it had when the turn ended. Past 200 files (or when the recorded line cannot hold their sizes) the turn is sent as it was before that limit existed, so a sub-agent file written in a later turn can change what is withheld from the earlier turn's copy. It can only make the copy withhold more, never less.
368
+
359
369
  ### Commit file names, and turn numbers (0.10.0)
360
370
 
361
371
  From 0.10.0 the commits a session makes also send the path of every file they changed, for the repositories you chose and for no
@@ -504,6 +514,20 @@ it, so turns that ran in other folders can appear in its copy. A session run fro
504
514
  calls name files inside a repository you chose, and it never gets a folder target. The client does not read its transcripts to look for
505
515
  repositories. Backfill does not offer or upload Cursor sessions.
506
516
 
517
+ ### Uploading happens in a short-lived background process
518
+
519
+ *What changed.* A hook no longer waits for the network. On each `Stop`, `PreCompact` or `SessionEnd` it writes one line to a local file under `~/.vibecommit/state/notices/_session-targets/` (the session, how far its transcript has grown, and a few facts about the moment: the working folder, the agent, which credential and server) and returns without waiting for the network. A small background process of the same program (`vibecommit __upload-worker`, not a command you run) does the uploading. It is started by the hook if none is running, sends one request at a time, waits as long as the request took (not less than a quarter of a second, not more than 30 seconds) before the next, and exits when nothing is left to send. There is one per machine and credential.
520
+
521
+ *What this means for you.* The upload can finish after your agent has exited, and one such process may live up to an hour. Whether an agent's own runner ends a background process when the agent quits is not something this client controls and has not been measured; if it does, the line stays on disk and the next agent turn starts a new process. If the server does not answer it keeps asking, 5 seconds apart and backing off to 5 minutes (it waits at least as long as the server's `Retry-After` asks, up to that 5-minute ceiling), and gives up after 30 minutes without progress or one hour in all; the next agent turn starts a new one. It sends what the hook would have sent, under the same consent, the same credential and the same client-side path redaction, and the same server-side scrubber: what is uploaded, and for which repositories, is exactly what *what we upload* describes. It also finishes sessions that ended with bytes still unsent, but only ones idle for 10 minutes, and at most 64 MiB per run (a starting value, not a measured one).
522
+
523
+ *Notices come one turn later.* The hook can no longer see the outcome of the upload it just recorded, so "VibeCommit captured this session.", "this machine's credential is no longer accepted" and the credential-ending notice appear on the next turn of the same session, from what the background process wrote down (`uploader.<id>.status`, counts, classes and times only, never a body, a path or a credential).
524
+
525
+ *`vibecommit status`* shows an `upload` row once anything has been recorded for upload: `up to date`, `sending`, `waiting to retry` or `not running`, with how many sessions have recorded turns not yet sent and how many bytes those are, and when the server last acknowledged. "Behind" means recorded turns only: sessions that ended with bytes unsent and have no recorded turn waiting (the background process finishes those on its own, within the limits above) are not counted, and turns recorded for another server or credential are. It is computed from the recorded lines against what was acknowledged and from whether the background process's lock is alive, not from what the process says about itself, so a process that died with bytes behind reads `not running`, never `up to date`. `status --json` adds an `uploader` object to both documents (`state`, `heartbeat_age_ms`, `behind: { sessions, bytes, sessions_checked }`, `last_ack_at_ms`, `last_attempt`); `sessions_checked` is how many recorded sessions were looked at, so an empty scan is not mistaken for a clean one. `last_attempt.status` is always `null`: the process records the kind of outcome, not an HTTP status.
526
+
527
+ *What did not change.* No new data leaves your machine, no setting is needed, the consent prompt is the same (apart from the scrubber count, below), and `vibecommit off` and the per-repository consent checks apply to the background process exactly as to a hook. A session whose id the server cannot take is still written off loudly, as before. Nothing here is claimed for Windows.
528
+
529
+ *The scrubber now removes 13 known secret formats* (it said 12): a PEM private key block joined the list on the server. The consent screen says 13.
530
+
507
531
  ## Status
508
532
 
509
533
  **Implemented**, not a scaffold. All six commands above have real bodies — OAuth sign-in
@@ -244,7 +244,7 @@ binaryExists = () => true) {
244
244
  * 2. **Its command is ours** — see `commandIsOurs`.
245
245
  *
246
246
  * ⚠ **THE FAILURE DIRECTION IS DELIBERATE.** A false "not ours" costs one
247
- * duplicate entry, which `claimFiring` (`CR-195`/U4a) already turns into a
247
+ * duplicate entry, which the worker's no-op replay of a duplicate firing (U6) already turns into a
248
248
  * single capture. A false "ours" overwrites a user's hook — the one outcome this
249
249
  * module exists to make impossible. Every doubt resolves to "not ours".
250
250
  */
@@ -318,7 +318,7 @@ export function hasRegistration(raw, shape, registration, binaryExists = existsS
318
318
  * dir) is exactly that: Gate A repointed a machine's working capture at a
319
319
  * directory about to be deleted, after which every hook fails quietly and
320
320
  * capture stops. Without this arm the same run appends a second entry and the
321
- * working one keeps firing (`claimFiring` makes it one capture). So: a moved
321
+ * working one keeps firing (a duplicate firing replays as a no-op, so it is one capture). So: a moved
322
322
  * path is claimed only when the binary it names is GONE — the reinstall this
323
323
  * arm exists for (an uninstalled prefix, a removed nvm version) — and a live
324
324
  * one is left alone. ⚠ Cost, accepted: two live installs leave two entries
package/dist/cli.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `vibecommit` — CLI entry point.
3
+ *
4
+ * Verb bodies that are NOT here, each an owned task in
5
+ * `coordination/CAPTURE_REFACTOR_TASKS.md`:
6
+ *
7
+ * why CR-086 report CR-108 connect's live-capture ending CR-025
8
+ * status/off output spec CR-021 failure policy CR-018
9
+ * repo binding key + X-Repo-Slug CR-017 / CR-019
10
+ *
11
+ * What IS established here, and must not be undone by those tasks:
12
+ * - no user-facing string literal in a command body (CR-090 → `src/copy/`)
13
+ * - the exit-code contract is split hook vs interactive (CR-022 → `src/exit.ts`)
14
+ * - one commit-header renderer taking the grade (CR-090 → `src/copy/grades.ts`)
15
+ * - every escape sequence comes from `src/term.ts` (DESIGN.md §13.2)
16
+ * - the hook contract is enforced by the entry, not by each verb
17
+ */
18
+ import { VERBS, isVerb, type Verb } from "./verbs.js";
19
+ export { VERBS, isVerb };
20
+ export type { Verb };
21
+ /**
22
+ * `--help`, wrapped hard at 80 columns per DESIGN.md §13.4.
23
+ *
24
+ * Never reflows to `process.stdout.columns`: identical commands must emit
25
+ * identical bytes, or issue-paste, screenshots and every golden-file test of the
26
+ * copy break. `test/help-golden.test.ts` is that golden file.
27
+ *
28
+ * No SGR here at all. Colour would have to be resolved before help could be
29
+ * rendered, and help is the one surface where structure alone is enough —
30
+ * indentation and the label gutter carry it, which is §13.1's point about
31
+ * `NO_COLOR` being a lossless degradation rather than a downgrade.
32
+ */
33
+ export declare function renderHelp(): string;
34
+ /** Run the interactive CLI for `argv` (everything after the binary): guards first, then the verb, then the exit code. */
35
+ export declare function runCli(argv: readonly string[]): void;
package/dist/cli.js ADDED
@@ -0,0 +1,412 @@
1
+ /**
2
+ * `vibecommit` — CLI entry point.
3
+ *
4
+ * Verb bodies that are NOT here, each an owned task in
5
+ * `coordination/CAPTURE_REFACTOR_TASKS.md`:
6
+ *
7
+ * why CR-086 report CR-108 connect's live-capture ending CR-025
8
+ * status/off output spec CR-021 failure policy CR-018
9
+ * repo binding key + X-Repo-Slug CR-017 / CR-019
10
+ *
11
+ * What IS established here, and must not be undone by those tasks:
12
+ * - no user-facing string literal in a command body (CR-090 → `src/copy/`)
13
+ * - the exit-code contract is split hook vs interactive (CR-022 → `src/exit.ts`)
14
+ * - one commit-header renderer taking the grade (CR-090 → `src/copy/grades.ts`)
15
+ * - every escape sequence comes from `src/term.ts` (DESIGN.md §13.2)
16
+ * - the hook contract is enforced by the entry, not by each verb
17
+ */
18
+ /**
19
+ * The INTERACTIVE half of the CLI: every verb a person types.
20
+ *
21
+ * ⛔ THIS FILE IS LOADED BY `index.ts` WITH A DYNAMIC `import()`, ONLY AFTER THE HOOK, THE UPLOAD WORKER AND THE GIT
22
+ * HOOK VERBS HAVE BEEN DISPATCHED. It used to be `index.ts` itself, which statically imported every verb, so a
23
+ * `vibecommit hook` process paid for `auth`, `connect`, `status` and the whole OAuth stack it never runs (import 389 ms
24
+ * against 132 ms for `hooks/entry.js`, at load 50-89). `test/load-path.test.ts` fails the build if a value import of an
25
+ * interactive verb is put back on a hook path.
26
+ */
27
+ import { writeSync } from "node:fs";
28
+ import { homedir } from "node:os";
29
+ import { createInterface } from "node:readline/promises";
30
+ import { Writable } from "node:stream";
31
+ import { resolveAgentId } from "./agents/registry.js";
32
+ import { auth } from "./commands/auth.js";
33
+ import { browserCredential } from "./commands/browser_credential.js";
34
+ import { updateNudge } from "./update_nudge.js";
35
+ import { connect } from "./commands/connect.js";
36
+ import { finalize } from "./commands/finalize.js";
37
+ import { offVerb, statusVerb } from "./commands/status.js";
38
+ import { report } from "./commands/report.js";
39
+ import { backfill, backfillForConnect } from "./commands/backfill.js";
40
+ import { scan } from "./commands/scan.js";
41
+ import { why } from "./commands/why.js";
42
+ import { HELP, INTERNAL, USAGE } from "./copy/index.js";
43
+ import { EXIT } from "./exit.js";
44
+ import { channelCommand, installChannel } from "./install.js";
45
+ import { renderJsonError } from "./json.js";
46
+ import { readStdin } from "./stdin.js";
47
+ import { LABEL_GUTTER, renderErrorBlock, resolveColour, wrap } from "./term.js";
48
+ import { CLIENT_VERSION } from "./version.js";
49
+ import { VERBS, isVerb } from "./verbs.js";
50
+ // `CR-216/U2` adds `auth`, and adding it MOVES `test/help.golden.txt` — which
51
+ // `CR-084d` and the `post-commit` note below were both careful not to do. The
52
+ // difference is the point: those two are a FLAG and a HIDDEN verb, neither of
53
+ // which a human types, so moving the golden file for them would have been noise.
54
+ // `auth` is typed by a human on a new machine before anything else works, so a
55
+ // help screen that omitted it would be the defect one layer up. The golden file
56
+ // exists to make a change to that screen deliberate, not to make it impossible.
57
+ /** The verbs with a `--json` mode (`src/json.ts`'s `JsonVerb`): a failure under the flag is a document, not a block. */
58
+ const JSON_VERBS = ["status", "why", "report", "scan"];
59
+ export { VERBS, isVerb };
60
+ /**
61
+ * `--help`, wrapped hard at 80 columns per DESIGN.md §13.4.
62
+ *
63
+ * Never reflows to `process.stdout.columns`: identical commands must emit
64
+ * identical bytes, or issue-paste, screenshots and every golden-file test of the
65
+ * copy break. `test/help-golden.test.ts` is that golden file.
66
+ *
67
+ * No SGR here at all. Colour would have to be resolved before help could be
68
+ * rendered, and help is the one surface where structure alone is enough —
69
+ * indentation and the label gutter carry it, which is §13.1's point about
70
+ * `NO_COLOR` being a lossless degradation rather than a downgrade.
71
+ */
72
+ export function renderHelp() {
73
+ const lines = [
74
+ ...wrap(HELP.tagline, 2),
75
+ "",
76
+ ...wrap(`${USAGE.usageLabel} ${HELP.usage}`, 2),
77
+ "",
78
+ ...wrap(USAGE.commandsLabel, 2),
79
+ ];
80
+ for (const verb of VERBS) {
81
+ // The description is wrapped on its own and the verb column is written over
82
+ // the first line's indent. Wrapping `verb + padding + description` as one
83
+ // string would not work: `wrap` normalises whitespace, so the fixed gutter
84
+ // would collapse to a single space.
85
+ const head = ` ${verb.padEnd(LABEL_GUTTER)}`;
86
+ const body = wrap(HELP.commands[verb], head.length);
87
+ lines.push(head + body[0].slice(head.length), ...body.slice(1));
88
+ }
89
+ lines.push("", ...wrap(`${USAGE.docsLabel} ${HELP.docsUrl}`, 2));
90
+ return lines.join("\n");
91
+ }
92
+ function interactiveContext(argv) {
93
+ return {
94
+ env: process.env,
95
+ home: homedir(),
96
+ cwd: process.cwd(),
97
+ nodeVersion: process.versions.node,
98
+ // `CR-183`. Resolved from argv HERE, the same pure read the hook branch at
99
+ // the bottom of this file makes — `connect` needs it to know whether the
100
+ // agent it was told about has a way to find the running session at all.
101
+ agentId: resolveAgentId(argv),
102
+ colour: resolveColour({
103
+ env: process.env,
104
+ isTty: process.stdout.isTTY === true,
105
+ noColorFlag: argv.includes("--no-color"),
106
+ json: argv.includes("--json"),
107
+ }),
108
+ stdinIsTty: process.stdin.isTTY === true,
109
+ stdout: process.stdout,
110
+ stderr: process.stderr,
111
+ /**
112
+ * The readline interface is created HERE, on the first question, and not
113
+ * once per invocation — CR-117.
114
+ *
115
+ * `createInterface` RESUMES stdin, and a resumed stdin keeps the event loop
116
+ * alive until it ends. Built eagerly, a verb that returns without ever
117
+ * prompting sat there until the pipe closed: `connect` refusing a non-TTY,
118
+ * and `status` / `off`, which never ask anything. On a terminal that looked
119
+ * fine, because stdin ends at EOF; under `cmd | vibecommit connect` in CI it
120
+ * is the hang the source finding names first, burning the job's whole
121
+ * timeout with the refusal already on screen and no way to explain it.
122
+ *
123
+ * It also makes style guide §10.2 rule 2 literally true rather than nearly
124
+ * true: on the refusal path stdin is not read, resumed, or opened at all.
125
+ */
126
+ ask: async (question) => {
127
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
128
+ try {
129
+ return await rl.question(question);
130
+ }
131
+ finally {
132
+ rl.close();
133
+ }
134
+ },
135
+ now: () => new Date(),
136
+ selfPath: process.argv[1] ?? "",
137
+ };
138
+ }
139
+ async function main(argv) {
140
+ const [verb] = argv;
141
+ if (verb === undefined || verb === "--help" || verb === "-h") {
142
+ process.stdout.write(`${renderHelp()}\n`);
143
+ return EXIT.ok;
144
+ }
145
+ if (verb === "--version" || verb === "-v") {
146
+ process.stdout.write(`${CLIENT_VERSION}\n`);
147
+ return EXIT.ok;
148
+ }
149
+ if (!isVerb(verb)) {
150
+ // PR 7 (Q6): a block like every other failure — a code, and the way to the command list as the LAST line. No help dump.
151
+ const colour = resolveColour({
152
+ env: process.env,
153
+ isTty: process.stderr.isTTY === true,
154
+ noColorFlag: argv.includes("--no-color"),
155
+ json: false,
156
+ });
157
+ process.stderr.write(`${renderErrorBlock({
158
+ kind: "bad",
159
+ code: "UNKNOWN_COMMAND",
160
+ what: USAGE.unknownCommandWhat(verb),
161
+ why: [],
162
+ fixLabel: USAGE.unknownCommandFix,
163
+ fixes: [channelCommand(process.env, process.argv[1] ?? "", USAGE.unknownCommandHelp)],
164
+ }, colour).join("\n")}\n`);
165
+ return EXIT.usage;
166
+ }
167
+ const ctx = interactiveContext(argv);
168
+ // Slice 2 (S4, R3-11): a credential `g` made and did not keep is revoked by the NEXT verb that runs — once, at its start, silent,
169
+ // needing no credential, inside one 3 s bound across every row. Not for `--help`, not for `scan` (network-free by contract,
170
+ // README), not for a `--stdin`, piped or no-terminal `auth` (below), and never from a hook (this is the verb path; hooks branch off at the bottom of this file).
171
+ // F8 (L10): nor for `auth` run with `--stdin`, piped or with no terminal ("it only saves the credential" stays exactly true of it).
172
+ const authSavesOnly = verb === "auth" && (argv.includes("--stdin") || !ctx.stdinIsTty);
173
+ if (verb !== "scan" && !authSavesOnly && !argv.includes("--help") && !argv.includes("-h")) {
174
+ // Loaded HERE, on the verb path only (the connect.ts rule): `vibecommit hook` never imports the module that holds a token.
175
+ const { retryPendingRevokes } = await import("./pending_revoke.js");
176
+ await retryPendingRevokes(ctx.home, ctx.env, globalThis.fetch, ctx.now());
177
+ }
178
+ const code = await runVerb(verb, argv, ctx);
179
+ // `13A`: the nudge is the LAST line of a verb that SUCCEEDED — never after an error (the §13.6 fix
180
+ // line must stay last) and never with `--json`. It lives here, not in the verbs, so no verb's own
181
+ // output or exit path can reach it by accident; `updateNudge` itself fails silent.
182
+ if (code === EXIT.ok &&
183
+ (verb === "status" || verb === "connect" || verb === "auth") &&
184
+ !argv.includes("--json") &&
185
+ !argv.includes("--help") &&
186
+ !argv.includes("-h")) {
187
+ const lines = await updateNudge(ctx);
188
+ if (lines.length > 0)
189
+ process.stdout.write(`${lines.join("\n")}\n`);
190
+ }
191
+ return code;
192
+ }
193
+ /** The real browser beat for `auth` and `connect`: the real network and the real clock, plus `--device`. */
194
+ const realBrowser = (ctx, retry, options) => browserCredential(ctx, retry, {}, options);
195
+ async function runVerb(verb, argv, ctx) {
196
+ switch (verb) {
197
+ case "auth":
198
+ // `CR-216/U2`. Both readers are injected here rather than reached for
199
+ // inside the verb, the same way `why` takes its reader: the suite drives
200
+ // every arm, and neither a TTY nor a pipe can be typed into by a test.
201
+ return await auth(ctx, argv.slice(1), {
202
+ readSecret: readSecretFromTty,
203
+ readStdin,
204
+ // `CR-226`. The browser mint, first on a terminal; the two readers above
205
+ // stay the fallback. Injected HERE and nowhere else — the verb's default
206
+ // is paste-only, so this line is what makes the route reachable.
207
+ browser: realBrowser,
208
+ // `CR-236`. After a save inside a git work tree on a terminal, `auth` continues into `connect`.
209
+ // ⛔ NO `browser` HERE, AND THAT IS THE "ONE AUTH" GUARANTEE: this verb just saved a credential, so
210
+ // the chained `connect` finds one; were it ever absent it falls to the old warning, never to a
211
+ // second sign-in. `test/auth-connect-chain.test.ts` brace-matches this call to keep it so.
212
+ // The backfill offer IS passed: it is not a sign-in, it asks its own y/N (default N) on a terminal only, and the chain
213
+ // itself runs only on a terminal (the `!piped` gate, D32 guard 1), so a piped or CI `auth` never reaches it.
214
+ // Slice 2: the account's network is passed (it is not a sign-in), so the chained `connect` syncs and shows the picker's
215
+ // "on your account" group. `g` needs a `browser`, which this call never has, so the chain can never reach it.
216
+ connect: (c) => connect(c, {}, { backfill: backfillForConnect, account: { fetch: globalThis.fetch } }),
217
+ // R3-7: a credential this verb saves, off the chain, is synced before it returns (only on a machine that has synced before).
218
+ account: { fetch: globalThis.fetch },
219
+ });
220
+ case "connect":
221
+ // `--sign-in` (CR-084d) is a FLAG, not a verb: `VERBS` is what `renderHelp`
222
+ // iterates, so a sixth verb would move `test/help.golden.txt`, and the
223
+ // brief for this task is explicit that the golden file must not move.
224
+ // `CR-226`: the browser beat for a fresh machine. Same rule as `auth` —
225
+ // `connect`'s default never opens a browser, so without this argument the
226
+ // route is unreachable from the install path.
227
+ // `TODOS[184]`: `help` added — `connect` has no `argv` param, so its `--help`/`-h` check reads this
228
+ // option field, computed the same way `signIn` already is.
229
+ return await connect(ctx, {
230
+ signIn: argv.includes("--sign-in"),
231
+ // `CR-234d`: sign in by code.
232
+ device: argv.includes("--device"),
233
+ // `D28`: the typed prompt even on a terminal — the checklist's way out for a screen reader.
234
+ typed: argv.includes("--typed"),
235
+ help: argv.includes("--help") || argv.includes("-h"),
236
+ },
237
+ // `D14`/`D15`: the backfill offer after "Capture is on" (PR 5's `backfillForConnect`).
238
+ // Slice 2: `account` is the network for the sync, the "on your account" group, `g` and the joining-machine question.
239
+ { browser: realBrowser, backfill: backfillForConnect, account: { fetch: globalThis.fetch } });
240
+ case "scan":
241
+ // `D33 · 27A`. Read-only, no network, and a public verb so an agent can list capturable repos
242
+ // without entering a consent flow. Reads its own `--json` like `status`.
243
+ return scan(ctx, argv.slice(1));
244
+ case "backfill":
245
+ // `D14`/`D15`. Asks first (default N), uploads earlier sessions one at a time, no commit links. Takes no arguments.
246
+ return await backfill(ctx, argv.slice(1));
247
+ case "status":
248
+ // `argv.slice(1)` for the same reason `why` and `report` take it: `status`
249
+ // reads its own `--json` flag rather than this package acquiring an
250
+ // argument parser it has deliberately never had (DESIGN.md §13.3).
251
+ // Slice 2 (S4): one bounded account sync first, then the screen. The sync only refreshes the files `status` reads.
252
+ return await statusVerb(ctx, argv.slice(1), { fetch: globalThis.fetch });
253
+ case "off":
254
+ // `TODOS[184]`: `off` now takes its trailing argv, so a stray argument (including `--help`
255
+ // landing here by accident) can no longer silently revoke consent.
256
+ // Slice 2 (S4): the turn-off is local first; with a credential it is then queued for the account and synced once.
257
+ return await offVerb(ctx, argv.slice(1), { fetch: globalThis.fetch });
258
+ case "why":
259
+ // CR-086. `argv.slice(1)` is everything after the verb — there is no
260
+ // argument parser in this package and `why` does not add one; it reads
261
+ // `<file>:<line>` and refuses `--json`, which is the smallest shape that
262
+ // satisfies the verbatim-approved `--help` sentence.
263
+ return await why(ctx, argv.slice(1));
264
+ case "report":
265
+ // CR-108, W10. Same shape as `why`: `argv.slice(1)` is everything after
266
+ // the verb, and the verb reads its own two flags rather than adding an
267
+ // argument parser this package deliberately does not have.
268
+ return await report(ctx, argv.slice(1));
269
+ case "finalize":
270
+ // `TODOS[146]`. Same shape as `report`: the verb reads its own flags.
271
+ return await finalize(ctx, argv.slice(1));
272
+ }
273
+ }
274
+ /**
275
+ * Read a secret from the terminal WITHOUT ECHOING IT — `CR-216/U2`.
276
+ *
277
+ * `ctx.ask` is not reusable for this. It is the consent prompt: it echoes, which
278
+ * is correct for `y/N` and wrong for a long-lived credential, which would then
279
+ * sit in the terminal's scrollback to be scrolled back to, screenshotted into a
280
+ * bug report, or captured by a terminal logger.
281
+ *
282
+ * The mute is the ordinary readline technique: `output` receives the prompt
283
+ * first and is muted only for the keystrokes that follow, so the question is
284
+ * still visible and the answer never is. The trailing newline is ours to write —
285
+ * the user's own Return was swallowed with the rest, and without it whatever
286
+ * prints next lands on the prompt line.
287
+ *
288
+ * Nothing here holds the secret: `rl.question`'s resolved string goes straight to
289
+ * the caller, which wraps it in an `IngestCredential` before doing anything else
290
+ * with it.
291
+ */
292
+ function readSecretFromTty(prompt) {
293
+ let muted = false;
294
+ const muffled = new Writable({
295
+ write(chunk, encoding, cb) {
296
+ if (!muted)
297
+ process.stdout.write(chunk, encoding);
298
+ cb();
299
+ },
300
+ });
301
+ const rl = createInterface({ input: process.stdin, output: muffled, terminal: true });
302
+ const answer = rl.question(prompt);
303
+ muted = true;
304
+ return answer.finally(() => {
305
+ rl.close();
306
+ process.stdout.write("\n");
307
+ });
308
+ }
309
+ /**
310
+ * The INTERACTIVE half of the split exit contract — CR-022, D61 round 3 §D8.
311
+ *
312
+ * The hook half is enforced in `hooks/entry.ts`: swallow everything, exit 0, stay
313
+ * silent, because a non-zero hook feeds stderr back into the user's agent and
314
+ * derails the task they were actually doing. **An interactive verb must not
315
+ * inherit that blanket.** As originally written the exit-0/stdout-empty rule read
316
+ * as governing the whole binary, which would make `why` on a read-lane 5xx exit 0
317
+ * and print nothing — a read verb failing silently, which is the defect this
318
+ * split exists to prevent.
319
+ *
320
+ * So: same fault, same binary, two modes, two behaviours. Interactive gets exit 1
321
+ * and §13.6's shape.
322
+ *
323
+ * Two things this deliberately does NOT do:
324
+ * - **No signal handling.** `Ctrl-C` cancelling a `connect` prompt is normal
325
+ * CLI behaviour, and suppressing `SIGINT` globally would be a UX regression
326
+ * dressed up as a fix. The hook's signal override stays scoped to hook mode.
327
+ * - **No stack trace** unless `VIBECOMMIT_DEBUG` is set (§13.6, first
328
+ * prohibition). The detail then goes out RAW, after the block, rather than
329
+ * through `wrap()`, which would reflow a stack into nonsense.
330
+ */
331
+ function reportUnexpected(error, argv) {
332
+ const colour = resolveColour({
333
+ env: process.env,
334
+ isTty: process.stderr.isTTY === true,
335
+ noColorFlag: argv.includes("--no-color"),
336
+ json: argv.includes("--json"),
337
+ });
338
+ const block = {
339
+ kind: "bad",
340
+ code: "INTERNAL_ERROR",
341
+ what: INTERNAL.unexpectedWhat,
342
+ why: [INTERNAL.unexpectedWhy],
343
+ fixLabel: INTERNAL.unexpectedFix,
344
+ fixes: [INTERNAL.unexpectedDebugCommand(installChannel(process.env, process.argv[1] ?? "").command)],
345
+ };
346
+ // PR 7: under `--json` on a read verb the failure is ONE document on stdout and nothing on stderr — the same shape every
347
+ // other `--json` failure has. (`VIBECOMMIT_DEBUG` is an explicit opt-in to the raw detail on stderr, and still prints.)
348
+ const jsonVerb = argv.includes("--json") ? JSON_VERBS.find((v) => v === argv[0]) : undefined;
349
+ const lines = jsonVerb === undefined ? renderErrorBlock(block, colour) : [];
350
+ if (jsonVerb !== undefined) {
351
+ try {
352
+ writeSync(1, renderJsonError(jsonVerb, block));
353
+ }
354
+ catch {
355
+ // An unwritable stdout is not worth a second exception on this path.
356
+ }
357
+ }
358
+ if (process.env.VIBECOMMIT_DEBUG !== undefined)
359
+ lines.push(...(lines.length > 0 ? [""] : []), detailOf(error));
360
+ if (lines.length === 0)
361
+ return;
362
+ // `writeSync(2, …)` rather than `process.stderr.write`: stderr to a pipe is
363
+ // asynchronous and `process.exit` does not flush it, so the caller below would
364
+ // race its own diagnostic away. Same reason `finish()` uses it on fd 1.
365
+ try {
366
+ writeSync(2, `${lines.join("\n")}\n`);
367
+ }
368
+ catch {
369
+ // An unwritable stderr is not worth a second exception on this path.
370
+ }
371
+ }
372
+ /**
373
+ * The raw detail, for `VIBECOMMIT_DEBUG` only.
374
+ *
375
+ * Interpolating an error is safe by construction here: an `IngestCredential`
376
+ * renders `vcik_…redacted` through `toString`, `toJSON` and the inspect hook, so
377
+ * a stack that embedded one cannot print the plaintext.
378
+ */
379
+ function detailOf(error) {
380
+ if (error instanceof Error && typeof error.stack === "string")
381
+ return error.stack;
382
+ return String(error);
383
+ }
384
+ /**
385
+ * Catch what escapes `main()` entirely — a stray timer's throw, a rejection from
386
+ * a promise nobody awaited. Without these, Node prints its own stack trace to
387
+ * stderr, which §13.6 forbids outright.
388
+ *
389
+ * These force an exit rather than setting `process.exitCode`, because by
390
+ * definition something unexpected is still in flight and may be holding the event
391
+ * loop open. The `main()` rejection path below does not: nothing is in flight
392
+ * there, and letting the loop drain preserves whatever the verb already wrote.
393
+ */
394
+ function installInteractiveGuards(argv) {
395
+ const fail = (error) => {
396
+ reportUnexpected(error, argv);
397
+ process.exit(EXIT.failure);
398
+ };
399
+ process.on("uncaughtException", fail);
400
+ process.on("unhandledRejection", fail);
401
+ }
402
+ /** Run the interactive CLI for `argv` (everything after the binary): guards first, then the verb, then the exit code. */
403
+ export function runCli(argv) {
404
+ installInteractiveGuards(argv);
405
+ void main(argv).then((code) => {
406
+ process.exitCode = code;
407
+ }, (error) => {
408
+ reportUnexpected(error, argv);
409
+ process.exitCode = EXIT.failure;
410
+ });
411
+ }
412
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -50,6 +50,7 @@ import { glyph, paint, renderErrorBlock, tildePath, truncateMiddle, truncatePath
50
50
  import { writeLines } from "./context.js";
51
51
  import { credentialProblem } from "./credential_problem.js";
52
52
  import { authCommandFor, channelCommand, connectCommandFor } from "../install.js";
53
+ import { identityDigest } from "../identity.js";
53
54
  import { closeSession, closeSpecOf, ingestUrlUnavailableBlock, resolveSessionTargets } from "./finalize.js";
54
55
  import { scanMachine } from "./scan.js";
55
56
  import { renderVerbHelp } from "../verb_help.js";
@@ -501,6 +502,18 @@ function classify(outcome) {
501
502
  }
502
503
  }
503
504
  }
505
+ /**
506
+ * U5b — what a session's recorded firings are replayed with (`finalize.takeSessionLock`), under the lock `resolveSessionTargets` takes, BEFORE
507
+ * the close reads the spool union: after U6 the hook promotes nothing and writes no state file, so a replay is the only thing that does.
508
+ */
509
+ function replayConfigOf(run) {
510
+ const { ctx } = run;
511
+ return { home: ctx.home, env: ctx.env, nodeVersion: ctx.nodeVersion, identity: identityDigest({ url: run.url, env: ctx.env }), url: run.url };
512
+ }
513
+ /** A replay that left a firing unconsumed: no close, so no seal over lines it still owes. A rejected credential is the run's `rejected`. */
514
+ function replayStopped(step) {
515
+ return step.kind === "fatal" && step.why === "rejected" ? { kind: "rejected" } : { kind: "failed", reason: BACKFILL.replayStopped };
516
+ }
504
517
  async function uploadOne(run, candidate) {
505
518
  const { ctx } = run;
506
519
  const found = statFile(candidate.path);
@@ -546,9 +559,11 @@ async function uploadOne(run, candidate) {
546
559
  // `turnEndCwd` is the folder the OFFER verified this session belongs to (the agent's own index, or the first `cwd` the transcript
547
560
  // records): the evidence that put it in this repo's offer, handed to the resolver so a transcript with no `cwd` of its own still
548
561
  // resolves. It cannot name a repo the offer did not (the candidate is this repo's) and it is no fallback identity.
549
- const resolved = await resolveSessionTargets({ home: ctx.home, env: ctx.env, nowMs: ctx.now().getTime(), turnEndCwd: candidate.dir }, candidate.sessionId, candidate.path, { keys, slugOf: onceStrictRepoSlug(keys.worktree) }, releases);
562
+ const resolved = await resolveSessionTargets({ home: ctx.home, env: ctx.env, nowMs: ctx.now().getTime(), turnEndCwd: candidate.dir }, candidate.sessionId, candidate.path, { keys, slugOf: onceStrictRepoSlug(keys.worktree) }, releases, { replay: replayConfigOf(run) });
550
563
  if (resolved.kind === "busy")
551
564
  return { kind: "skipped", reason: BACKFILL.parentBusy };
565
+ if (resolved.kind === "replay-stopped")
566
+ return replayStopped(resolved.step);
552
567
  const heartbeat = resolved.heartbeat;
553
568
  // This repo's target: one of its work trees, or any work tree of the same git common dir (a session recorded in a LINKED work tree
554
569
  // belongs to the clone the offer was made for).
@@ -612,9 +627,11 @@ async function uploadParent(run, candidate) {
612
627
  return { kind: "skipped", reason: BACKFILL.turnedOff };
613
628
  const releases = [];
614
629
  try {
615
- const resolved = await resolveSessionTargets({ home: ctx.home, env: ctx.env, nowMs: ctx.now().getTime(), turnEndCwd: null }, candidate.sessionId, candidate.path, null, releases);
630
+ const resolved = await resolveSessionTargets({ home: ctx.home, env: ctx.env, nowMs: ctx.now().getTime(), turnEndCwd: null }, candidate.sessionId, candidate.path, null, releases, { replay: replayConfigOf(run) });
616
631
  if (resolved.kind === "busy")
617
632
  return { kind: "skipped", reason: BACKFILL.parentBusy };
633
+ if (resolved.kind === "replay-stopped")
634
+ return replayStopped(resolved.step);
618
635
  const heartbeat = resolved.heartbeat;
619
636
  if (resolved.targets.length === 0)
620
637
  return { kind: "skipped", reason: BACKFILL.parentTouchedNothing };