@vibe-commit/capture 0.6.0 → 0.7.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 (143) hide show
  1. package/README.md +84 -5
  2. package/dist/agent_hooks.d.ts +8 -0
  3. package/dist/agent_hooks.js +31 -1
  4. package/dist/agent_hooks.js.map +1 -1
  5. package/dist/agents/claude_code.d.ts +34 -0
  6. package/dist/agents/claude_code.js +141 -8
  7. package/dist/agents/claude_code.js.map +1 -1
  8. package/dist/agents/codex_cli.d.ts +13 -0
  9. package/dist/agents/codex_cli.js +241 -2
  10. package/dist/agents/codex_cli.js.map +1 -1
  11. package/dist/agents/cursor.d.ts +34 -0
  12. package/dist/agents/cursor.js +115 -1
  13. package/dist/agents/cursor.js.map +1 -1
  14. package/dist/agents/session_listing.d.ts +29 -0
  15. package/dist/agents/session_listing.js +49 -0
  16. package/dist/agents/session_listing.js.map +1 -0
  17. package/dist/agents/types.d.ts +118 -8
  18. package/dist/agents/types.js +17 -3
  19. package/dist/agents/types.js.map +1 -1
  20. package/dist/commands/approval_pending.d.ts +9 -0
  21. package/dist/commands/approval_pending.js +23 -0
  22. package/dist/commands/approval_pending.js.map +1 -0
  23. package/dist/commands/auth.js +31 -11
  24. package/dist/commands/auth.js.map +1 -1
  25. package/dist/commands/backfill.d.ts +160 -0
  26. package/dist/commands/backfill.js +748 -0
  27. package/dist/commands/backfill.js.map +1 -0
  28. package/dist/commands/browser_credential.d.ts +2 -0
  29. package/dist/commands/browser_credential.js +31 -14
  30. package/dist/commands/browser_credential.js.map +1 -1
  31. package/dist/commands/connect.d.ts +137 -0
  32. package/dist/commands/connect.js +203 -109
  33. package/dist/commands/connect.js.map +1 -1
  34. package/dist/commands/connect_picker.d.ts +27 -0
  35. package/dist/commands/connect_picker.js +415 -0
  36. package/dist/commands/connect_picker.js.map +1 -0
  37. package/dist/commands/context.d.ts +16 -0
  38. package/dist/commands/context.js +27 -0
  39. package/dist/commands/context.js.map +1 -1
  40. package/dist/commands/credential_problem.d.ts +15 -4
  41. package/dist/commands/credential_problem.js +29 -22
  42. package/dist/commands/credential_problem.js.map +1 -1
  43. package/dist/commands/device_credential.d.ts +2 -0
  44. package/dist/commands/device_credential.js +16 -14
  45. package/dist/commands/device_credential.js.map +1 -1
  46. package/dist/commands/finalize.d.ts +95 -0
  47. package/dist/commands/finalize.js +249 -74
  48. package/dist/commands/finalize.js.map +1 -1
  49. package/dist/commands/finish_mint.d.ts +2 -6
  50. package/dist/commands/finish_mint.js +10 -2
  51. package/dist/commands/finish_mint.js.map +1 -1
  52. package/dist/commands/report.js +38 -30
  53. package/dist/commands/report.js.map +1 -1
  54. package/dist/commands/scan.d.ts +162 -0
  55. package/dist/commands/scan.js +507 -0
  56. package/dist/commands/scan.js.map +1 -0
  57. package/dist/commands/status.js +119 -22
  58. package/dist/commands/status.js.map +1 -1
  59. package/dist/commands/status_hooks.d.ts +14 -0
  60. package/dist/commands/status_hooks.js +81 -0
  61. package/dist/commands/status_hooks.js.map +1 -0
  62. package/dist/commands/status_machine.d.ts +18 -0
  63. package/dist/commands/status_machine.js +149 -0
  64. package/dist/commands/status_machine.js.map +1 -0
  65. package/dist/commands/why.js +44 -31
  66. package/dist/commands/why.js.map +1 -1
  67. package/dist/consent.d.ts +111 -0
  68. package/dist/consent.js +203 -1
  69. package/dist/consent.js.map +1 -1
  70. package/dist/copy/error_codes.d.ts +222 -0
  71. package/dist/copy/error_codes.js +105 -0
  72. package/dist/copy/error_codes.js.map +1 -0
  73. package/dist/copy/index.d.ts +2 -2
  74. package/dist/copy/index.js +2 -2
  75. package/dist/copy/index.js.map +1 -1
  76. package/dist/copy/strings.d.ts +497 -29
  77. package/dist/copy/strings.js +601 -35
  78. package/dist/copy/strings.js.map +1 -1
  79. package/dist/copy/time.d.ts +11 -0
  80. package/dist/copy/time.js +18 -0
  81. package/dist/copy/time.js.map +1 -1
  82. package/dist/credential.d.ts +5 -1
  83. package/dist/credential.js +2 -2
  84. package/dist/credential.js.map +1 -1
  85. package/dist/git.d.ts +43 -0
  86. package/dist/git.js +95 -0
  87. package/dist/git.js.map +1 -1
  88. package/dist/hooks/entry.d.ts +18 -1
  89. package/dist/hooks/entry.js +110 -13
  90. package/dist/hooks/entry.js.map +1 -1
  91. package/dist/hooks/post_commit.js +3 -3
  92. package/dist/hooks/post_commit.js.map +1 -1
  93. package/dist/hooks/post_rewrite.js +6 -4
  94. package/dist/hooks/post_rewrite.js.map +1 -1
  95. package/dist/hooks/session_start.d.ts +93 -0
  96. package/dist/hooks/session_start.js +199 -0
  97. package/dist/hooks/session_start.js.map +1 -0
  98. package/dist/index.d.ts +1 -1
  99. package/dist/index.js +56 -8
  100. package/dist/index.js.map +1 -1
  101. package/dist/install.d.ts +57 -0
  102. package/dist/install.js +54 -20
  103. package/dist/install.js.map +1 -1
  104. package/dist/json.d.ts +32 -9
  105. package/dist/json.js +35 -8
  106. package/dist/json.js.map +1 -1
  107. package/dist/machine_state.d.ts +52 -0
  108. package/dist/machine_state.js +63 -0
  109. package/dist/machine_state.js.map +1 -0
  110. package/dist/oauth/session.d.ts +1 -0
  111. package/dist/org_pending.d.ts +52 -0
  112. package/dist/org_pending.js +109 -0
  113. package/dist/org_pending.js.map +1 -0
  114. package/dist/paths.d.ts +50 -0
  115. package/dist/paths.js +64 -0
  116. package/dist/paths.js.map +1 -1
  117. package/dist/picker.d.ts +180 -0
  118. package/dist/picker.js +477 -0
  119. package/dist/picker.js.map +1 -0
  120. package/dist/picker_checklist.d.ts +125 -0
  121. package/dist/picker_checklist.js +375 -0
  122. package/dist/picker_checklist.js.map +1 -0
  123. package/dist/policy.d.ts +54 -0
  124. package/dist/policy.js +47 -0
  125. package/dist/policy.js.map +1 -1
  126. package/dist/post.d.ts +50 -16
  127. package/dist/post.js +265 -58
  128. package/dist/post.js.map +1 -1
  129. package/dist/state.d.ts +6 -0
  130. package/dist/state.js +38 -1
  131. package/dist/state.js.map +1 -1
  132. package/dist/system_message.d.ts +8 -1
  133. package/dist/system_message.js +19 -1
  134. package/dist/system_message.js.map +1 -1
  135. package/dist/term.d.ts +28 -2
  136. package/dist/term.js +23 -2
  137. package/dist/term.js.map +1 -1
  138. package/dist/verb_help.d.ts +13 -0
  139. package/dist/verb_help.js +40 -0
  140. package/dist/verb_help.js.map +1 -0
  141. package/dist/version.d.ts +1 -1
  142. package/dist/version.js +1 -1
  143. package/package.json +5 -9
@@ -28,12 +28,20 @@ export const HELP = {
28
28
  commands: {
29
29
  // `CR-216/U2`. FIRST in the list by habit, not by sequence: that "nothing for `connect` to use until
30
30
  // this has run" reason has been false since `CR-226` (`connect` signs in itself on a machine with no
31
- // credential). `connect` is the golden path; this verb renews or replaces the credential, and inside a
32
- // git work tree it then continues into `connect`. Outside one it saves the credential and stops there
33
- // — the escape hatch for a renewal that should not touch the repo you happen to be standing in.
34
- auth: "Renew or replace this machine's credential. In a git repo it then connects that repo; elsewhere it only saves the credential.",
35
- connect: "Connect this repo and capture the current session.",
36
- status: "Show whether capture is on for this repo and when it last succeeded.",
31
+ // credential). `connect` is the golden path; this verb renews or replaces the credential, and on a terminal it
32
+ // then continues into `connect` — inside a git work tree, or (0.7.0, D32) outside one on a machine that captures
33
+ // nothing yet. With `--stdin` or no terminal it saves the credential and stops there.
34
+ auth: "Renew or replace this machine's credential. On a terminal it then goes on to connect: in a git repo, or on a machine that captures nothing yet. Otherwise it only saves the credential.",
35
+ // 0.7.0 (D34): from any folder. A repo with a remote is chosen in the picker; a repo with no remote keeps the old flow.
36
+ connect: "Choose the repositories to capture, from any folder, and connect them. On a machine with no credential it signs in first.",
37
+ // `D33 · 27A`. A public verb so an AI agent can list capturable repos without entering a consent
38
+ // flow. It opens no session file and changes no setting, which is what lets it be run before `connect`.
39
+ // ⚠ NOT "changes nothing" (PR 1 F1): opening Codex's index read-only makes SQLite create its own
40
+ // `-wal`/`-shm` journal files beside it, as any SQLite reader does, so the line claims only what is true.
41
+ scan: "List the repositories coding agents have been used in on this machine. Uploads nothing and changes no setting.",
42
+ // `D14`/`D15`. Offered by `connect` too. Asks first (default N) and says what it will not do.
43
+ backfill: "Upload earlier coding-agent sessions still on this machine, for the repositories you chose. Asks first; no commit links.",
44
+ status: "Show whether capture is on for this repo, or for this machine outside a repo, and when it last succeeded.",
37
45
  off: "Stop capturing for this repo.",
38
46
  // VERBATIM-APPROVED (D61 §PS6). Any reword needs a new claims-register row.
39
47
  why: "Show the conversation turns recorded against the commit that last changed this line.",
@@ -42,6 +50,117 @@ export const HELP = {
42
50
  finalize: "Close a finished session's last turn so commits made in it can be recorded.",
43
51
  },
44
52
  docsUrl: "https://vibecommit.ai/docs/what-we-upload",
53
+ /**
54
+ * PR 6 item 2 (devex review): what `<verb> --help` prints BEYOND the one sentence above, which stays the first
55
+ * paragraph. Agents read `--help` first, and until now the flags and exit codes existed only in the README.
56
+ *
57
+ * ⚠ Every flag here is one the verb's own argv reader acts on (`src/index.ts`, and each verb's parser), and every exit
58
+ * code is one the verb returns (`src/exit.ts`). A flag the verb ignores, or a code it never returns, would be copy no
59
+ * gate can check, so `test/verb-help.test.ts` ties each list to the source it describes.
60
+ */
61
+ verbs: {
62
+ connect: {
63
+ usage: "vibecommit connect [--device] [--sign-in] [--typed]",
64
+ options: [
65
+ ["--device", "Sign in with a code approved on another device, for a machine with no browser."],
66
+ ["--sign-in", "Sign in through the browser before connecting."],
67
+ ["--typed", "Use the typed prompt even on a terminal, for a screen reader."],
68
+ ],
69
+ exits: [
70
+ ["0", "Done."],
71
+ ["1", "Failed, or a replacement credential was declined."],
72
+ ["2", "Bad arguments, or no person at a terminal to answer the consent question."],
73
+ ["3", "Not connected: this machine's credential is not accepted."],
74
+ ],
75
+ },
76
+ auth: {
77
+ usage: "vibecommit auth [--stdin | --paste | --device]",
78
+ options: [
79
+ ["--stdin", "Read the credential from standard input and stop after saving it."],
80
+ ["--paste", "Skip the browser and paste a credential made in Settings."],
81
+ ["--device", "Sign in with a code approved on another device."],
82
+ ],
83
+ exits: [
84
+ ["0", "Credential saved."],
85
+ ["1", "Nothing was saved."],
86
+ ["2", "Bad arguments, such as a credential passed as an argument."],
87
+ ],
88
+ },
89
+ scan: {
90
+ usage: "vibecommit scan [--json]",
91
+ options: [["--json", "Print one JSON document instead of text."]],
92
+ exits: [
93
+ ["0", "Repositories found."],
94
+ ["4", "None found."],
95
+ ],
96
+ },
97
+ backfill: {
98
+ usage: "vibecommit backfill",
99
+ options: [],
100
+ exits: [
101
+ ["0", "Done, declined at the question, or interrupted with Ctrl+C. Sessions held for an organization's approval count as done."],
102
+ ["1", "The server stopped the run, could not be reached or refused this machine's credential, or it could not run. Nothing already sent is lost; run it again to resume."],
103
+ ["2", "No terminal, or an argument was given."],
104
+ ["3", "Not connected."],
105
+ ["4", "Nothing to upload."],
106
+ ],
107
+ },
108
+ status: {
109
+ usage: "vibecommit status [--json]",
110
+ options: [["--json", "Print one JSON document instead of text."]],
111
+ exits: [
112
+ ["0", "Reported."],
113
+ ["1", "Failed."],
114
+ ["2", "Bad arguments."],
115
+ ["3", "Not connected."],
116
+ ],
117
+ },
118
+ why: {
119
+ usage: "vibecommit why <file>:<line> [--json]",
120
+ options: [["--json", "Print one JSON document instead of text."]],
121
+ exits: [
122
+ ["0", "Answered."],
123
+ ["1", "Failed."],
124
+ ["2", "Bad arguments."],
125
+ ["3", "Not connected."],
126
+ ],
127
+ },
128
+ report: {
129
+ usage: "vibecommit report --since <date> [--ref <ref>] [--json]",
130
+ options: [
131
+ ["--since", "Start of the window, as YYYY-MM-DD. Required."],
132
+ ["--ref", "The branch or commit to count. Default: the current branch."],
133
+ ["--json", "Print one JSON document instead of text."],
134
+ ],
135
+ exits: [
136
+ ["0", "Reported."],
137
+ ["1", "Failed."],
138
+ ["2", "Bad arguments."],
139
+ ["3", "Not connected."],
140
+ ["4", "The window is empty: nothing to report."],
141
+ ],
142
+ },
143
+ finalize: {
144
+ usage: "vibecommit finalize --session <id> | --latest [--force]",
145
+ options: [
146
+ ["--session", "The session to close."],
147
+ ["--latest", "The most recent session, when only one is recent."],
148
+ ["--force", "Skip the check that the transcript has stopped changing. Only for a session that is over."],
149
+ ],
150
+ exits: [
151
+ ["0", "Closed: the server confirmed the last turn is recorded."],
152
+ ["1", "Not confirmed closed: not a git repository, a transcript that cannot be read, a credential that is no longer accepted, or a delivery that failed."],
153
+ ["2", "Nothing was sent: bad arguments, an unknown or still-running session, or several recent sessions for --latest."],
154
+ ["3", "Not connected: this repo is not captured, or this machine has no credential."],
155
+ ["5", "Delivered, but the server did not confirm it. Treat it as not closed."],
156
+ ],
157
+ },
158
+ },
159
+ /** The two section labels of a verb's help. */
160
+ optionsLabel: "Options:",
161
+ /** A verb with no flags of its own (`backfill`) says so under the label, so "Options:" never sits over nothing. */
162
+ noOptions: "None. It takes no arguments.",
163
+ exitCodesLabel: "Exit codes:",
45
164
  };
46
165
  /**
47
166
  * The ENTRY POINT's own chrome and its two usage failures — `CR-090d`.
@@ -74,8 +193,14 @@ export const USAGE = {
74
193
  usageLabel: "Usage:",
75
194
  commandsLabel: "Commands:",
76
195
  docsLabel: "Docs:",
77
- /** The unknown-verb line. Help follows it, so this says only what went wrong. */
78
- unknownCommand: (verb) => `Unknown command: ${verb}`,
196
+ /**
197
+ * The unknown-verb block (PR 7, Q6: `UNKNOWN_COMMAND`, routed through the shared renderer). The help text is no longer
198
+ * dumped after it, so the fix — the way to the list of commands — is the last thing on screen. The verb is whatever
199
+ * the user typed, so it is cut to a length the line can hold.
200
+ */
201
+ unknownCommandWhat: (verb) => `\`${verb.length > 40 ? `${verb.slice(0, 39)}…` : verb}\` is not a vibecommit command.`,
202
+ unknownCommandFix: "See the commands:",
203
+ unknownCommandHelp: "vibecommit --help",
79
204
  };
80
205
  /** `vibecommit status` (CR-021). */
81
206
  /**
@@ -108,6 +233,25 @@ export const STATUS = {
108
233
  * changed them to match the screen rather than changing the screen.
109
234
  */
110
235
  repoLabel: "repo",
236
+ /** 0.7.0, D6 · 2A: why this repo is on. One row under `repo`, only inside a repo that is on. */
237
+ basisLabel: "basis",
238
+ basisOwner: (owner) => `owner grant ${owner}/`,
239
+ basisRepo: "repo grant",
240
+ basisClone: "per clone",
241
+ /** The machine-level screen (any folder that is not a work tree). */
242
+ offMachine: "Capture is not on for any repository on this machine.",
243
+ ownersLabel: "owners",
244
+ reposLabel: "repos",
245
+ offLabel: "off",
246
+ /**
247
+ * Machine-level `status`: the repos whose own last upload was refused because their org has not approved capture. §10.2's held
248
+ * vocabulary — the beat word `held`, and the backfill screen's "the org must approve capture" (Alexander, 2026-10-24 17:20).
249
+ */
250
+ heldLabel: "held",
251
+ heldReason: "the org must approve capture",
252
+ ownerTail: "all its repos, including ones you clone later",
253
+ changeLabel: "Change what is captured",
254
+ turnOffInside: "Turn one repo off, run inside it",
111
255
  lastSuccessLabel: "last sent",
112
256
  dashboardLabel: "dashboard",
113
257
  /** `<slug> (git toplevel <path>)` — §10.3's repo row. */
@@ -128,6 +272,21 @@ export const STATUS = {
128
272
  * after the rows, with the glyph turned `warn`. The date is absolute and in UTC.
129
273
  */
130
274
  credentialEnding: (date, authCommand) => `This machine's credential ends ${date}. Run \`${authCommand}\` to renew it.`,
275
+ /**
276
+ * 0.7.0. A `grants.json` that is present and unreadable denies capture for every clone that has a remote
277
+ * (`consent.ts`'s `isCaptureAllowed`), and a hook cannot say so, so `status` does. The file name is the user's
278
+ * to fix; `connect` is where the next consent lands once it is readable.
279
+ */
280
+ grantsUnreadable: (connect) => `~/.vibecommit/grants.json can't be read, so capture is stopped on this machine for every repository that has a remote. Fix the file or move it aside, then run \`${connect}\`.`,
281
+ /**
282
+ * 0.7.0, `SessionStart` (T21). `npm i -g` alone never adds the event to an agent's config, so a 0.6.x machine is
283
+ * missing it until `connect` runs. Only said on the npm channel: the plugin ships its own `hooks.json`, and its
284
+ * contract (no `SessionStart`, no git hooks) is unchanged.
285
+ */
286
+ sessionStartMissing: (agents, connect) => `${agents} will not add commit hooks to a new clone: VibeCommit's SessionStart hook is not registered there. Run \`${connect}\` to register it.`,
287
+ /** 0.7.0, D4 · A. A hook the user deleted stays deleted; `connect` is the watched way back. */
288
+ commitHooksOff: (connect) => `Commit hooks are off in this repository: sessions are captured, commits are not linked to them. Run \`${connect}\` here to add them.`,
289
+ commitHooksForeign: (connect) => `This clone has its own commit hook, so VibeCommit's is not installed here. Run \`${connect}\` here to chain it.`,
131
290
  credentialShadowed: "VIBECOMMIT_TOKEN is set and takes precedence over the credential saved on this machine. Unset it to use the saved one.",
132
291
  /**
133
292
  * `CR-228`, `TODOS[142]` — bytes the client ADVANCED PAST without delivering.
@@ -154,8 +313,8 @@ export const STATUS = {
154
313
  * turn only when a following prompt arrives, so nothing will ever seal this one and the commits made in
155
314
  * it cannot be recorded. States the consequence and the command; promises nothing about the commits.
156
315
  */
157
- openTail: (sessions) => `${sessions === 1 ? "1 session has" : `${sessions} sessions have`} ended with the last turn still ` +
158
- "open, so commits made in that turn are not recorded. Run `vibecommit finalize --latest` to close it.",
316
+ openTail: (sessions, finalizeLatest) => `${sessions === 1 ? "1 session has" : `${sessions} sessions have`} ended with the last turn still ` +
317
+ `open, so commits made in that turn are not recorded. Run \`${finalizeLatest}\` to close it.`,
159
318
  /**
160
319
  * `TODOS[145]` — commits the client has seen and is holding because no turn has closed yet. Normal
161
320
  * mid-session; shown as a fact, not as a fault.
@@ -259,12 +418,12 @@ export const STATUS = {
259
418
  * cannot tell a crashed session from one idle at lunch, and closing a session still in progress seals
260
419
  * a turn that was not actually over.
261
420
  */
262
- heldIdle: (bytes, sessions, commits, ids = []) => {
421
+ heldIdle: (bytes, sessions, commits, finalizeSession, ids = []) => {
263
422
  const who = sessions === 1 ? `session ${namedIds(ids)}` : `${sessions} sessions (${namedIds(ids)})`;
264
423
  const commitsClause = commits > 0 ? `, including ${commits} ${commits === 1 ? "commit" : "commits"}` : "";
265
424
  const action = sessions === 1
266
- ? `If that session is finished, \`vibecommit finalize --session ${ids[0]}\` tries to send them and, if that works, closes it`
267
- : "If a session above is finished, `vibecommit finalize --session <id>` tries to send its bytes and, if that works, closes it";
425
+ ? `If that session is finished, \`${finalizeSession} ${ids[0]}\` tries to send them and, if that works, closes it`
426
+ : `If a session above is finished, \`${finalizeSession} <id>\` tries to send its bytes and, if that works, closes it`;
268
427
  return (`${BYTE_COUNT.format(bytes)} ${bytes === 1 ? "byte" : "bytes"} held from ${who}${commitsClause}, with no hook in over 30 ` +
269
428
  `minutes. ${action}; anything held past 24 hours is written off instead.`);
270
429
  },
@@ -299,7 +458,18 @@ export const CONNECT = {
299
458
  * the nouns are transcribed rather than reworded: *"the output of commands it
300
459
  * ran"*, never *"Bash tool results"*.
301
460
  */
302
- consentHeading: "VibeCommit uploads your Claude Code transcripts for this repo.",
461
+ consentHeading: "VibeCommit uploads your coding-agent transcripts for this repo.",
462
+ /**
463
+ * D23 · 17A: the picker path's heading (every `connect` that shows the picker, in a repo or not). `consentHeading`
464
+ * above remains only on the no-remote (`local:`) in-repo path, where there is nothing to pick (D35 · 29A: "Claude
465
+ * Code transcripts" understated what uploads once `connect` also wired Codex and Cursor hooks, 0.2.0).
466
+ */
467
+ consentHeadingPicker: "VibeCommit uploads your coding-agent transcripts for the repositories you choose.",
468
+ /**
469
+ * ONE sentence, not a paragraph, added on the picker path only (design doc step 2, "The disclosure"): what the grant
470
+ * covers beyond the repos named on screen. The four paragraphs under either heading stay byte-identical.
471
+ */
472
+ consentScope: "The choice covers future clones of these repositories too, and adds commit hooks to each clone where it can.",
303
473
  /**
304
474
  * ⚠ "private-repo paths" IS THE APPROVED WORDING, and it is the reason
305
475
  * `private` is deliberately NOT registered in `claims.ts`: a substring gate on
@@ -407,11 +577,33 @@ export const CONNECT = {
407
577
  // --- Mechanism copy (CR-016). ---
408
578
  /** The `[y/N]` suffix. The capital is the default and the default is NO. */
409
579
  consentAnswers: "[y/N]",
410
- /** Non-TTY refusal — DESIGN.md §13.7 exit 2, D65 §DR7. */
411
- nonInteractiveWhat: "Consent needs an interactive terminal.",
412
- nonInteractiveWhy: "Standard input is not a terminal, so the consent prompt cannot be answered. Nothing was read and nothing was uploaded.",
413
- nonInteractiveFix: "Run this in a terminal:",
580
+ /**
581
+ * Non-TTY refusal — DESIGN.md §13.7 exit 2, D65 §DR7. PR 6 item 6: written for an AGENT that hit it, which is who reaches
582
+ * this branch. It says who must answer ("a person"), that an agent cannot, and what to relay, and it carries the code
583
+ * `CONSENT_NEEDS_TTY` for a wrapper to key on — PR 7 registered it and renders it on the block's own code line, so it is not
584
+ * repeated in the sentence. No `--yes`: that would bypass consent (D65 §DR7). The `what` line has no "you" (§13.6).
585
+ */
586
+ nonInteractiveWhat: "Consent needs a person at a terminal.",
587
+ nonInteractiveWhy: "An agent cannot answer it, and standard input is not a terminal. Nothing was read and nothing was uploaded.",
588
+ /** A repo with no remote has a folder to name; the label is the fix and the command follows it. */
589
+ nonInteractiveFix: (folder) => `Ask the user to run this in ${folder}:`,
590
+ /**
591
+ * The picker path's refusal (VQ + VT, PR 6 item 6 fit): it names no folder (the picker is not about one repo), has no
592
+ * "you" in the `what` line, and is reached BEFORE the scan beat, so "Nothing was read and nothing was uploaded" stays
593
+ * literally true.
594
+ */
595
+ pickerNonInteractiveWhat: "Choosing repositories needs a person at a terminal.",
596
+ pickerNonInteractiveWhy: "An agent cannot answer it, and standard input is not a terminal, so no repository can be chosen and consent cannot be asked. Nothing was read and nothing was uploaded.",
597
+ pickerNonInteractiveFix: "Ask the user to run this in a terminal:",
414
598
  alreadyConnected: "Capture is already on for this repository.",
599
+ /**
600
+ * 0.7.0. `grants.json` is present and unreadable, which denies this repository whatever consent it has (see
601
+ * `consent.ts`'s `isCaptureAllowed`). Answering the consent prompt could not change that, so `connect` stops
602
+ * BEFORE asking rather than take a yes it cannot honour.
603
+ */
604
+ grantsUnreadableWhat: "Capture can't be turned on: ~/.vibecommit/grants.json can't be read.",
605
+ grantsUnreadableWhy: "Capture is stopped on this machine for every repository that has a remote until the file is readable. Nothing was changed.",
606
+ grantsUnreadableFixLabel: "Fix the file or move it aside, then run:",
415
607
  /**
416
608
  * The browser sign-in beat (PKCE loopback → minted ingest credential) is the
417
609
  * oauth lane's, not this task's. Until it lands, the supported path is the one
@@ -531,12 +723,12 @@ export const COMMIT_HOOK = {
531
723
  installed: (repo) => `Commit capture is on for this clone (${repo}). Commits are recorded as they happen, including ones made by \`make\`, \`gh\`, an alias or a script.`,
532
724
  chained: (repo) => `Commit capture is on for this clone (${repo}). You already had a \`post-commit\` hook — it was kept and still runs first; ours runs after it.`,
533
725
  already: "Commit capture was already on for this clone.",
534
- perClone: "Run `vibecommit connect` in each clone you want commits recorded for — git hooks are per-clone, not per-account.",
726
+ perClone: (connect) => `Run \`${connect}\` in each clone you want commits recorded for — git hooks are per-clone, not per-account.`,
535
727
  hooksPathWhat: "Commit capture could NOT be installed for this clone.",
536
728
  hooksPathWhy: (configured) => `This repository sets \`core.hooksPath\` to ${configured}, so git ignores \`.git/hooks\` entirely and a hook installed there would never run. Transcript capture is unaffected; only commit observation is.`,
537
729
  hooksPathFixLabel: "Fix, either one:",
538
730
  hooksPathFixUnset: "git config --unset core.hooksPath",
539
- hooksPathFixManual: "or add `vibecommit post-commit` to your existing post-commit hook",
731
+ hooksPathFixManual: (postCommit) => `or add \`${postCommit}\` to your existing post-commit hook`,
540
732
  failedWhat: "Commit capture could NOT be installed for this clone.",
541
733
  failedWhy: (why) => `Writing the \`post-commit\` hook failed: ${why}. Transcript capture is unaffected; only commit observation is.`,
542
734
  };
@@ -567,7 +759,7 @@ export const AGENT_HOOKS = {
567
759
  refusedWhat: (agent) => `Session capture could NOT be set up for ${agent}.`,
568
760
  refusedWhy: (path, why) => `${path} could not be read as JSON (${why}), and it was left exactly as it is. Overwriting a file we cannot parse would discard settings we cannot see.`,
569
761
  refusedFixLabel: "Fix:",
570
- refusedFix: (path) => `check ${path} for a syntax error, then run \`vibecommit connect\` again`,
762
+ refusedFix: (path, connect) => `check ${path} for a syntax error, then run \`${connect}\` again`,
571
763
  failedWhat: (agent) => `Session capture could NOT be set up for ${agent}.`,
572
764
  failedWhy: (path, why) => `Writing ${path} failed: ${why}. Commit capture and any agent already configured are unaffected.`,
573
765
  };
@@ -697,9 +889,302 @@ export const SIGNIN = {
697
889
  busyWhy: "Only one process may renew a sign-in at a time, and the one holding it did not finish. Nothing was read and the stored sign-in is unchanged.",
698
890
  busyFix: "Run it again in a moment:",
699
891
  };
892
+ /**
893
+ * `vibecommit scan` — design review D17 · 11A, D26, D27, D33 · 27A, and the screen VQ drew for it
894
+ * (style guide §10.13) on 2026-10-24.
895
+ *
896
+ * ⛔ **The nouns are the agent-neutral ones** (D11 · A, D35): "coding agent", never "Claude Code
897
+ * session", because the scan covers three agents and the picker's heading says the same. The agent
898
+ * NAMES appear only on a repo's second line, where they are facts about which store a count came from.
899
+ *
900
+ * ⚠ **Every sentence here is a claims-register row** (the register is `code`'s, VR's PR). None says
901
+ * "every" or "all": the scan counts transcripts still on disk, and what `cleanupPeriodDays` already
902
+ * deleted is not among them.
903
+ */
904
+ export const SCAN = {
905
+ /** The beat line, drawn in the existing beat shape and overwritten once with `done` (D17). */
906
+ beat: "Looking for repositories on this machine...",
907
+ beatDone: "done",
908
+ found: (count) => `Found ${count} ${count === 1 ? "repository" : "repositories"} you've used a coding agent in on this machine.`,
909
+ sessions: (count) => `${count} ${count === 1 ? "session" : "sessions"}`,
910
+ since: (date) => `since ${date}`,
911
+ /** What an agent is called on a repo's second line. The ids are wire values, not labels. */
912
+ agent: { "claude-code": "Claude Code", "codex-cli": "Codex", cursor: "Cursor" },
913
+ notListed: "Not listed here:",
914
+ noRemote: (count, connect) => count === 1 ? `1 repo with no remote: run \`${connect}\` inside it` : `${count} repos with no remote: run \`${connect}\` inside each`,
915
+ /**
916
+ * The picker's own line (PR 1 ruling 2): ONE count and ONE line for a folder with two or more
917
+ * candidate paths, a hashed name (cut, so it cannot be inverted) and a deleted sibling. Claude Code's
918
+ * and Cursor's only — a Codex row records its real `cwd` and is never ambiguous.
919
+ */
920
+ ambiguous: (count) => count === 1
921
+ ? "1 session in a folder that can't be matched to one repo"
922
+ : `${count} sessions in folders that can't be matched to one repo`,
923
+ /** Scan-only (ruling 5a): a folder, or a Codex `cwd`, that exists but is not inside a git work tree. */
924
+ nonRepo: (count) => count === 1
925
+ ? "1 session in a folder that isn't a git repository"
926
+ : `${count} sessions in folders that aren't git repositories`,
927
+ missingFolder: (count) => count === 1
928
+ ? "1 session in a folder that no longer exists"
929
+ : `${count} sessions in folders that no longer exist`,
930
+ /**
931
+ * ⛔ CLAIMS ONLY WHAT GIT SHOWS (ruling 7). The test is a session's mtime against the OLDEST reflog entry,
932
+ * and `gc.reflogExpire` prunes a long-lived clone's oldest entries, so "older than the reflog" is the fact
933
+ * and "the folder was reused" is a possibility — the same repo deleted and cloned again reads the same.
934
+ */
935
+ pathReused: (count) => count === 1
936
+ ? "1 session older than its clone's reflog (possibly a reused folder)"
937
+ : `${count} sessions older than their clone's reflog (possibly a reused folder)`,
938
+ /** One clone: the path rides the line. More: this heading, then one indented path per clone. */
939
+ noReflogOne: (path) => `1 clone with no reflog: ${path} (backfill skips it)`,
940
+ noReflogMany: (count) => `${count} clones with no reflog (backfill skips them):`,
941
+ /** Rollouts on disk that no index row names (ruling 5c) — counted, never opened. */
942
+ unindexed: (count) => count === 1
943
+ ? "1 Codex session missing from its thread index"
944
+ : `${count} Codex sessions missing from its thread index`,
945
+ /**
946
+ * `Codex sessions couldn't be read (unrecognised index)`, one line per unreadable agent (R3-15). The
947
+ * bracket names a cause the user can act on; a plain read failure (ruling 6) has none to name, so it
948
+ * gets none. The `--json` `reason` is the same three words either way.
949
+ */
950
+ unreadable: (agent, reason) => reason === null
951
+ ? `${agent} sessions couldn't be read`
952
+ : `${agent} sessions couldn't be read (${reason})`,
953
+ next: "Choose what is captured:",
954
+ /** Exit 4. Both screens end on the same command. */
955
+ none: "No coding-agent sessions were found on this machine.",
956
+ /**
957
+ * Exit 4 with `Not listed here:` lines: sessions exist that could not be listed, so `none` would be
958
+ * untrue. `none` prints only when that block is empty, i.e. the scan saw no session at all (VQ, PR 1
959
+ * ruling 3).
960
+ */
961
+ noneListable: "No repository could be listed from the sessions on this machine.",
962
+ noneNext: "Choose owners or repos anyway:",
963
+ };
964
+ /**
965
+ * `vibecommit backfill` — style guide §10.14, design review D14 · 8A, D15 · 9A; capture 0.7.0, PR 5.
966
+ *
967
+ * Every sentence here is a claims-register row. ⛔ Three promises it must never make: that a backfilled session is
968
+ * LINKED to a commit (it arrives without links, and the copy says so), that "every" or "all" earlier sessions are
969
+ * uploaded (only the ones still on this machine and not yet sent), and that a held session will upload by itself (no
970
+ * hook fires for an old session, so the held line points at a re-run).
971
+ *
972
+ * The two folder reasons are different facts (VQ ruling): `folder differs` is a RECORDED folder that is not this clone,
973
+ * `folder not recorded` is a transcript that records none.
974
+ */
975
+ export const BACKFILL = {
976
+ /**
977
+ * Inside `connect` the repos are the ones it just showed, so "these"; run on its own there is no "these", so it names whose
978
+ * they are (VQ, PR 5 review).
979
+ */
980
+ offerHeading: (repos, standalone) => standalone
981
+ ? `Earlier sessions are still on this machine for ${repos} ${repos === 1 ? "repository" : "repositories"} you chose.`
982
+ : `Earlier sessions are still on this machine for ${repos} of these repos.`,
983
+ /** The count on a repo row. "Not yet uploaded", so it is never mistaken for the picker's total on disk. */
984
+ notYetUploaded: (count) => `${count} not yet uploaded`,
985
+ /**
986
+ * Cursor sessions are not offered (Alexander, 2026-10-24 13:54 EST: Cursor's transcripts record no folder, so the
987
+ * folder check cannot run). One muted line, only when a repo you chose has some.
988
+ */
989
+ cursorNotUploaded: (count) => count === 1
990
+ ? "1 Cursor session isn't uploaded: Cursor doesn't record the folder a session started in."
991
+ : `${count} Cursor sessions aren't uploaded: Cursor doesn't record the folder a session started in.`,
992
+ /** Two lines, as §10.14 draws them. */
993
+ /**
994
+ * A session over the 50 MiB cap is left out of the count and named here, so the offer never promises an upload the run would
995
+ * skip on every re-run. (Sub-agent streams are not capped.)
996
+ */
997
+ overSizeNote: (count) => count === 1 ? "1 session over 50 MiB isn't uploaded." : `${count} sessions over 50 MiB aren't uploaded.`,
998
+ /**
999
+ * A clone with no reflog cannot be tested for a reused path (a transcript older than the clone's first reflog entry), and
1000
+ * that test is the only guard against one, so its sessions are left out of the count and named here with the clone.
1001
+ */
1002
+ noReflogNote: (count, path) => count === 1
1003
+ ? `1 session in a clone with no reflog isn't uploaded: ${path}`
1004
+ : `${count} sessions in a clone with no reflog aren't uploaded: ${path}`,
1005
+ uploadNote: ["They upload one at a time, the same way new sessions do, without", "commit links."],
1006
+ question: (sessions) => sessions === 1 ? "Upload this session now? [y/N] " : `Upload these ${sessions} sessions now? [y/N] `,
1007
+ /** A no. A completed consent flow (§13.7), not an error. */
1008
+ declined: (backfill) => `Nothing was uploaded. Run \`${backfill}\` any time.`,
1009
+ nothingToUpload: "Nothing to upload: no earlier session on this machine is waiting for a repository you've chosen.",
1010
+ runHeading: (sessions) => sessions === 1 ? "Uploading 1 session..." : `Uploading ${sessions} sessions, one at a time...`,
1011
+ /** Outcome fragments on a repo line, joined by " · ". */
1012
+ uploaded: (count) => `${count} uploaded`,
1013
+ skipped: (count, reason) => `${count} skipped, ${reason}`,
1014
+ held: (count) => `${count} held, the org must approve capture`,
1015
+ failed: (count, reason) => `${count} failed, ${reason}`,
1016
+ stopped: "stopped",
1017
+ /** Skip reasons. */
1018
+ overSize: "over 50 MiB",
1019
+ folderDiffers: "folder differs",
1020
+ folderNotRecorded: "folder not recorded",
1021
+ /** The transcript could not be opened at all (permissions), which is a different fact from one that records no folder. */
1022
+ couldntBeRead: "couldn't be read",
1023
+ /** The file vanished between the offer and its upload: a skip, not a refusal by the server. */
1024
+ fileGone: "the file is gone",
1025
+ /** `vibecommit off` (or a grant removed) while the run was going: the rest of that repo is not sent. */
1026
+ turnedOff: "turned off while this ran",
1027
+ outsideAgentFolder: "not in the agent's session folder",
1028
+ /** Failure reasons: a `never`. The server's own code is never shown (§13.6: no bare status, no raw error). */
1029
+ refusedByServer: "the server refused it",
1030
+ repositoryForbidden: "another organization owns this repository",
1031
+ noRepoIdentity: "no repository identity",
1032
+ unrecognisedFileName: "the file name isn't a session id",
1033
+ /** The summary. The glyph is `ok` only when nothing was held, stopped or failed. */
1034
+ summary: (uploaded, total, skipped, held, failed) => {
1035
+ const noun = total === 1 ? "session" : "sessions";
1036
+ const tail = [
1037
+ skipped > 0 ? `${skipped} skipped` : null,
1038
+ held > 0 ? `${held} held` : null,
1039
+ failed > 0 ? `${failed} failed` : null,
1040
+ ].filter((part) => part !== null);
1041
+ return `Uploaded ${uploaded} of ${total} ${noun}.${tail.length > 0 ? ` ${tail.join(", ")}.` : ""}`;
1042
+ },
1043
+ /** `owners` is the held repos' owners, already phrased ("acme-corp", "acme-corp or other-org"). */
1044
+ heldBlock: (owners) => `Held sessions wait for ${owners} to approve capture:`,
1045
+ heldThenRun: "Then run:",
1046
+ /**
1047
+ * The stop block: a server `later` (5xx, 429) stops the run, with the resume fix. ⛔ It states only what landed: `uploaded` is
1048
+ * sessions the server took, not sessions looked at, and "not yet uploaded" is what a re-run will OFFER again (skipped and held
1049
+ * sessions among them), not a promise of what it will send.
1050
+ */
1051
+ stopWhat: (uploaded, total) => `Upload stopped: ${uploaded} of ${total} ${total === 1 ? "session" : "sessions"} uploaded.`,
1052
+ stopWhy: (notUploaded) => `The server asked the client to try again later. Nothing already sent is lost, and a re-run offers the ${notUploaded} ${notUploaded === 1 ? "session" : "sessions"} not yet uploaded.`,
1053
+ /** No response at all (a refused connection, a timeout): not the server asking anything. */
1054
+ stopWhyUnreachable: (notUploaded) => `The server could not be reached. Nothing already sent is lost, and a re-run offers the ${notUploaded} ${notUploaded === 1 ? "session" : "sessions"} not yet uploaded.`,
1055
+ stopFixLabel: "Resume:",
1056
+ /** Ctrl+C at the y/N or during the upload. Exit 0: capture is on. */
1057
+ interrupted: (backfill) => `Stopped. A re-run of ${backfill} sends what is left.`,
1058
+ /** Exit 3. */
1059
+ notConnectedWhat: "This machine isn't connected.",
1060
+ notConnectedWhy: "`backfill` uploads with this machine's credential, and there isn't one. Nothing was read.",
1061
+ notConnectedFixLabel: "Connect first:",
1062
+ /** `grants.json` is present and unreadable: every repo with a remote is denied, so there is nothing honest to offer. */
1063
+ grantsUnreadableWhat: "Backfill can't run: ~/.vibecommit/grants.json can't be read.",
1064
+ grantsUnreadableWhy: "Capture is stopped on this machine for every repository that has a remote until the file is readable. Nothing was read and nothing was uploaded.",
1065
+ grantsUnreadableFixLabel: "Fix the file or move it aside, then run:",
1066
+ /** Exit 2. A prompt that proceeds when nobody can answer it is not a prompt. */
1067
+ nonInteractiveWhat: "`backfill` needs an interactive terminal.",
1068
+ nonInteractiveWhy: "Standard input is not a terminal, so the question before uploading cannot be answered. Nothing was read and nothing was uploaded.",
1069
+ nonInteractiveFix: "Run this in a terminal:",
1070
+ /** The credential was rejected mid-run (a 401): the same block `finalize` shows, in this verb's words. */
1071
+ credentialStoppedWhat: "Upload stopped: this machine's credential is no longer accepted.",
1072
+ credentialStoppedWhy: "Nothing already sent is lost, and a re-run offers the sessions not yet uploaded.",
1073
+ credentialStoppedFixLabel: "Renew it, then run backfill again:",
1074
+ usageWhat: "That is not something `vibecommit backfill` understands.",
1075
+ usageWhy: "Nothing was read and nothing was uploaded. `backfill` takes no arguments.",
1076
+ };
1077
+ /**
1078
+ * `vibecommit connect` from any folder — the picker, the grant block, the hooks step and the endings (0.7.0, PR 3).
1079
+ * Style guide §10.12, copied from the merged guide and the design doc; the design doc wins where they differ.
1080
+ */
1081
+ export const PICKER = {
1082
+ onNow: "on now",
1083
+ turnedOff: "turned off",
1084
+ /** Why an `on now` row is on (D5 · 1A), so a 0.6.x upgrader sees what is already captured. */
1085
+ reasonOwner: "owner",
1086
+ reasonRepo: "repo",
1087
+ reasonPerClone: "per clone",
1088
+ /** The owner row ALWAYS states how many repos `aN` covers (D9), so a hidden repo is never covered unseen. */
1089
+ ownerAll: (count) => `all ${count} ${count === 1 ? "repo" : "repos"} here, including repos you clone later`,
1090
+ ownerOnReposHere: (count) => `${count} ${count === 1 ? "repo" : "repos"} here`,
1091
+ turnOnHint: "choose it to turn it back on",
1092
+ /** Where the working folder is a repo, the row is marked so a pre-tick (or a `$HOME` dotfiles repo) is legible. */
1093
+ thisRepo: "this repo",
1094
+ thisFolder: "this folder",
1095
+ moreTyped: (count, owner) => `${count} more in ${owner}/ · type more to list them`,
1096
+ moreChecklist: (count, owner) => `${count} more in ${owner}/`,
1097
+ /** The typed prompt (D8). */
1098
+ choose: (hasOwners) => hasOwners
1099
+ ? "Choose a1, numbers (1 2), owner/ or owner/repo."
1100
+ : "Choose numbers (1 2), owner/ or owner/repo.",
1101
+ removeHint: "Remove one with -N, -owner/ or -owner/repo.",
1102
+ enterNothing: "Press Enter to change nothing.",
1103
+ /** D34 in the typed prompt: the working folder's repo is pre-selected, so Enter leads to the (default No) y/N. */
1104
+ enterThisRepo: (slug) => `Press Enter to choose ${slug}, this repo.`,
1105
+ promptMark: "> ",
1106
+ /** `tokens` is the already-joined list (`9 (no row 9), acme, corp`): the corpus renders every template with a string. */
1107
+ notUnderstood: (tokens) => `Not understood: ${tokens}. Nothing was chosen.`,
1108
+ noRow: (n) => `${n} (no row ${n})`,
1109
+ alreadyOn: (token, remove) => `${token} (already on; ${remove} stops it)`,
1110
+ notOn: (token) => `${token} (not on)`,
1111
+ /** The empty screen (D12 · 6A): a new laptop, no agent session on disk at all. */
1112
+ emptyNone: "No coding-agent sessions were found on this machine.",
1113
+ emptyExpected: "That's expected on a new machine. Type the owners or repos to capture; every clone of them is captured once Claude Code, Codex or Cursor runs in it.",
1114
+ emptyType: "Type owner/ or owner/repo, for example alexvibecommit/.",
1115
+ /** Sessions exist but none could be listed (the `Not listed here:` block is not empty): scan's own wording. */
1116
+ emptyListable: "Type owner/ or owner/repo, for example alexvibecommit/, to capture a repository that is not listed.",
1117
+ /** The checklist (D28). */
1118
+ hint: "↑↓ move · space choose · enter done · t typed prompt · ctrl+c cancel",
1119
+ hintTyped: (connectTyped) => `For a screen reader, run: ${connectTyped}`,
1120
+ /** The enter-with-nothing lines (D5). */
1121
+ nothingGranted: (connect) => `Nothing granted. Run \`${connect}\` again any time.`,
1122
+ noChanges: (summary) => `No changes. Capture stays on for ${summary}.`,
1123
+ cancelled: "Cancelled. Nothing was changed.",
1124
+ /** The grant block (D7 · 3A). */
1125
+ chose: "You chose:",
1126
+ labelOwners: "owners",
1127
+ labelRepos: "repos",
1128
+ labelTurnOn: "turn on",
1129
+ labelStop: "stop",
1130
+ ownerHere: (count) => `(${count} here, and later clones)`,
1131
+ noCloneHere: "(no clone here yet)",
1132
+ questionGrant: "Upload transcripts from these repositories?",
1133
+ questionStop: "Stop capturing these repositories?",
1134
+ stopped: "Stopped capturing the repositories above.",
1135
+ /** "1 owner and 2 repos" — the on-now summary of Enter-with-grants, `auth`'s one line and the status heading. */
1136
+ summary: (owners, repos) => {
1137
+ const o = `${owners} ${owners === 1 ? "owner" : "owners"}`;
1138
+ const r = `${repos} ${repos === 1 ? "repo" : "repos"}`;
1139
+ if (owners > 0 && repos > 0)
1140
+ return `${o} and ${r}`;
1141
+ return owners > 0 ? o : r;
1142
+ },
1143
+ authStays: (summary) => `Capture stays on for ${summary} on this machine.`,
1144
+ /** The hooks step (D13 · 7A). */
1145
+ hooksBeat: (clones) => `Adding commit hooks to ${clones} ${clones === 1 ? "clone" : "clones"}...`,
1146
+ hooksDone: "done",
1147
+ hooksCount: (added, chained, notAdded) => {
1148
+ const head = `${added} added${chained > 0 ? `, ${chained} of them after your own hook` : ""}`;
1149
+ return notAdded > 0 ? `${head} · ${notAdded} not added` : head;
1150
+ },
1151
+ hooksNotAdded: (count) => `Commit hooks were not added to ${count} ${count === 1 ? "clone" : "clones"}:`,
1152
+ hooksPathReason: "core.hooksPath is set",
1153
+ /** A hook write that FAILED is a real exception with its own reason (VQ, item 9): neither dropped nor given `hooksPath`'s fix. */
1154
+ hooksFailedReason: "the hook could not be written",
1155
+ hooksStillCaptured: "Sessions there are still captured. To record commits too, either:",
1156
+ hooksStillCapturedOther: "Sessions there are still captured.",
1157
+ hooksFailedFixLabel: "Where the hook could not be written, try again:",
1158
+ /** "Capture is on" (D18 · 12A, D19 · 13A). */
1159
+ onFor: (summary) => `Capture is on for ${summary} on this machine.`,
1160
+ onProof: "The next Claude Code, Codex or Cursor session in any of them is captured. In Claude Code, its first reply then shows \"VibeCommit captured this session.\"",
1161
+ stopOne: "To stop one repo, run inside it:",
1162
+ /**
1163
+ * Alexander's "caveat for other owners" (2026-10-24; amended 17:20), on the "Capture is on" block. It prints for the owners the
1164
+ * test capture did NOT check: from `~` that is every granted owner (there is no session to probe), and in a repo it is every
1165
+ * granted owner except the one whose delta the server answered with a 2xx. For an owner that is not the signed-in person's own
1166
+ * account the client cannot know whether an org is yet to approve capture, so it says one may be. Never printed when every
1167
+ * such owner is the user's own account.
1168
+ */
1169
+ ownerApproval: (status) => `An organization can require an owner's approval before its repos are captured; once a session in one of its repos has tried to upload, ${status} shows any that are waiting.`,
1170
+ onNewLaptop: (names) => `Capture is on for ${names} on this machine.`,
1171
+ onNewLaptopProof: "No clone of it is here yet. Clone one and open a coding agent in it; that session is captured.",
1172
+ };
700
1173
  /** `vibecommit off`. */
701
1174
  export const OFF = {
702
1175
  done: "Capture is off for this repository.",
1176
+ /**
1177
+ * 0.7.0, a claims-register row. A repo with a remote is turned off BY SLUG, which reaches every clone of it on this
1178
+ * machine. Slice 2 extends the sentence to the other machines once they sync; until then it says only what is true.
1179
+ */
1180
+ doneSlug: (slug) => `Turned off ${slug} for every clone on this machine.`,
1181
+ /** D31: bare `off` outside a repository. The picker is where an owner or another repo is unticked. */
1182
+ outsideRepoWhat: "`off` turns off the repository you're in.",
1183
+ outsideRepoFixLabel: "To stop capturing an owner or another repo, untick it in the picker:",
1184
+ /** `grants.json` is unreadable, so the exclusion could not be recorded. The clone's own consent was still withdrawn. */
1185
+ grantsUnreadableWhat: "The turn-off could not be recorded: ~/.vibecommit/grants.json can't be read.",
1186
+ grantsUnreadableWhy: "Capture is already stopped for this repository while the file can't be read, and this clone's own consent was withdrawn.",
1187
+ grantsUnreadableFixLabel: "Fix the file or move it aside, then run:",
703
1188
  alreadyOff: "Capture was already off for this repository.",
704
1189
  note: "Sessions already recorded are unaffected.",
705
1190
  // `TODOS[184]`. `off` used to ignore `argv` entirely and always run — a stray
@@ -989,7 +1474,10 @@ export const WHY = {
989
1474
  */
990
1475
  skewWhat: "The record service sent an answer this version is too old to show.",
991
1476
  skewWhy: "The service described this commit in a way this build does not recognise. The record itself is fine — this copy of the client cannot render it, so it showed nothing rather than guessing at part of it.",
1477
+ /** The PLUGIN channel's remedy (`COMMANDS.pluginInstall`). */
992
1478
  skewFix: "Reinstall the VibeCommit plugin to pick up the current client, then run this again:",
1479
+ /** The npm channel's remedy (`COMMANDS.npmUpdate`): an npm install has no plugin to reinstall. */
1480
+ skewFixNpm: "Update VibeCommit to pick up the current client, then run this again:",
993
1481
  /** The bearer is dead after one refresh and one retry. Exit 3, not 1. */
994
1482
  signedOutWhat: "This machine is not signed in to VibeCommit.",
995
1483
  signedOutWhy: "Reading a record needs a signed-in user, and no usable sign-in was found on this machine.",
@@ -1276,20 +1764,44 @@ export const ABSENCE = {
1276
1764
  * know were bound: `closed` says the last turn is recorded, not that any commit matched it.
1277
1765
  */
1278
1766
  export const FINALIZE = {
1279
- needTarget: "Say which session to close: --session <id>, or --latest.",
1767
+ // PR 7 (Q7): where the remedy is a command it sits in the §13.6 fix block, not inside the `what` line. The `what` lines
1768
+ // below lost exactly the clause that moved; the fix commands are the ones the old sentence named.
1769
+ needTarget: "Say which session to close.",
1770
+ needTargetFix: "Name one, or take the latest:",
1280
1771
  badSessionId: "That is not a session id (letters, digits, dot, dash and underscore only).",
1281
- noRecentSession: "No session in this repository has delivered anything in the last 24 hours. Use --session <id>.",
1282
- severalRecent: "More than one session in this repository is recent. Name one with --session <id>:",
1772
+ noRecentSession: "No session in this repository has delivered anything in the last 24 hours.",
1773
+ noRecentSessionFix: "Name a session:",
1774
+ severalRecent: "More than one session in this repository is recent.",
1775
+ severalRecentFix: "Name one:",
1776
+ /** `finalize --session <id>` for a session that is known, e.g. the one whose delivery just failed. */
1777
+ commandSession: (id) => `vibecommit finalize --session ${id}`,
1778
+ /** The same with the placeholder, for a fix line written before a session is known. */
1779
+ commandSessionAny: "vibecommit finalize --session <id>",
1780
+ /** The command without its id, for a sentence that names the id itself (`status`'s held-idle line). */
1781
+ commandSessionBase: "vibecommit finalize --session",
1782
+ commandSessionForce: (id) => `vibecommit finalize --session ${id} --force`,
1283
1783
  recentRow: (id, minutesAgo) => `${id} (${minutesAgo} min ago)`,
1284
1784
  unknownSession: (id) => `This repository has no captured session ${id}.`,
1285
1785
  transcriptMissing: (id) => `Could not find the transcript for session ${id}.`,
1286
- stillWriting: "That session's transcript changed a moment ago. Wait for the session to finish, or pass --force if it is over.",
1786
+ stillWriting: "That session's transcript changed a moment ago.",
1787
+ stillWritingFix: "Wait for the session to finish, or if it is over, run:",
1287
1788
  closed: (id) => `Closed session ${id}: its last turn is recorded, so commits made in it can now be recorded.`,
1288
1789
  alreadyClosed: (id) => `Session ${id} had nothing left open. Nothing changed.`,
1289
1790
  notAcknowledged: "The server accepted the data but did not confirm that it closed the session; it may predate this command. The last turn may still be open.",
1290
- refused: "The server declined to close the session on this attempt. Run the command again.",
1291
- notDelivered: "Could not deliver the session. Nothing was closed. Run the command again.",
1292
- credentialStopped: "VibeCommit stopped sending for this credential. Run `vibecommit auth` to replace it.",
1791
+ refused: "The server declined to close the session on this attempt.",
1792
+ retryFix: "Run it again:",
1793
+ notDelivered: "Could not deliver the session. Nothing was closed.",
1794
+ credentialStopped: "VibeCommit stopped sending for this credential.",
1795
+ credentialStoppedFix: "Replace it:",
1796
+ /**
1797
+ * PR 7 (Q2, VQ's wording): the ingest address in the environment (`VIBECOMMIT_INGEST_URL`) is one this client refuses to
1798
+ * send to, so retrying cannot help. Shared by `finalize` and `backfill`, so the `why` says only "Nothing was sent": it must
1799
+ * not claim a session was or was not closed. The VALUE is never echoed (a URL can carry a secret).
1800
+ */
1801
+ ingestUrlWhat: "The address VibeCommit sends sessions to can't be used.",
1802
+ ingestUrlWhy: "VIBECOMMIT_INGEST_URL must be an https address, or an http address on this machine, and it isn't. Nothing was sent.",
1803
+ ingestUrlFix: "Correct it, or remove it to use the default address:",
1804
+ ingestUrlCommand: "unset VIBECOMMIT_INGEST_URL",
1293
1805
  /**
1294
1806
  * `TODOS[177]` follow-up (VL, 2026-10-08, VG's silent-loss finding) — ONE stderr line for the whole run when
1295
1807
  * `finalize` sets any inferred-squash pairs aside (the main delivery, the drain loop, or both, summed). Printed
@@ -1317,8 +1829,8 @@ export const REJECTED = {
1317
1829
  * command across lines; `unconfirmed` is their join, the sentence the plan wrote.
1318
1830
  */
1319
1831
  unconfirmedWhat: "Couldn't confirm this machine's credential with the server.",
1320
- unconfirmedFix: "Run `vibecommit status` later.",
1321
- unconfirmed: "Couldn't confirm this machine's credential with the server. Run `vibecommit status` later.",
1832
+ unconfirmedFix: (status) => `Run \`${status}\` later.`,
1833
+ unconfirmed: (status) => `Couldn't confirm this machine's credential with the server. Run \`${status}\` later.`,
1322
1834
  };
1323
1835
  /** `TODOS[184]`-style one-sentence facts the retire step prints; every one is true whatever the server did. */
1324
1836
  export const RETIRE = {
@@ -1350,6 +1862,12 @@ export const REPLACE = {
1350
1862
  questionUnconfirmed: "Upload this machine's transcripts to the workspace you signed in to?",
1351
1863
  replacesValue: (org) => `credential for ${org}`,
1352
1864
  question: (org) => `Upload this machine's transcripts to ${org}?`,
1865
+ /**
1866
+ * 0.7.0, D7 · 3A: on the PICKER path the question names the repos, not the machine, and is the second of at most three
1867
+ * questions in a run. The workspace is cut to 28 columns (not `question`'s 30) so the line stays within 80 with `[y/N]`.
1868
+ */
1869
+ questionRepos: (org) => `Upload transcripts from the repos above to ${org}?`,
1870
+ questionReposUnconfirmed: "Upload transcripts from the repos above to your signed-in workspace?",
1353
1871
  declined: "Nothing was saved. The approval is discarded.",
1354
1872
  /** Ctrl+C at the question (the prompt was aborted): the same outcome as N, said as what it was. */
1355
1873
  cancelled: "Cancelled. Nothing was saved.",
@@ -1444,13 +1962,19 @@ export const UPDATE = {
1444
1962
  plugin: (latest) => `capture ${latest} is available: update the VibeCommit plugin in Claude Code.`,
1445
1963
  };
1446
1964
  export const ERRORS = {
1447
- notConnected: "This repository is not connected. Run `vibecommit connect`.",
1965
+ notConnected: "This repository is not connected.",
1966
+ /** PR 7 (Q7): the remedy was a sentence inside the `what` line; it is a command now, so it moves to the fix block. */
1967
+ notConnectedFix: "Connect it:",
1448
1968
  networkLater: "Could not reach VibeCommit. This session will be recorded on the next turn.",
1449
1969
  // `11A`. The `what` of the §13.6 rejected-credential block (`REJECTED`), not a second sentence: the
1450
1970
  // old line sent users to `connect`, which printed "Connected" over the same dead credential (G1).
1451
1971
  authFatal: "This machine's credential is no longer accepted.",
1452
1972
  payloadNever: "This session was too large to record and has been skipped.",
1453
1973
  notAGitRepo: "Not a git repository.",
1974
+ /** PR 7 (VQ): names the folder, tilde-shortened and cut from the LEFT by the caller (`truncatePath`, §13.4). */
1975
+ notAGitRepoWhy: (folder) => `${folder} is not inside a git work tree.`,
1976
+ /** A fix with no command to give: the remedy is where to run it, so the block has a label and no commands. */
1977
+ notAGitRepoFix: "Run it again inside a git repository.",
1454
1978
  };
1455
1979
  /**
1456
1980
  * The interactive top-level catch (CR-022).
@@ -1470,7 +1994,8 @@ export const INTERNAL = {
1470
1994
  unexpectedWhat: "VibeCommit stopped on an unexpected error.",
1471
1995
  unexpectedWhy: "The command did not finish.",
1472
1996
  unexpectedFix: "Run it again with the error detail shown:",
1473
- unexpectedDebugCommand: "VIBECOMMIT_DEBUG=1 vibecommit <command>",
1997
+ // The command is this install's own (PR 7): `node <path> <command>` on a plugin-only machine, which has no `vibecommit`.
1998
+ unexpectedDebugCommand: (command) => `VIBECOMMIT_DEBUG=1 ${command} <command>`,
1474
1999
  };
1475
2000
  /**
1476
2001
  * Credential-loading failures (CR-016).
@@ -1498,6 +2023,10 @@ export const CREDENTIAL = {
1498
2023
  insecureFileModeLabel: "mode",
1499
2024
  insecureFileWhy: "It was not read. A credential file must be readable only by its owner.",
1500
2025
  insecureFileFix: "Restore the mode, then reconnect:",
2026
+ /** The FOLDER (`~/.vibecommit`) is open to other accounts, not the file: same code, its own what, why and `chmod`. */
2027
+ insecureDirWhat: "The folder that holds the credential file is open to other accounts on this machine.",
2028
+ insecureDirWhyLabel: "folder",
2029
+ insecureDirWhy: "The credential was not read. This folder must be accessible only to its owner.",
1501
2030
  unreadableWhat: "The credential file could not be read.",
1502
2031
  unreadableWhy: "It exists but does not contain a credential in the expected form.",
1503
2032
  /** `CR-216/U4`. Was "Replace it by reconnecting:" — see `wrongClassFix` above. */
@@ -1531,15 +2060,26 @@ export const AUTH = {
1531
2060
  nextCommand: "vibecommit connect",
1532
2061
  emptyWhat: "No credential was given.",
1533
2062
  emptyWhy: "Nothing was read, so nothing was written.",
1534
- emptyFix: "Mint one, then run:",
2063
+ /**
2064
+ * PR 6 item 1. Was "Mint one, then run:" over the URL alone, so the label promised a command and printed a link, and the
2065
+ * non-interactive path was named nowhere. The URL comes FIRST (inside the label, where it cannot be wrapped away from
2066
+ * its sentence) and the commands LAST, so the fix stays last (§13.6). A function because `URLS` is declared below.
2067
+ */
2068
+ mintFix: (url) => `Mint one at ${url}, then run either:`,
2069
+ /**
2070
+ * The two ways to hand it over: piped (a script or an agent), or typed at a terminal. `--stdin` never continues into `connect`
2071
+ * (D32 guard 1). `auth` is `authCommandFor(env, selfPath)`: a plugin-only machine has no `vibecommit` on PATH, so the line names
2072
+ * `node <path> auth` there (README, "Keeping a machine connected"), and handing it `vibecommit auth` would be a new wrong output.
2073
+ */
2074
+ mintCommands: (auth) => [`printf %s "$TOKEN" | ${auth} --stdin`, auth],
1535
2075
  wrongClassWhat: "That is not a VibeCommit ingest credential.",
1536
2076
  wrongClassWhy: `An ingest credential begins ${INGEST_TOKEN_PREFIX}. Nothing was written and nothing was sent.`,
1537
- wrongClassFix: "Mint one, then run:",
1538
2077
  argvWhat: "A credential must not be passed as an argument.",
1539
2078
  argvWhy: "Arguments are written to your shell history and are readable by other processes on this machine while the command runs. Treat the one you just typed as exposed and revoke it.",
1540
2079
  argvFix: "Read it from the terminal, or from a pipe:",
1541
2080
  argvFixPrompt: "vibecommit auth",
1542
- argvFixStdin: 'printf %s "$TOKEN" | vibecommit auth --stdin',
2081
+ /** A pipe, so `channelCommand` cannot rewrite it: it takes the channel's `auth` (`authCommandFor`) instead. */
2082
+ argvFixStdin: (auth) => `printf %s "$TOKEN" | ${auth} --stdin`,
1543
2083
  // --- `CR-226`: the browser mint, and the paste it falls back to. ---
1544
2084
  /**
1545
2085
  * Printed before the request, naming the label the credential will carry in
@@ -1630,6 +2170,18 @@ export const SYSTEM_MESSAGE = {
1630
2170
  * users and its hash chain detects post-write tampering").
1631
2171
  */
1632
2172
  captureConfirmed: (dashboardUrl) => `VibeCommit captured this session. ${dashboardUrl}`,
2173
+ /**
2174
+ * States 6-9 — style guide §10.1 (0.7.0, design review D16 · 10A), all emitted by the `SessionStart` hook. One
2175
+ * plain-text line each, ≤120 characters, no SGR. The wording is the design doc's and the merged style guide's,
2176
+ * byte for byte; `test/session-start.test.ts` pins the length of every one.
2177
+ *
2178
+ * 6 — hooks added, once per clone. 7 — `core.hooksPath` is set, once per clone. 8 — the clone has its own
2179
+ * commit hook, once per clone. 9 — `grants.json` is unreadable, once per (machine, day).
2180
+ */
2181
+ hooksAdded: "VibeCommit added its post-commit and post-rewrite hooks to this clone, so commits can link to sessions.",
2182
+ hooksPathSet: "VibeCommit captures sessions here, not commits: core.hooksPath is set. Run `vibecommit status` here for the fix.",
2183
+ ownCommitHook: "VibeCommit didn't add its commit hook here: this clone has its own. Run `vibecommit connect` here to chain it.",
2184
+ grantsUnreadable: "VibeCommit capture stopped on this machine: ~/.vibecommit/grants.json can't be read. Run `vibecommit status`.",
1633
2185
  };
1634
2186
  /**
1635
2187
  * The out-of-tree redaction marker — `CR-024d`, D57 §OV8d.
@@ -1660,6 +2212,10 @@ export const REDACTION = {
1660
2212
  /** Commands the copy points at. One definition, so a rename cannot half-land. */
1661
2213
  export const COMMANDS = {
1662
2214
  connect: "vibecommit connect",
2215
+ /** `D33 · 27A`. */
2216
+ scan: "vibecommit scan",
2217
+ /** `D14`/`D15`. */
2218
+ backfill: "vibecommit backfill",
1663
2219
  /**
1664
2220
  * `CR-216/U2`. The only verb a credential FAULT's fix line may name — it can
1665
2221
  * replace the credential from any state. `CR-226` lets `connect` mint one
@@ -1680,6 +2236,10 @@ export const COMMANDS = {
1680
2236
  /** The ref form, for the detached-HEAD refusal. */
1681
2237
  reportRef: "vibecommit report --since 2026-07-08 --ref main",
1682
2238
  off: "vibecommit off",
2239
+ /** The picker's screen-reader flag: a FLAG on `connect`, like `signIn`. */
2240
+ connectTyped: "vibecommit connect --typed",
2241
+ /** The git hook's own verb. `hooksPathFixManual` tells a user to add it to a hook they keep. */
2242
+ postCommit: "vibecommit post-commit",
1683
2243
  /**
1684
2244
  * `CR-086`. The ARGUMENT SHAPE, shown once so the usage error and the retry
1685
2245
  * fix line cannot drift apart. There is no argument parser in this package —
@@ -1690,6 +2250,8 @@ export const COMMANDS = {
1690
2250
  */
1691
2251
  why: "vibecommit why src/auth/session.ts:47",
1692
2252
  chmodCredentials: "chmod 600 ~/.vibecommit/credentials.json",
2253
+ /** The folder's remedy, for `CREDENTIAL_FILE_INSECURE` when the folder is the one with the wrong mode. */
2254
+ chmodDirectory: "chmod 700 ~/.vibecommit",
1693
2255
  /**
1694
2256
  * ⛔ THE INSTALL COMMAND, TRANSCRIBED FROM `claude-plugin/README.md:12` — and
1695
2257
  * it is deliberately NOT an invented update verb (`U1`, 2026-08-30).
@@ -1707,6 +2269,8 @@ export const COMMANDS = {
1707
2269
  * together — the wording and the command are one unit.
1708
2270
  */
1709
2271
  pluginInstall: "/plugin install vibecommit@vibecommit-capture",
2272
+ /** The npm channel's way to a newer client: the package's own documented install, at `latest`. */
2273
+ npmUpdate: "npm install -g @vibe-commit/capture@latest",
1710
2274
  };
1711
2275
  /**
1712
2276
  * Every URL the CLI prints. Here rather than inline for the same reason as the
@@ -1730,6 +2294,8 @@ export const URLS = {
1730
2294
  * deepening this constant is that task's, not a later guess.
1731
2295
  */
1732
2296
  commits: "https://vibecommit.ai/app/commits",
2297
+ /** `/app/conversations`: where an uploaded backfill lands (D19). `/app` (`dashboard`) is the ending after a no. */
2298
+ conversations: "https://vibecommit.ai/app/conversations",
1733
2299
  /**
1734
2300
  * One recorded session — the ending's link when the ingest 2xx carried a `conversation_id` (`DX D8`).
1735
2301
  * Verified by file: `vibecommit-web` `app/app/conversations/[id]/page.tsx` exists on `main`, and an