@vibe-commit/capture 0.6.1 → 0.8.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 (172) hide show
  1. package/README.md +191 -9
  2. package/dist/account_not_here.d.ts +15 -0
  3. package/dist/account_not_here.js +25 -0
  4. package/dist/account_not_here.js.map +1 -0
  5. package/dist/agent_hooks.d.ts +8 -0
  6. package/dist/agent_hooks.js +31 -1
  7. package/dist/agent_hooks.js.map +1 -1
  8. package/dist/agents/claude_code.d.ts +34 -0
  9. package/dist/agents/claude_code.js +141 -8
  10. package/dist/agents/claude_code.js.map +1 -1
  11. package/dist/agents/codex_cli.d.ts +13 -0
  12. package/dist/agents/codex_cli.js +241 -2
  13. package/dist/agents/codex_cli.js.map +1 -1
  14. package/dist/agents/cursor.d.ts +34 -0
  15. package/dist/agents/cursor.js +115 -1
  16. package/dist/agents/cursor.js.map +1 -1
  17. package/dist/agents/session_listing.d.ts +29 -0
  18. package/dist/agents/session_listing.js +49 -0
  19. package/dist/agents/session_listing.js.map +1 -0
  20. package/dist/agents/types.d.ts +118 -8
  21. package/dist/agents/types.js +17 -3
  22. package/dist/agents/types.js.map +1 -1
  23. package/dist/commands/approval_pending.d.ts +9 -0
  24. package/dist/commands/approval_pending.js +23 -0
  25. package/dist/commands/approval_pending.js.map +1 -0
  26. package/dist/commands/auth.d.ts +7 -0
  27. package/dist/commands/auth.js +52 -11
  28. package/dist/commands/auth.js.map +1 -1
  29. package/dist/commands/backfill.d.ts +160 -0
  30. package/dist/commands/backfill.js +748 -0
  31. package/dist/commands/backfill.js.map +1 -0
  32. package/dist/commands/browser_credential.d.ts +6 -2
  33. package/dist/commands/browser_credential.js +34 -14
  34. package/dist/commands/browser_credential.js.map +1 -1
  35. package/dist/commands/connect.d.ts +146 -0
  36. package/dist/commands/connect.js +216 -117
  37. package/dist/commands/connect.js.map +1 -1
  38. package/dist/commands/connect_account.d.ts +135 -0
  39. package/dist/commands/connect_account.js +321 -0
  40. package/dist/commands/connect_account.js.map +1 -0
  41. package/dist/commands/connect_picker.d.ts +27 -0
  42. package/dist/commands/connect_picker.js +549 -0
  43. package/dist/commands/connect_picker.js.map +1 -0
  44. package/dist/commands/context.d.ts +16 -0
  45. package/dist/commands/context.js +27 -0
  46. package/dist/commands/context.js.map +1 -1
  47. package/dist/commands/credential_problem.d.ts +15 -4
  48. package/dist/commands/credential_problem.js +29 -22
  49. package/dist/commands/credential_problem.js.map +1 -1
  50. package/dist/commands/device_credential.d.ts +5 -1
  51. package/dist/commands/device_credential.js +17 -14
  52. package/dist/commands/device_credential.js.map +1 -1
  53. package/dist/commands/finalize.d.ts +95 -0
  54. package/dist/commands/finalize.js +249 -74
  55. package/dist/commands/finalize.js.map +1 -1
  56. package/dist/commands/finish_mint.d.ts +30 -6
  57. package/dist/commands/finish_mint.js +26 -6
  58. package/dist/commands/finish_mint.js.map +1 -1
  59. package/dist/commands/report.js +38 -30
  60. package/dist/commands/report.js.map +1 -1
  61. package/dist/commands/scan.d.ts +162 -0
  62. package/dist/commands/scan.js +507 -0
  63. package/dist/commands/scan.js.map +1 -0
  64. package/dist/commands/status.d.ts +19 -1
  65. package/dist/commands/status.js +232 -26
  66. package/dist/commands/status.js.map +1 -1
  67. package/dist/commands/status_hooks.d.ts +14 -0
  68. package/dist/commands/status_hooks.js +81 -0
  69. package/dist/commands/status_hooks.js.map +1 -0
  70. package/dist/commands/status_machine.d.ts +27 -0
  71. package/dist/commands/status_machine.js +210 -0
  72. package/dist/commands/status_machine.js.map +1 -0
  73. package/dist/commands/sync_reason.d.ts +17 -0
  74. package/dist/commands/sync_reason.js +25 -0
  75. package/dist/commands/sync_reason.js.map +1 -0
  76. package/dist/commands/why.js +44 -31
  77. package/dist/commands/why.js.map +1 -1
  78. package/dist/consent.d.ts +217 -0
  79. package/dist/consent.js +400 -1
  80. package/dist/consent.js.map +1 -1
  81. package/dist/copy/error_codes.d.ts +222 -0
  82. package/dist/copy/error_codes.js +105 -0
  83. package/dist/copy/error_codes.js.map +1 -0
  84. package/dist/copy/index.d.ts +2 -2
  85. package/dist/copy/index.js +2 -2
  86. package/dist/copy/index.js.map +1 -1
  87. package/dist/copy/strings.d.ts +617 -30
  88. package/dist/copy/strings.js +741 -36
  89. package/dist/copy/strings.js.map +1 -1
  90. package/dist/copy/time.d.ts +11 -0
  91. package/dist/copy/time.js +18 -0
  92. package/dist/copy/time.js.map +1 -1
  93. package/dist/credential.d.ts +5 -1
  94. package/dist/credential.js +2 -2
  95. package/dist/credential.js.map +1 -1
  96. package/dist/git.d.ts +43 -0
  97. package/dist/git.js +95 -0
  98. package/dist/git.js.map +1 -1
  99. package/dist/grants_sync.d.ts +165 -0
  100. package/dist/grants_sync.js +289 -0
  101. package/dist/grants_sync.js.map +1 -0
  102. package/dist/grants_verb.d.ts +70 -0
  103. package/dist/grants_verb.js +121 -0
  104. package/dist/grants_verb.js.map +1 -0
  105. package/dist/hooks/entry.d.ts +18 -1
  106. package/dist/hooks/entry.js +117 -14
  107. package/dist/hooks/entry.js.map +1 -1
  108. package/dist/hooks/post_commit.js +3 -3
  109. package/dist/hooks/post_commit.js.map +1 -1
  110. package/dist/hooks/post_rewrite.js +6 -4
  111. package/dist/hooks/post_rewrite.js.map +1 -1
  112. package/dist/hooks/session_grants.d.ts +59 -0
  113. package/dist/hooks/session_grants.js +91 -0
  114. package/dist/hooks/session_grants.js.map +1 -0
  115. package/dist/hooks/session_start.d.ts +114 -0
  116. package/dist/hooks/session_start.js +243 -0
  117. package/dist/hooks/session_start.js.map +1 -0
  118. package/dist/index.d.ts +1 -1
  119. package/dist/index.js +76 -11
  120. package/dist/index.js.map +1 -1
  121. package/dist/install.d.ts +57 -0
  122. package/dist/install.js +54 -20
  123. package/dist/install.js.map +1 -1
  124. package/dist/json.d.ts +32 -9
  125. package/dist/json.js +35 -8
  126. package/dist/json.js.map +1 -1
  127. package/dist/machine_state.d.ts +52 -0
  128. package/dist/machine_state.js +63 -0
  129. package/dist/machine_state.js.map +1 -0
  130. package/dist/oauth/retire.d.ts +10 -0
  131. package/dist/oauth/retire.js +13 -0
  132. package/dist/oauth/retire.js.map +1 -1
  133. package/dist/oauth/session.d.ts +1 -0
  134. package/dist/org_pending.d.ts +52 -0
  135. package/dist/org_pending.js +109 -0
  136. package/dist/org_pending.js.map +1 -0
  137. package/dist/paths.d.ts +70 -0
  138. package/dist/paths.js +90 -0
  139. package/dist/paths.js.map +1 -1
  140. package/dist/pending_revoke.d.ts +117 -0
  141. package/dist/pending_revoke.js +233 -0
  142. package/dist/pending_revoke.js.map +1 -0
  143. package/dist/pending_revoke_file.d.ts +55 -0
  144. package/dist/pending_revoke_file.js +74 -0
  145. package/dist/pending_revoke_file.js.map +1 -0
  146. package/dist/picker.d.ts +208 -0
  147. package/dist/picker.js +526 -0
  148. package/dist/picker.js.map +1 -0
  149. package/dist/picker_checklist.d.ts +141 -0
  150. package/dist/picker_checklist.js +411 -0
  151. package/dist/picker_checklist.js.map +1 -0
  152. package/dist/policy.d.ts +70 -1
  153. package/dist/policy.js +68 -2
  154. package/dist/policy.js.map +1 -1
  155. package/dist/post.d.ts +91 -18
  156. package/dist/post.js +259 -27
  157. package/dist/post.js.map +1 -1
  158. package/dist/state.d.ts +6 -0
  159. package/dist/state.js +38 -1
  160. package/dist/state.js.map +1 -1
  161. package/dist/system_message.d.ts +32 -2
  162. package/dist/system_message.js +47 -5
  163. package/dist/system_message.js.map +1 -1
  164. package/dist/term.d.ts +28 -2
  165. package/dist/term.js +23 -2
  166. package/dist/term.js.map +1 -1
  167. package/dist/verb_help.d.ts +13 -0
  168. package/dist/verb_help.js +40 -0
  169. package/dist/verb_help.js.map +1 -0
  170. package/dist/version.d.ts +1 -1
  171. package/dist/version.js +1 -1
  172. 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 you in after you say yes, unless you choose g, which signs in first to read your account's grants.",
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,52 @@ 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
+ /**
242
+ * Slice 2 (S4), brief §3.3: inside a repo that is NOT on, the account may still grant it. The account never admits a repo on
243
+ * its own (R3-22): naming it here is `connect`'s picker, and the action row already offers `connect`.
244
+ */
245
+ basisAccountOnly: "granted on your account, not on this machine",
246
+ /**
247
+ * Slice 2 (S4), brief §3.3: the machine screen's account rows, after `off` and before `held`. `on account` lists what the
248
+ * user's account grants that THIS machine has not named (an owner as `owner/`); the second line says what to do about it.
249
+ */
250
+ accountLabel: "on account",
251
+ accountNotHere: (connect) => `not on this machine · to capture them here, run: ${connect}`,
252
+ /** Changes queued for the account that no sync has delivered yet. They go out with the next sync; only `connect`, `status`, `off` and a terminal `auth` sync. */
253
+ pendingLabel: "pending",
254
+ pendingValue: (n) => `${n} ${n === 1 ? "change" : "changes"} not yet sent to your account · sent at the next sync`,
255
+ /**
256
+ * VQ's ruling (ii): a queued change the server refused with 400 is kept (a consent statement is never dropped) but a retry
257
+ * fails again, so this row does not say "the next command retries". The URL is the one the web app's error state uses.
258
+ */
259
+ pendingRejectedValue: (n) => `${n} ${n === 1 ? "change" : "changes"} your account didn't accept · report this: github.com/Vibe-Commit/claude-plugin/issues`,
260
+ /**
261
+ * The first sync uploads what this machine already named (per-clone consents made under 0.6.x/0.7.0 reach the account, R3-21).
262
+ * Slugs only, never paths; more than three names shows two and "and N more", and the full list follows, one per line. The names arrive as ONE string, a name per line, because the copy corpus renders every template with string arguments.
263
+ */
264
+ accountSaving: (namesOnePerLine) => {
265
+ const names = namesOnePerLine.split("\n");
266
+ return `Saving this machine's repository choices to your VibeCommit account: ${names.length > 3 ? `${names.slice(0, 2).join(", ")} and ${names.length - 2} more` : names.join(", ")}.`;
267
+ },
268
+ /** The machine-level screen (any folder that is not a work tree). */
269
+ offMachine: "Capture is not on for any repository on this machine.",
270
+ ownersLabel: "owners",
271
+ reposLabel: "repos",
272
+ offLabel: "off",
273
+ /**
274
+ * Machine-level `status`: the repos whose own last upload was refused because their org has not approved capture. §10.2's held
275
+ * vocabulary — the beat word `held`, and the backfill screen's "the org must approve capture" (Alexander, 2026-10-24 17:20).
276
+ */
277
+ heldLabel: "held",
278
+ heldReason: "the org must approve capture",
279
+ ownerTail: "all its repos, including ones you clone later",
280
+ changeLabel: "Change what is captured",
281
+ turnOffInside: "Turn one repo off, run inside it",
111
282
  lastSuccessLabel: "last sent",
112
283
  dashboardLabel: "dashboard",
113
284
  /** `<slug> (git toplevel <path>)` — §10.3's repo row. */
@@ -128,6 +299,21 @@ export const STATUS = {
128
299
  * after the rows, with the glyph turned `warn`. The date is absolute and in UTC.
129
300
  */
130
301
  credentialEnding: (date, authCommand) => `This machine's credential ends ${date}. Run \`${authCommand}\` to renew it.`,
302
+ /**
303
+ * 0.7.0. A `grants.json` that is present and unreadable denies capture for every clone that has a remote
304
+ * (`consent.ts`'s `isCaptureAllowed`), and a hook cannot say so, so `status` does. The file name is the user's
305
+ * to fix; `connect` is where the next consent lands once it is readable.
306
+ */
307
+ 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}\`.`,
308
+ /**
309
+ * 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
310
+ * missing it until `connect` runs. Only said on the npm channel: the plugin ships its own `hooks.json`, and its
311
+ * contract (no `SessionStart`, no git hooks) is unchanged.
312
+ */
313
+ 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.`,
314
+ /** 0.7.0, D4 · A. A hook the user deleted stays deleted; `connect` is the watched way back. */
315
+ commitHooksOff: (connect) => `Commit hooks are off in this repository: sessions are captured, commits are not linked to them. Run \`${connect}\` here to add them.`,
316
+ commitHooksForeign: (connect) => `This clone has its own commit hook, so VibeCommit's is not installed here. Run \`${connect}\` here to chain it.`,
131
317
  credentialShadowed: "VIBECOMMIT_TOKEN is set and takes precedence over the credential saved on this machine. Unset it to use the saved one.",
132
318
  /**
133
319
  * `CR-228`, `TODOS[142]` — bytes the client ADVANCED PAST without delivering.
@@ -154,8 +340,8 @@ export const STATUS = {
154
340
  * turn only when a following prompt arrives, so nothing will ever seal this one and the commits made in
155
341
  * it cannot be recorded. States the consequence and the command; promises nothing about the commits.
156
342
  */
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.",
343
+ openTail: (sessions, finalizeLatest) => `${sessions === 1 ? "1 session has" : `${sessions} sessions have`} ended with the last turn still ` +
344
+ `open, so commits made in that turn are not recorded. Run \`${finalizeLatest}\` to close it.`,
159
345
  /**
160
346
  * `TODOS[145]` — commits the client has seen and is holding because no turn has closed yet. Normal
161
347
  * mid-session; shown as a fact, not as a fault.
@@ -259,12 +445,12 @@ export const STATUS = {
259
445
  * cannot tell a crashed session from one idle at lunch, and closing a session still in progress seals
260
446
  * a turn that was not actually over.
261
447
  */
262
- heldIdle: (bytes, sessions, commits, ids = []) => {
448
+ heldIdle: (bytes, sessions, commits, finalizeSession, ids = []) => {
263
449
  const who = sessions === 1 ? `session ${namedIds(ids)}` : `${sessions} sessions (${namedIds(ids)})`;
264
450
  const commitsClause = commits > 0 ? `, including ${commits} ${commits === 1 ? "commit" : "commits"}` : "";
265
451
  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";
452
+ ? `If that session is finished, \`${finalizeSession} ${ids[0]}\` tries to send them and, if that works, closes it`
453
+ : `If a session above is finished, \`${finalizeSession} <id>\` tries to send its bytes and, if that works, closes it`;
268
454
  return (`${BYTE_COUNT.format(bytes)} ${bytes === 1 ? "byte" : "bytes"} held from ${who}${commitsClause}, with no hook in over 30 ` +
269
455
  `minutes. ${action}; anything held past 24 hours is written off instead.`);
270
456
  },
@@ -299,7 +485,18 @@ export const CONNECT = {
299
485
  * the nouns are transcribed rather than reworded: *"the output of commands it
300
486
  * ran"*, never *"Bash tool results"*.
301
487
  */
302
- consentHeading: "VibeCommit uploads your Claude Code transcripts for this repo.",
488
+ consentHeading: "VibeCommit uploads your coding-agent transcripts for this repo.",
489
+ /**
490
+ * D23 · 17A: the picker path's heading (every `connect` that shows the picker, in a repo or not). `consentHeading`
491
+ * above remains only on the no-remote (`local:`) in-repo path, where there is nothing to pick (D35 · 29A: "Claude
492
+ * Code transcripts" understated what uploads once `connect` also wired Codex and Cursor hooks, 0.2.0).
493
+ */
494
+ consentHeadingPicker: "VibeCommit uploads your coding-agent transcripts for the repositories you choose.",
495
+ /**
496
+ * ONE sentence, not a paragraph, added on the picker path only (design doc step 2, "The disclosure"): what the grant
497
+ * covers beyond the repos named on screen. The four paragraphs under either heading stay byte-identical.
498
+ */
499
+ consentScope: "The choice covers future clones of these repositories too, adds commit hooks to each clone where it can, and is saved to your VibeCommit account so your other machines running VibeCommit 0.8.0 or later can offer it.",
303
500
  /**
304
501
  * ⚠ "private-repo paths" IS THE APPROVED WORDING, and it is the reason
305
502
  * `private` is deliberately NOT registered in `claims.ts`: a substring gate on
@@ -407,11 +604,33 @@ export const CONNECT = {
407
604
  // --- Mechanism copy (CR-016). ---
408
605
  /** The `[y/N]` suffix. The capital is the default and the default is NO. */
409
606
  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:",
607
+ /**
608
+ * 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
609
+ * this branch. It says who must answer ("a person"), that an agent cannot, and what to relay, and it carries the code
610
+ * `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
611
+ * repeated in the sentence. No `--yes`: that would bypass consent (D65 §DR7). The `what` line has no "you" (§13.6).
612
+ */
613
+ nonInteractiveWhat: "Consent needs a person at a terminal.",
614
+ nonInteractiveWhy: "An agent cannot answer it, and standard input is not a terminal. Nothing was read and nothing was uploaded.",
615
+ /** A repo with no remote has a folder to name; the label is the fix and the command follows it. */
616
+ nonInteractiveFix: (folder) => `Ask the user to run this in ${folder}:`,
617
+ /**
618
+ * 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
619
+ * "you" in the `what` line, and is reached BEFORE the scan beat, so "Nothing was read and nothing was uploaded" stays
620
+ * literally true.
621
+ */
622
+ pickerNonInteractiveWhat: "Choosing repositories needs a person at a terminal.",
623
+ 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.",
624
+ pickerNonInteractiveFix: "Ask the user to run this in a terminal:",
414
625
  alreadyConnected: "Capture is already on for this repository.",
626
+ /**
627
+ * 0.7.0. `grants.json` is present and unreadable, which denies this repository whatever consent it has (see
628
+ * `consent.ts`'s `isCaptureAllowed`). Answering the consent prompt could not change that, so `connect` stops
629
+ * BEFORE asking rather than take a yes it cannot honour.
630
+ */
631
+ grantsUnreadableWhat: "Capture can't be turned on: ~/.vibecommit/grants.json can't be read.",
632
+ grantsUnreadableWhy: "Capture is stopped on this machine for every repository that has a remote until the file is readable. Nothing was changed.",
633
+ grantsUnreadableFixLabel: "Fix the file or move it aside, then run:",
415
634
  /**
416
635
  * The browser sign-in beat (PKCE loopback → minted ingest credential) is the
417
636
  * oauth lane's, not this task's. Until it lands, the supported path is the one
@@ -531,12 +750,12 @@ export const COMMIT_HOOK = {
531
750
  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
751
  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
752
  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.",
753
+ perClone: (connect) => `Run \`${connect}\` in each clone you want commits recorded for — git hooks are per-clone, not per-account.`,
535
754
  hooksPathWhat: "Commit capture could NOT be installed for this clone.",
536
755
  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
756
  hooksPathFixLabel: "Fix, either one:",
538
757
  hooksPathFixUnset: "git config --unset core.hooksPath",
539
- hooksPathFixManual: "or add `vibecommit post-commit` to your existing post-commit hook",
758
+ hooksPathFixManual: (postCommit) => `or add \`${postCommit}\` to your existing post-commit hook`,
540
759
  failedWhat: "Commit capture could NOT be installed for this clone.",
541
760
  failedWhy: (why) => `Writing the \`post-commit\` hook failed: ${why}. Transcript capture is unaffected; only commit observation is.`,
542
761
  };
@@ -567,7 +786,7 @@ export const AGENT_HOOKS = {
567
786
  refusedWhat: (agent) => `Session capture could NOT be set up for ${agent}.`,
568
787
  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
788
  refusedFixLabel: "Fix:",
570
- refusedFix: (path) => `check ${path} for a syntax error, then run \`vibecommit connect\` again`,
789
+ refusedFix: (path, connect) => `check ${path} for a syntax error, then run \`${connect}\` again`,
571
790
  failedWhat: (agent) => `Session capture could NOT be set up for ${agent}.`,
572
791
  failedWhy: (path, why) => `Writing ${path} failed: ${why}. Commit capture and any agent already configured are unaffected.`,
573
792
  };
@@ -697,10 +916,391 @@ export const SIGNIN = {
697
916
  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
917
  busyFix: "Run it again in a moment:",
699
918
  };
919
+ /**
920
+ * `vibecommit scan` — design review D17 · 11A, D26, D27, D33 · 27A, and the screen VQ drew for it
921
+ * (style guide §10.13) on 2026-10-24.
922
+ *
923
+ * ⛔ **The nouns are the agent-neutral ones** (D11 · A, D35): "coding agent", never "Claude Code
924
+ * session", because the scan covers three agents and the picker's heading says the same. The agent
925
+ * NAMES appear only on a repo's second line, where they are facts about which store a count came from.
926
+ *
927
+ * ⚠ **Every sentence here is a claims-register row** (the register is `code`'s, VR's PR). None says
928
+ * "every" or "all": the scan counts transcripts still on disk, and what `cleanupPeriodDays` already
929
+ * deleted is not among them.
930
+ */
931
+ export const SCAN = {
932
+ /** The beat line, drawn in the existing beat shape and overwritten once with `done` (D17). */
933
+ beat: "Looking for repositories on this machine...",
934
+ beatDone: "done",
935
+ found: (count) => `Found ${count} ${count === 1 ? "repository" : "repositories"} you've used a coding agent in on this machine.`,
936
+ sessions: (count) => `${count} ${count === 1 ? "session" : "sessions"}`,
937
+ since: (date) => `since ${date}`,
938
+ /** What an agent is called on a repo's second line. The ids are wire values, not labels. */
939
+ agent: { "claude-code": "Claude Code", "codex-cli": "Codex", cursor: "Cursor" },
940
+ notListed: "Not listed here:",
941
+ noRemote: (count, connect) => count === 1 ? `1 repo with no remote: run \`${connect}\` inside it` : `${count} repos with no remote: run \`${connect}\` inside each`,
942
+ /**
943
+ * The picker's own line (PR 1 ruling 2): ONE count and ONE line for a folder with two or more
944
+ * candidate paths, a hashed name (cut, so it cannot be inverted) and a deleted sibling. Claude Code's
945
+ * and Cursor's only — a Codex row records its real `cwd` and is never ambiguous.
946
+ */
947
+ ambiguous: (count) => count === 1
948
+ ? "1 session in a folder that can't be matched to one repo"
949
+ : `${count} sessions in folders that can't be matched to one repo`,
950
+ /** Scan-only (ruling 5a): a folder, or a Codex `cwd`, that exists but is not inside a git work tree. */
951
+ nonRepo: (count) => count === 1
952
+ ? "1 session in a folder that isn't a git repository"
953
+ : `${count} sessions in folders that aren't git repositories`,
954
+ missingFolder: (count) => count === 1
955
+ ? "1 session in a folder that no longer exists"
956
+ : `${count} sessions in folders that no longer exist`,
957
+ /**
958
+ * ⛔ CLAIMS ONLY WHAT GIT SHOWS (ruling 7). The test is a session's mtime against the OLDEST reflog entry,
959
+ * and `gc.reflogExpire` prunes a long-lived clone's oldest entries, so "older than the reflog" is the fact
960
+ * and "the folder was reused" is a possibility — the same repo deleted and cloned again reads the same.
961
+ */
962
+ pathReused: (count) => count === 1
963
+ ? "1 session older than its clone's reflog (possibly a reused folder)"
964
+ : `${count} sessions older than their clone's reflog (possibly a reused folder)`,
965
+ /** One clone: the path rides the line. More: this heading, then one indented path per clone. */
966
+ noReflogOne: (path) => `1 clone with no reflog: ${path} (backfill skips it)`,
967
+ noReflogMany: (count) => `${count} clones with no reflog (backfill skips them):`,
968
+ /** Rollouts on disk that no index row names (ruling 5c) — counted, never opened. */
969
+ unindexed: (count) => count === 1
970
+ ? "1 Codex session missing from its thread index"
971
+ : `${count} Codex sessions missing from its thread index`,
972
+ /**
973
+ * `Codex sessions couldn't be read (unrecognised index)`, one line per unreadable agent (R3-15). The
974
+ * bracket names a cause the user can act on; a plain read failure (ruling 6) has none to name, so it
975
+ * gets none. The `--json` `reason` is the same three words either way.
976
+ */
977
+ unreadable: (agent, reason) => reason === null
978
+ ? `${agent} sessions couldn't be read`
979
+ : `${agent} sessions couldn't be read (${reason})`,
980
+ next: "Choose what is captured:",
981
+ /** Exit 4. Both screens end on the same command. */
982
+ none: "No coding-agent sessions were found on this machine.",
983
+ /**
984
+ * Exit 4 with `Not listed here:` lines: sessions exist that could not be listed, so `none` would be
985
+ * untrue. `none` prints only when that block is empty, i.e. the scan saw no session at all (VQ, PR 1
986
+ * ruling 3).
987
+ */
988
+ noneListable: "No repository could be listed from the sessions on this machine.",
989
+ noneNext: "Choose owners or repos anyway:",
990
+ };
991
+ /**
992
+ * `vibecommit backfill` — style guide §10.14, design review D14 · 8A, D15 · 9A; capture 0.7.0, PR 5.
993
+ *
994
+ * Every sentence here is a claims-register row. ⛔ Three promises it must never make: that a backfilled session is
995
+ * LINKED to a commit (it arrives without links, and the copy says so), that "every" or "all" earlier sessions are
996
+ * uploaded (only the ones still on this machine and not yet sent), and that a held session will upload by itself (no
997
+ * hook fires for an old session, so the held line points at a re-run).
998
+ *
999
+ * The two folder reasons are different facts (VQ ruling): `folder differs` is a RECORDED folder that is not this clone,
1000
+ * `folder not recorded` is a transcript that records none.
1001
+ */
1002
+ export const BACKFILL = {
1003
+ /**
1004
+ * Inside `connect` the repos are the ones it just showed, so "these"; run on its own there is no "these", so it names whose
1005
+ * they are (VQ, PR 5 review).
1006
+ */
1007
+ offerHeading: (repos, standalone) => standalone
1008
+ ? `Earlier sessions are still on this machine for ${repos} ${repos === 1 ? "repository" : "repositories"} you chose.`
1009
+ : `Earlier sessions are still on this machine for ${repos} of these repos.`,
1010
+ /** The count on a repo row. "Not yet uploaded", so it is never mistaken for the picker's total on disk. */
1011
+ notYetUploaded: (count) => `${count} not yet uploaded`,
1012
+ /**
1013
+ * Cursor sessions are not offered (Alexander, 2026-10-24 13:54 EST: Cursor's transcripts record no folder, so the
1014
+ * folder check cannot run). One muted line, only when a repo you chose has some.
1015
+ */
1016
+ cursorNotUploaded: (count) => count === 1
1017
+ ? "1 Cursor session isn't uploaded: Cursor doesn't record the folder a session started in."
1018
+ : `${count} Cursor sessions aren't uploaded: Cursor doesn't record the folder a session started in.`,
1019
+ /** Two lines, as §10.14 draws them. */
1020
+ /**
1021
+ * 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
1022
+ * skip on every re-run. (Sub-agent streams are not capped.)
1023
+ */
1024
+ overSizeNote: (count) => count === 1 ? "1 session over 50 MiB isn't uploaded." : `${count} sessions over 50 MiB aren't uploaded.`,
1025
+ /**
1026
+ * A clone with no reflog cannot be tested for a reused path (a transcript older than the clone's first reflog entry), and
1027
+ * that test is the only guard against one, so its sessions are left out of the count and named here with the clone.
1028
+ */
1029
+ noReflogNote: (count, path) => count === 1
1030
+ ? `1 session in a clone with no reflog isn't uploaded: ${path}`
1031
+ : `${count} sessions in a clone with no reflog aren't uploaded: ${path}`,
1032
+ uploadNote: ["They upload one at a time, the same way new sessions do, without", "commit links."],
1033
+ question: (sessions) => sessions === 1 ? "Upload this session now? [y/N] " : `Upload these ${sessions} sessions now? [y/N] `,
1034
+ /** A no. A completed consent flow (§13.7), not an error. */
1035
+ declined: (backfill) => `Nothing was uploaded. Run \`${backfill}\` any time.`,
1036
+ nothingToUpload: "Nothing to upload: no earlier session on this machine is waiting for a repository you've chosen.",
1037
+ runHeading: (sessions) => sessions === 1 ? "Uploading 1 session..." : `Uploading ${sessions} sessions, one at a time...`,
1038
+ /** Outcome fragments on a repo line, joined by " · ". */
1039
+ uploaded: (count) => `${count} uploaded`,
1040
+ skipped: (count, reason) => `${count} skipped, ${reason}`,
1041
+ held: (count) => `${count} held, the org must approve capture`,
1042
+ failed: (count, reason) => `${count} failed, ${reason}`,
1043
+ stopped: "stopped",
1044
+ /** Skip reasons. */
1045
+ overSize: "over 50 MiB",
1046
+ folderDiffers: "folder differs",
1047
+ folderNotRecorded: "folder not recorded",
1048
+ /** The transcript could not be opened at all (permissions), which is a different fact from one that records no folder. */
1049
+ couldntBeRead: "couldn't be read",
1050
+ /** The file vanished between the offer and its upload: a skip, not a refusal by the server. */
1051
+ fileGone: "the file is gone",
1052
+ /** `vibecommit off` (or a grant removed) while the run was going: the rest of that repo is not sent. */
1053
+ turnedOff: "turned off while this ran",
1054
+ outsideAgentFolder: "not in the agent's session folder",
1055
+ /** Failure reasons: a `never`. The server's own code is never shown (§13.6: no bare status, no raw error). */
1056
+ refusedByServer: "the server refused it",
1057
+ repositoryForbidden: "another organization owns this repository",
1058
+ noRepoIdentity: "no repository identity",
1059
+ unrecognisedFileName: "the file name isn't a session id",
1060
+ /** The summary. The glyph is `ok` only when nothing was held, stopped or failed. */
1061
+ summary: (uploaded, total, skipped, held, failed) => {
1062
+ const noun = total === 1 ? "session" : "sessions";
1063
+ const tail = [
1064
+ skipped > 0 ? `${skipped} skipped` : null,
1065
+ held > 0 ? `${held} held` : null,
1066
+ failed > 0 ? `${failed} failed` : null,
1067
+ ].filter((part) => part !== null);
1068
+ return `Uploaded ${uploaded} of ${total} ${noun}.${tail.length > 0 ? ` ${tail.join(", ")}.` : ""}`;
1069
+ },
1070
+ /** `owners` is the held repos' owners, already phrased ("acme-corp", "acme-corp or other-org"). */
1071
+ heldBlock: (owners) => `Held sessions wait for ${owners} to approve capture:`,
1072
+ heldThenRun: "Then run:",
1073
+ /**
1074
+ * The stop block: a server `later` (5xx, 429) stops the run, with the resume fix. ⛔ It states only what landed: `uploaded` is
1075
+ * sessions the server took, not sessions looked at, and "not yet uploaded" is what a re-run will OFFER again (skipped and held
1076
+ * sessions among them), not a promise of what it will send.
1077
+ */
1078
+ stopWhat: (uploaded, total) => `Upload stopped: ${uploaded} of ${total} ${total === 1 ? "session" : "sessions"} uploaded.`,
1079
+ 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.`,
1080
+ /** No response at all (a refused connection, a timeout): not the server asking anything. */
1081
+ 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.`,
1082
+ stopFixLabel: "Resume:",
1083
+ /** Ctrl+C at the y/N or during the upload. Exit 0: capture is on. */
1084
+ interrupted: (backfill) => `Stopped. A re-run of ${backfill} sends what is left.`,
1085
+ /** Exit 3. */
1086
+ notConnectedWhat: "This machine isn't connected.",
1087
+ notConnectedWhy: "`backfill` uploads with this machine's credential, and there isn't one. Nothing was read.",
1088
+ notConnectedFixLabel: "Connect first:",
1089
+ /** `grants.json` is present and unreadable: every repo with a remote is denied, so there is nothing honest to offer. */
1090
+ grantsUnreadableWhat: "Backfill can't run: ~/.vibecommit/grants.json can't be read.",
1091
+ 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.",
1092
+ grantsUnreadableFixLabel: "Fix the file or move it aside, then run:",
1093
+ /** Exit 2. A prompt that proceeds when nobody can answer it is not a prompt. */
1094
+ nonInteractiveWhat: "`backfill` needs an interactive terminal.",
1095
+ nonInteractiveWhy: "Standard input is not a terminal, so the question before uploading cannot be answered. Nothing was read and nothing was uploaded.",
1096
+ nonInteractiveFix: "Run this in a terminal:",
1097
+ /** The credential was rejected mid-run (a 401): the same block `finalize` shows, in this verb's words. */
1098
+ credentialStoppedWhat: "Upload stopped: this machine's credential is no longer accepted.",
1099
+ credentialStoppedWhy: "Nothing already sent is lost, and a re-run offers the sessions not yet uploaded.",
1100
+ credentialStoppedFixLabel: "Renew it, then run backfill again:",
1101
+ usageWhat: "That is not something `vibecommit backfill` understands.",
1102
+ usageWhy: "Nothing was read and nothing was uploaded. `backfill` takes no arguments.",
1103
+ };
1104
+ /**
1105
+ * `vibecommit connect` from any folder — the picker, the grant block, the hooks step and the endings (0.7.0, PR 3).
1106
+ * Style guide §10.12, copied from the merged guide and the design doc; the design doc wins where they differ.
1107
+ */
1108
+ export const PICKER = {
1109
+ onNow: "on now",
1110
+ turnedOff: "turned off",
1111
+ /** Why an `on now` row is on (D5 · 1A), so a 0.6.x upgrader sees what is already captured. */
1112
+ reasonOwner: "owner",
1113
+ reasonRepo: "repo",
1114
+ reasonPerClone: "per clone",
1115
+ /** The owner row ALWAYS states how many repos `aN` covers (D9), so a hidden repo is never covered unseen. */
1116
+ ownerAll: (count) => `all ${count} ${count === 1 ? "repo" : "repos"} here, including repos you clone later`,
1117
+ ownerOnReposHere: (count) => `${count} ${count === 1 ? "repo" : "repos"} here`,
1118
+ turnOnHint: "choose it to turn it back on",
1119
+ /** Where the working folder is a repo, the row is marked so a pre-tick (or a `$HOME` dotfiles repo) is legible. */
1120
+ thisRepo: "this repo",
1121
+ thisFolder: "this folder",
1122
+ moreTyped: (count, owner) => `${count} more in ${owner}/ · type more to list them`,
1123
+ moreChecklist: (count, owner) => `${count} more in ${owner}/`,
1124
+ /** The typed prompt (D8). */
1125
+ choose: (hasOwners) => hasOwners
1126
+ ? "Choose a1, numbers (1 2), owner/ or owner/repo."
1127
+ : "Choose numbers (1 2), owner/ or owner/repo.",
1128
+ removeHint: "Remove one with -N, -owner/ or -owner/repo.",
1129
+ enterNothing: "Press Enter to change nothing.",
1130
+ /** D34 in the typed prompt: the working folder's repo is pre-selected, so Enter leads to the (default No) y/N. */
1131
+ enterThisRepo: (slug) => `Press Enter to choose ${slug}, this repo.`,
1132
+ promptMark: "> ",
1133
+ /** `tokens` is the already-joined list (`9 (no row 9), acme, corp`): the corpus renders every template with a string. */
1134
+ notUnderstood: (tokens) => `Not understood: ${tokens}. Nothing was chosen.`,
1135
+ noRow: (n) => `${n} (no row ${n})`,
1136
+ alreadyOn: (token, remove) => `${token} (already on; ${remove} stops it)`,
1137
+ notOn: (token) => `${token} (not on)`,
1138
+ /** The empty screen (D12 · 6A): a new laptop, no agent session on disk at all. */
1139
+ emptyNone: "No coding-agent sessions were found on this machine.",
1140
+ 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.",
1141
+ emptyType: "Type owner/ or owner/repo, for example alexvibecommit/.",
1142
+ /** Sessions exist but none could be listed (the `Not listed here:` block is not empty): scan's own wording. */
1143
+ emptyListable: "Type owner/ or owner/repo, for example alexvibecommit/, to capture a repository that is not listed.",
1144
+ /** The checklist (D28). */
1145
+ hint: "↑↓ move · space choose · enter done · t typed prompt · ctrl+c cancel",
1146
+ hintTyped: (connectTyped) => `For a screen reader, run: ${connectTyped}`,
1147
+ /** The enter-with-nothing lines (D5). */
1148
+ nothingGranted: (connect) => `Nothing granted. Run \`${connect}\` again any time.`,
1149
+ noChanges: (summary) => `No changes. Capture stays on for ${summary}.`,
1150
+ cancelled: "Cancelled. Nothing was changed.",
1151
+ /** The grant block (D7 · 3A). */
1152
+ chose: "You chose:",
1153
+ labelOwners: "owners",
1154
+ labelRepos: "repos",
1155
+ labelTurnOn: "turn on",
1156
+ labelStop: "stop",
1157
+ ownerHere: (count) => `(${count} here, and later clones)`,
1158
+ noCloneHere: "(no clone here yet)",
1159
+ questionGrant: "Upload transcripts from these repositories?",
1160
+ questionStop: "Stop capturing these repositories?",
1161
+ stopped: "Stopped capturing the repositories above.",
1162
+ /** "1 owner and 2 repos" — the on-now summary of Enter-with-grants, `auth`'s one line and the status heading. */
1163
+ summary: (owners, repos) => {
1164
+ const o = `${owners} ${owners === 1 ? "owner" : "owners"}`;
1165
+ const r = `${repos} ${repos === 1 ? "repo" : "repos"}`;
1166
+ if (owners > 0 && repos > 0)
1167
+ return `${o} and ${r}`;
1168
+ return owners > 0 ? o : r;
1169
+ },
1170
+ authStays: (summary) => `Capture stays on for ${summary} on this machine.`,
1171
+ /** The hooks step (D13 · 7A). */
1172
+ hooksBeat: (clones) => `Adding commit hooks to ${clones} ${clones === 1 ? "clone" : "clones"}...`,
1173
+ hooksDone: "done",
1174
+ hooksCount: (added, chained, notAdded) => {
1175
+ const head = `${added} added${chained > 0 ? `, ${chained} of them after your own hook` : ""}`;
1176
+ return notAdded > 0 ? `${head} · ${notAdded} not added` : head;
1177
+ },
1178
+ hooksNotAdded: (count) => `Commit hooks were not added to ${count} ${count === 1 ? "clone" : "clones"}:`,
1179
+ hooksPathReason: "core.hooksPath is set",
1180
+ /** A hook write that FAILED is a real exception with its own reason (VQ, item 9): neither dropped nor given `hooksPath`'s fix. */
1181
+ hooksFailedReason: "the hook could not be written",
1182
+ hooksStillCaptured: "Sessions there are still captured. To record commits too, either:",
1183
+ hooksStillCapturedOther: "Sessions there are still captured.",
1184
+ hooksFailedFixLabel: "Where the hook could not be written, try again:",
1185
+ /** "Capture is on" (D18 · 12A, D19 · 13A). */
1186
+ onFor: (summary) => `Capture is on for ${summary} on this machine.`,
1187
+ 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.\"",
1188
+ stopOne: "To stop one repo, run inside it:",
1189
+ /**
1190
+ * Alexander's "caveat for other owners" (2026-10-24; amended 17:20), on the "Capture is on" block. It prints for the owners the
1191
+ * test capture did NOT check: from `~` that is every granted owner (there is no session to probe), and in a repo it is every
1192
+ * 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
1193
+ * account the client cannot know whether an org is yet to approve capture, so it says one may be. Never printed when every
1194
+ * such owner is the user's own account.
1195
+ */
1196
+ 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.`,
1197
+ onNewLaptop: (names) => `Capture is on for ${names} on this machine.`,
1198
+ onNewLaptopProof: "No clone of it is here yet. Clone one and open a coding agent in it; that session is captured.",
1199
+ // --- Slice 2 (S4 part 3, capture 0.8.0): the account's grants in `connect`. Brief §2-§3, VQ's rulings 2026-10-24. ---
1200
+ /** The group of account grants this machine has not named (§2): numbered, unticked until chosen, like any other row. */
1201
+ onAccount: "on your account",
1202
+ onAccountHint: "choose it to capture it here",
1203
+ /** A `turn on` row whose repo was turned off on the ACCOUNT (not only here): the yes re-includes it everywhere. */
1204
+ turnedOffOnAccount: "(turned off on your account)",
1205
+ /** Screen 1a: the second line of the prompt block, on a machine with no credential, a terminal and a browser to sign in with. */
1206
+ gLine: "Or type g for the repositories your account already grants. It signs in first.",
1207
+ /** The one token a mixed answer is refused for; `notUnderstood` wraps it in D8's line. */
1208
+ gMixedToken: "g can't be combined with other choices",
1209
+ /** Screen 1b: the grant block's heading when the list is the account's, not the person's picks. */
1210
+ accountGrants: "Your account grants:",
1211
+ /** Screen 1b, N: the credential `g` signed in with is revoked. The removed-here variant is a revoke that could not reach the server. */
1212
+ gDeclinedRevoked: "Nothing is captured on this machine. The credential made for this was revoked.",
1213
+ gDeclinedRemoved: "Nothing is captured on this machine. The credential made for this was removed here; the server will be told on the next vibecommit command that contacts the server.",
1214
+ /** Ctrl+C after a mint (VQ's correction): never "Nothing was changed", because a credential was made and then removed. */
1215
+ gCancelledRevoked: "Cancelled. Nothing is captured on this machine. The credential made for this was revoked.",
1216
+ gCancelledRemoved: "Cancelled. Nothing is captured on this machine. The credential made for this was removed here; the server will be told on the next vibecommit command that contacts the server.",
1217
+ /**
1218
+ * `g` stopped because this machine could not make the write-ahead record of the credential it had just been given (an unwritable,
1219
+ * unreadable or full queue). The credential is revoked at once (`REPLACE.declinedRevoked`), or `REPLACE.declinedOrphan` says where
1220
+ * to remove it. An insecure queue file gets the insecure-file block instead of this line.
1221
+ */
1222
+ gNotRecorded: "g stopped: this machine couldn't keep a record of the credential it was just given.",
1223
+ /**
1224
+ * The yes found `g`'s own record of the credential UNUSABLE (the queue file became insecure or unreadable while the question was
1225
+ * open): nothing was revoked by anyone, this machine just cannot keep the record, so no credential is kept.
1226
+ */
1227
+ gRecordUnusable: (connect) => `This machine's record of the credential this run had made couldn't be used, so none was kept. The repositories you chose stay named here; run \`${connect}\` again to sign in.`,
1228
+ /**
1229
+ * The yes found a `credentials.json` already there (another run saved one while the question was open): that one is kept, this run's
1230
+ * is not, and the grants the yes wrote are saved — capture IS on, so this never says nothing is captured. The fate of the
1231
+ * credential this run made follows on its own line (`REPLACE.declinedRevoked`, `gRemovedWillTell`, or `REPLACE.declinedOrphan`).
1232
+ */
1233
+ gOccupied: (connect) => `A credential was saved on this machine while this question was open, so that one is kept and the one this run made was not. The repositories you chose are saved; run \`${connect}\` again to finish.`,
1234
+ /** The fate line for `gOccupied` when a readable row still holds the credential this run made. */
1235
+ gRemovedWillTell: "The credential just created was removed here; the server will be told on the next vibecommit command that contacts the server.",
1236
+ /**
1237
+ * The yes found `g`'s own record of the credential gone: another vibecommit command (a retry that ran while the question was open)
1238
+ * had already revoked it. No credential is kept, and the grants the yes wrote stay. `connect` is THIS install's command.
1239
+ */
1240
+ gRevokedElsewhere: (connect) => `Another vibecommit command revoked the credential this run had made, so none was kept. The repositories you chose stay named here; run \`${connect}\` again to sign in.`,
1241
+ /** Screen 1c: `g` found no grants on the account. The credential is kept and the person chooses here. */
1242
+ gEmpty: "Your account doesn't grant any repositories yet. Choose them here instead:",
1243
+ /**
1244
+ * `g` signed in but the account's grants could not be read (the server did not answer, or answered off-contract). It must not
1245
+ * say the account is empty, and it says what to do: run `connect` again. `connect` is THIS install's command (a function of it).
1246
+ */
1247
+ gUnavailable: (connect) => `The repositories your account grants couldn't be read right now. Run \`${connect}\` again to choose from them.`,
1248
+ /** Screen 2: a machine joining an account by picking local repos is asked about the rest of the account's grants. */
1249
+ joinAlso: (namesOnePerLine) => {
1250
+ const names = namesOnePerLine.split("\n");
1251
+ const head = names.length > 3
1252
+ ? `${names.slice(0, 2).join(", ")} and ${names.length - 2} more`
1253
+ : names.length > 1
1254
+ ? `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`
1255
+ : names[0];
1256
+ return `Your account also grants ${head}.`;
1257
+ },
1258
+ joinQuestion: "Capture those here too?",
1259
+ /**
1260
+ * Under "Capture is on" when the sync after `connect`'s writes did not reach the server (VQ, on VT's question). Capture IS on —
1261
+ * local grants admit the repo — so this is not a warning; it only tells the truth about the account. Only `connect`, `status` and
1262
+ * `off` sync, so it names `status` (the install channel's command); a 400 and a 401 have their own lines (below and `ERRORS`).
1263
+ */
1264
+ accountNotTold: (status) => `Your account hasn't been told yet; running \`${status}\` retries.`,
1265
+ /**
1266
+ * "0.8.0 truth fixes" F2: the same line on a machine that HAS SYNCED before. There a grant the account has not been told about
1267
+ * holds the repository's sessions on this machine (`isCaptureAllowed`, step 3), so "Capture is on" needs the truth about it.
1268
+ * A machine that has never synced holds nothing and keeps `accountNotTold`.
1269
+ */
1270
+ accountNotToldHeld: (status) => `Your account hasn't been told yet. Until it is, sessions in the repositories you just chose that it doesn't grant wait on this machine; running \`${status}\` retries.`,
1271
+ joinDeclined: (status) => `They stay granted on your account and aren't captured on this machine. ${status} lists them.`,
1272
+ };
700
1273
  /** `vibecommit off`. */
701
1274
  export const OFF = {
702
1275
  done: "Capture is off for this repository.",
1276
+ /**
1277
+ * 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
1278
+ * machine. Slice 2 extends the sentence to the other machines once they sync; until then it says only what is true.
1279
+ */
1280
+ doneSlug: (slug) => `Turned off ${slug} for every clone on this machine.`,
1281
+ /** D31: bare `off` outside a repository. The picker is where an owner or another repo is unticked. */
1282
+ outsideRepoWhat: "`off` turns off the repository you're in.",
1283
+ outsideRepoFixLabel: "To stop capturing an owner or another repo, untick it in the picker:",
1284
+ /** `grants.json` is unreadable, so the exclusion could not be recorded. The clone's own consent was still withdrawn. */
1285
+ grantsUnreadableWhat: "The turn-off could not be recorded: ~/.vibecommit/grants.json can't be read.",
1286
+ grantsUnreadableWhy: "Capture is already stopped for this repository while the file can't be read, and this clone's own consent was withdrawn.",
1287
+ grantsUnreadableFixLabel: "Fix the file or move it aside, then run:",
703
1288
  alreadyOff: "Capture was already off for this repository.",
1289
+ /**
1290
+ * Slice 2 (S4), brief §2 and §5. On a machine with a credential the exclusion is also queued for the account and synced
1291
+ * (3 s), so the success line says where else it reaches; `unsyncedOthers` says who keeps capturing until they sync; and
1292
+ * `notSentYet` is the design's sentence for a write that could not reach the server (it stays queued).
1293
+ */
1294
+ doneSlugAccount: (slug) => `Turned off ${slug} for every clone on this machine, and on your other machines running VibeCommit 0.8.0 or later once they sync.`,
1295
+ // ⛔ A 0.6.x/0.7.0 machine never syncs, so every sentence about "your other machines" names the version that does (VQ, on #87).
1296
+ unsyncedOthers: (n) => n === 1
1297
+ ? "1 of your machines hasn't synced and keeps capturing it until it syncs on VibeCommit 0.8.0 or later."
1298
+ : `${n} of your machines haven't synced and keep capturing it until they sync on VibeCommit 0.8.0 or later.`,
1299
+ notSentYet: "Stopped here. Your other machines running VibeCommit 0.8.0 or later don't know yet; it will retry.",
1300
+ /** The write was stopped, not just unsent (a 400, a 401, too large, unreadable): "it will retry" would be false, so it does not say so. */
1301
+ notSentStopped: "Stopped here. Your other machines running VibeCommit 0.8.0 or later don't know yet.",
1302
+ /** The account already had it turned off (another machine, or the dashboard); this machine now agrees. */
1303
+ alreadyOffAccount: "Already off on your account; now off on this machine too.",
704
1304
  note: "Sessions already recorded are unaffected.",
705
1305
  // `TODOS[184]`. `off` used to ignore `argv` entirely and always run — a stray
706
1306
  // argument, including `--help`'s own flag landing here by a bug, must never
@@ -989,7 +1589,10 @@ export const WHY = {
989
1589
  */
990
1590
  skewWhat: "The record service sent an answer this version is too old to show.",
991
1591
  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.",
1592
+ /** The PLUGIN channel's remedy (`COMMANDS.pluginInstall`). */
992
1593
  skewFix: "Reinstall the VibeCommit plugin to pick up the current client, then run this again:",
1594
+ /** The npm channel's remedy (`COMMANDS.npmUpdate`): an npm install has no plugin to reinstall. */
1595
+ skewFixNpm: "Update VibeCommit to pick up the current client, then run this again:",
993
1596
  /** The bearer is dead after one refresh and one retry. Exit 3, not 1. */
994
1597
  signedOutWhat: "This machine is not signed in to VibeCommit.",
995
1598
  signedOutWhy: "Reading a record needs a signed-in user, and no usable sign-in was found on this machine.",
@@ -1276,20 +1879,44 @@ export const ABSENCE = {
1276
1879
  * know were bound: `closed` says the last turn is recorded, not that any commit matched it.
1277
1880
  */
1278
1881
  export const FINALIZE = {
1279
- needTarget: "Say which session to close: --session <id>, or --latest.",
1882
+ // 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
1883
+ // below lost exactly the clause that moved; the fix commands are the ones the old sentence named.
1884
+ needTarget: "Say which session to close.",
1885
+ needTargetFix: "Name one, or take the latest:",
1280
1886
  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>:",
1887
+ noRecentSession: "No session in this repository has delivered anything in the last 24 hours.",
1888
+ noRecentSessionFix: "Name a session:",
1889
+ severalRecent: "More than one session in this repository is recent.",
1890
+ severalRecentFix: "Name one:",
1891
+ /** `finalize --session <id>` for a session that is known, e.g. the one whose delivery just failed. */
1892
+ commandSession: (id) => `vibecommit finalize --session ${id}`,
1893
+ /** The same with the placeholder, for a fix line written before a session is known. */
1894
+ commandSessionAny: "vibecommit finalize --session <id>",
1895
+ /** The command without its id, for a sentence that names the id itself (`status`'s held-idle line). */
1896
+ commandSessionBase: "vibecommit finalize --session",
1897
+ commandSessionForce: (id) => `vibecommit finalize --session ${id} --force`,
1283
1898
  recentRow: (id, minutesAgo) => `${id} (${minutesAgo} min ago)`,
1284
1899
  unknownSession: (id) => `This repository has no captured session ${id}.`,
1285
1900
  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.",
1901
+ stillWriting: "That session's transcript changed a moment ago.",
1902
+ stillWritingFix: "Wait for the session to finish, or if it is over, run:",
1287
1903
  closed: (id) => `Closed session ${id}: its last turn is recorded, so commits made in it can now be recorded.`,
1288
1904
  alreadyClosed: (id) => `Session ${id} had nothing left open. Nothing changed.`,
1289
1905
  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.",
1906
+ refused: "The server declined to close the session on this attempt.",
1907
+ retryFix: "Run it again:",
1908
+ notDelivered: "Could not deliver the session. Nothing was closed.",
1909
+ credentialStopped: "VibeCommit stopped sending for this credential.",
1910
+ credentialStoppedFix: "Replace it:",
1911
+ /**
1912
+ * PR 7 (Q2, VQ's wording): the ingest address in the environment (`VIBECOMMIT_INGEST_URL`) is one this client refuses to
1913
+ * send to, so retrying cannot help. Shared by `finalize` and `backfill`, so the `why` says only "Nothing was sent": it must
1914
+ * not claim a session was or was not closed. The VALUE is never echoed (a URL can carry a secret).
1915
+ */
1916
+ ingestUrlWhat: "The address VibeCommit sends sessions to can't be used.",
1917
+ ingestUrlWhy: "VIBECOMMIT_INGEST_URL must be an https address, or an http address on this machine, and it isn't. Nothing was sent.",
1918
+ ingestUrlFix: "Correct it, or remove it to use the default address:",
1919
+ ingestUrlCommand: "unset VIBECOMMIT_INGEST_URL",
1293
1920
  /**
1294
1921
  * `TODOS[177]` follow-up (VL, 2026-10-08, VG's silent-loss finding) — ONE stderr line for the whole run when
1295
1922
  * `finalize` sets any inferred-squash pairs aside (the main delivery, the drain loop, or both, summed). Printed
@@ -1317,8 +1944,8 @@ export const REJECTED = {
1317
1944
  * command across lines; `unconfirmed` is their join, the sentence the plan wrote.
1318
1945
  */
1319
1946
  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.",
1947
+ unconfirmedFix: (status) => `Run \`${status}\` later.`,
1948
+ unconfirmed: (status) => `Couldn't confirm this machine's credential with the server. Run \`${status}\` later.`,
1322
1949
  };
1323
1950
  /** `TODOS[184]`-style one-sentence facts the retire step prints; every one is true whatever the server did. */
1324
1951
  export const RETIRE = {
@@ -1350,6 +1977,12 @@ export const REPLACE = {
1350
1977
  questionUnconfirmed: "Upload this machine's transcripts to the workspace you signed in to?",
1351
1978
  replacesValue: (org) => `credential for ${org}`,
1352
1979
  question: (org) => `Upload this machine's transcripts to ${org}?`,
1980
+ /**
1981
+ * 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
1982
+ * questions in a run. The workspace is cut to 28 columns (not `question`'s 30) so the line stays within 80 with `[y/N]`.
1983
+ */
1984
+ questionRepos: (org) => `Upload transcripts from the repos above to ${org}?`,
1985
+ questionReposUnconfirmed: "Upload transcripts from the repos above to your signed-in workspace?",
1353
1986
  declined: "Nothing was saved. The approval is discarded.",
1354
1987
  /** Ctrl+C at the question (the prompt was aborted): the same outcome as N, said as what it was. */
1355
1988
  cancelled: "Cancelled. Nothing was saved.",
@@ -1444,13 +2077,19 @@ export const UPDATE = {
1444
2077
  plugin: (latest) => `capture ${latest} is available: update the VibeCommit plugin in Claude Code.`,
1445
2078
  };
1446
2079
  export const ERRORS = {
1447
- notConnected: "This repository is not connected. Run `vibecommit connect`.",
2080
+ notConnected: "This repository is not connected.",
2081
+ /** PR 7 (Q7): the remedy was a sentence inside the `what` line; it is a command now, so it moves to the fix block. */
2082
+ notConnectedFix: "Connect it:",
1448
2083
  networkLater: "Could not reach VibeCommit. This session will be recorded on the next turn.",
1449
2084
  // `11A`. The `what` of the §13.6 rejected-credential block (`REJECTED`), not a second sentence: the
1450
2085
  // old line sent users to `connect`, which printed "Connected" over the same dead credential (G1).
1451
2086
  authFatal: "This machine's credential is no longer accepted.",
1452
2087
  payloadNever: "This session was too large to record and has been skipped.",
1453
2088
  notAGitRepo: "Not a git repository.",
2089
+ /** PR 7 (VQ): names the folder, tilde-shortened and cut from the LEFT by the caller (`truncatePath`, §13.4). */
2090
+ notAGitRepoWhy: (folder) => `${folder} is not inside a git work tree.`,
2091
+ /** A fix with no command to give: the remedy is where to run it, so the block has a label and no commands. */
2092
+ notAGitRepoFix: "Run it again inside a git repository.",
1454
2093
  };
1455
2094
  /**
1456
2095
  * The interactive top-level catch (CR-022).
@@ -1470,7 +2109,8 @@ export const INTERNAL = {
1470
2109
  unexpectedWhat: "VibeCommit stopped on an unexpected error.",
1471
2110
  unexpectedWhy: "The command did not finish.",
1472
2111
  unexpectedFix: "Run it again with the error detail shown:",
1473
- unexpectedDebugCommand: "VIBECOMMIT_DEBUG=1 vibecommit <command>",
2112
+ // The command is this install's own (PR 7): `node <path> <command>` on a plugin-only machine, which has no `vibecommit`.
2113
+ unexpectedDebugCommand: (command) => `VIBECOMMIT_DEBUG=1 ${command} <command>`,
1474
2114
  };
1475
2115
  /**
1476
2116
  * Credential-loading failures (CR-016).
@@ -1498,6 +2138,10 @@ export const CREDENTIAL = {
1498
2138
  insecureFileModeLabel: "mode",
1499
2139
  insecureFileWhy: "It was not read. A credential file must be readable only by its owner.",
1500
2140
  insecureFileFix: "Restore the mode, then reconnect:",
2141
+ /** The FOLDER (`~/.vibecommit`) is open to other accounts, not the file: same code, its own what, why and `chmod`. */
2142
+ insecureDirWhat: "The folder that holds the credential file is open to other accounts on this machine.",
2143
+ insecureDirWhyLabel: "folder",
2144
+ insecureDirWhy: "The credential was not read. This folder must be accessible only to its owner.",
1501
2145
  unreadableWhat: "The credential file could not be read.",
1502
2146
  unreadableWhy: "It exists but does not contain a credential in the expected form.",
1503
2147
  /** `CR-216/U4`. Was "Replace it by reconnecting:" — see `wrongClassFix` above. */
@@ -1531,15 +2175,26 @@ export const AUTH = {
1531
2175
  nextCommand: "vibecommit connect",
1532
2176
  emptyWhat: "No credential was given.",
1533
2177
  emptyWhy: "Nothing was read, so nothing was written.",
1534
- emptyFix: "Mint one, then run:",
2178
+ /**
2179
+ * 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
2180
+ * non-interactive path was named nowhere. The URL comes FIRST (inside the label, where it cannot be wrapped away from
2181
+ * its sentence) and the commands LAST, so the fix stays last (§13.6). A function because `URLS` is declared below.
2182
+ */
2183
+ mintFix: (url) => `Mint one at ${url}, then run either:`,
2184
+ /**
2185
+ * The two ways to hand it over: piped (a script or an agent), or typed at a terminal. `--stdin` never continues into `connect`
2186
+ * (D32 guard 1). `auth` is `authCommandFor(env, selfPath)`: a plugin-only machine has no `vibecommit` on PATH, so the line names
2187
+ * `node <path> auth` there (README, "Keeping a machine connected"), and handing it `vibecommit auth` would be a new wrong output.
2188
+ */
2189
+ mintCommands: (auth) => [`printf %s "$TOKEN" | ${auth} --stdin`, auth],
1535
2190
  wrongClassWhat: "That is not a VibeCommit ingest credential.",
1536
2191
  wrongClassWhy: `An ingest credential begins ${INGEST_TOKEN_PREFIX}. Nothing was written and nothing was sent.`,
1537
- wrongClassFix: "Mint one, then run:",
1538
2192
  argvWhat: "A credential must not be passed as an argument.",
1539
2193
  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
2194
  argvFix: "Read it from the terminal, or from a pipe:",
1541
2195
  argvFixPrompt: "vibecommit auth",
1542
- argvFixStdin: 'printf %s "$TOKEN" | vibecommit auth --stdin',
2196
+ /** A pipe, so `channelCommand` cannot rewrite it: it takes the channel's `auth` (`authCommandFor`) instead. */
2197
+ argvFixStdin: (auth) => `printf %s "$TOKEN" | ${auth} --stdin`,
1543
2198
  // --- `CR-226`: the browser mint, and the paste it falls back to. ---
1544
2199
  /**
1545
2200
  * Printed before the request, naming the label the credential will carry in
@@ -1590,7 +2245,14 @@ export const RUNTIME = {
1590
2245
  * 4. No URL longer than the line.
1591
2246
  */
1592
2247
  export const SYSTEM_MESSAGE = {
1593
- notConnected: "VibeCommit is installed but not connected. Run `vibecommit connect` to start capturing.",
2248
+ /**
2249
+ * State 1. The command is the channel's (`installChannel`): `vibecommit connect` on npm, `node <path> connect` for the plugin
2250
+ * bundle, which has no `vibecommit` on PATH (slice 2, design Next Steps 6, the mechanism state 2 already uses). On npm the line is
2251
+ * the same bytes it always was.
2252
+ */
2253
+ notConnected: (connectCommand) => `VibeCommit is installed but not connected. Run \`${connectCommand}\` to start capturing.`,
2254
+ /** State 1 when the channel's command would blow `credentialRejectedBudget` (a long plugin realpath). */
2255
+ notConnectedFallback: "VibeCommit is installed but not connected. Connect it: vibecommit.ai/install",
1594
2256
  /**
1595
2257
  * State 2 — `11A`. House prefix kept, ONE line, and the cause is no longer named: the server's 401
1596
2258
  * body is opaque on purpose (it cannot say "revoked" vs "expired"), so "revoked" was a guess that
@@ -1630,6 +2292,35 @@ export const SYSTEM_MESSAGE = {
1630
2292
  * users and its hash chain detects post-write tampering").
1631
2293
  */
1632
2294
  captureConfirmed: (dashboardUrl) => `VibeCommit captured this session. ${dashboardUrl}`,
2295
+ /**
2296
+ * States 6-9 — style guide §10.1 (0.7.0, design review D16 · 10A), all emitted by the `SessionStart` hook. One
2297
+ * plain-text line each, ≤120 characters, no SGR. The wording is the design doc's and the merged style guide's,
2298
+ * byte for byte; `test/session-start.test.ts` pins the length of every one.
2299
+ *
2300
+ * 6 — hooks added, once per clone. 7 — `core.hooksPath` is set, once per clone. 8 — the clone has its own
2301
+ * commit hook, once per clone. 9 — `grants.json` is unreadable, once per (machine, day).
2302
+ */
2303
+ hooksAdded: "VibeCommit added its post-commit and post-rewrite hooks to this clone, so commits can link to sessions.",
2304
+ hooksPathSet: "VibeCommit captures sessions here, not commits: core.hooksPath is set. Run `vibecommit status` here for the fix.",
2305
+ ownCommitHook: "VibeCommit didn't add its commit hook here: this clone has its own. Run `vibecommit connect` here to chain it.",
2306
+ grantsUnreadable: "VibeCommit capture stopped on this machine: ~/.vibecommit/grants.json can't be read. Run `vibecommit status`.",
2307
+ /**
2308
+ * State 10 — slice 2 (`CR-259`, brief §3 item 4). Emitted by the `SessionStart` hook, once per (machine, account epoch), when its one
2309
+ * bounded `GET` found account grants this machine has not named. SessionStart-only like states 6-9 (the plugin registers no
2310
+ * SessionStart), so it names the CLI by its own name, in backticks as 7-9 do; ≤120 characters.
2311
+ *
2312
+ * The counts are of what `status` lists under "on account", split: `owners` are the account's owner grants this machine has not named
2313
+ * (`octocat/`), `repos` its repository grants not named here, not under a named owner, and not turned off. The forms: "2 repositories",
2314
+ * "1 owner and 3 repositories", "2 owners"; the singulars are "1 repository" and "1 owner".
2315
+ */
2316
+ accountGrantsNotHere: (owners, repos) => {
2317
+ const o = `${owners} ${owners === 1 ? "owner" : "owners"}`;
2318
+ const r = `${repos} ${repos === 1 ? "repository" : "repositories"}`;
2319
+ const what = owners > 0 && repos > 0 ? `${o} and ${r}` : owners > 0 ? o : r;
2320
+ return `Your account grants ${what} not captured on this machine yet. Run \`vibecommit status\`.`;
2321
+ },
2322
+ /** State 10 when the counts would blow the ≤120 budget (an absurd number of grants): no counts, the same command. */
2323
+ accountGrantsNotHereFallback: "Your account grants repositories not captured on this machine yet. Run `vibecommit status`.",
1633
2324
  };
1634
2325
  /**
1635
2326
  * The out-of-tree redaction marker — `CR-024d`, D57 §OV8d.
@@ -1660,6 +2351,10 @@ export const REDACTION = {
1660
2351
  /** Commands the copy points at. One definition, so a rename cannot half-land. */
1661
2352
  export const COMMANDS = {
1662
2353
  connect: "vibecommit connect",
2354
+ /** `D33 · 27A`. */
2355
+ scan: "vibecommit scan",
2356
+ /** `D14`/`D15`. */
2357
+ backfill: "vibecommit backfill",
1663
2358
  /**
1664
2359
  * `CR-216/U2`. The only verb a credential FAULT's fix line may name — it can
1665
2360
  * replace the credential from any state. `CR-226` lets `connect` mint one
@@ -1680,6 +2375,10 @@ export const COMMANDS = {
1680
2375
  /** The ref form, for the detached-HEAD refusal. */
1681
2376
  reportRef: "vibecommit report --since 2026-07-08 --ref main",
1682
2377
  off: "vibecommit off",
2378
+ /** The picker's screen-reader flag: a FLAG on `connect`, like `signIn`. */
2379
+ connectTyped: "vibecommit connect --typed",
2380
+ /** The git hook's own verb. `hooksPathFixManual` tells a user to add it to a hook they keep. */
2381
+ postCommit: "vibecommit post-commit",
1683
2382
  /**
1684
2383
  * `CR-086`. The ARGUMENT SHAPE, shown once so the usage error and the retry
1685
2384
  * fix line cannot drift apart. There is no argument parser in this package —
@@ -1690,6 +2389,8 @@ export const COMMANDS = {
1690
2389
  */
1691
2390
  why: "vibecommit why src/auth/session.ts:47",
1692
2391
  chmodCredentials: "chmod 600 ~/.vibecommit/credentials.json",
2392
+ /** The folder's remedy, for `CREDENTIAL_FILE_INSECURE` when the folder is the one with the wrong mode. */
2393
+ chmodDirectory: "chmod 700 ~/.vibecommit",
1693
2394
  /**
1694
2395
  * ⛔ THE INSTALL COMMAND, TRANSCRIBED FROM `claude-plugin/README.md:12` — and
1695
2396
  * it is deliberately NOT an invented update verb (`U1`, 2026-08-30).
@@ -1707,6 +2408,8 @@ export const COMMANDS = {
1707
2408
  * together — the wording and the command are one unit.
1708
2409
  */
1709
2410
  pluginInstall: "/plugin install vibecommit@vibecommit-capture",
2411
+ /** The npm channel's way to a newer client: the package's own documented install, at `latest`. */
2412
+ npmUpdate: "npm install -g @vibe-commit/capture@latest",
1710
2413
  };
1711
2414
  /**
1712
2415
  * Every URL the CLI prints. Here rather than inline for the same reason as the
@@ -1730,6 +2433,8 @@ export const URLS = {
1730
2433
  * deepening this constant is that task's, not a later guess.
1731
2434
  */
1732
2435
  commits: "https://vibecommit.ai/app/commits",
2436
+ /** `/app/conversations`: where an uploaded backfill lands (D19). `/app` (`dashboard`) is the ending after a no. */
2437
+ conversations: "https://vibecommit.ai/app/conversations",
1733
2438
  /**
1734
2439
  * One recorded session — the ending's link when the ingest 2xx carried a `conversation_id` (`DX D8`).
1735
2440
  * Verified by file: `vibecommit-web` `app/app/conversations/[id]/page.tsx` exists on `main`, and an