@vibe-commit/capture 0.7.0 → 0.9.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 (152) hide show
  1. package/README.md +238 -7
  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/agents/claude_code.js +13 -0
  6. package/dist/agents/claude_code.js.map +1 -1
  7. package/dist/agents/codex_cli.js +11 -0
  8. package/dist/agents/codex_cli.js.map +1 -1
  9. package/dist/agents/cursor.js +7 -1
  10. package/dist/agents/cursor.js.map +1 -1
  11. package/dist/agents/registry.d.ts +7 -0
  12. package/dist/agents/registry.js +9 -0
  13. package/dist/agents/registry.js.map +1 -1
  14. package/dist/agents/types.d.ts +32 -0
  15. package/dist/agents/types.js.map +1 -1
  16. package/dist/bounded_id.d.ts +29 -0
  17. package/dist/bounded_id.js +42 -0
  18. package/dist/bounded_id.js.map +1 -0
  19. package/dist/codex_folders.d.ts +340 -0
  20. package/dist/codex_folders.js +1448 -0
  21. package/dist/codex_folders.js.map +1 -0
  22. package/dist/commands/auth.d.ts +7 -0
  23. package/dist/commands/auth.js +21 -0
  24. package/dist/commands/auth.js.map +1 -1
  25. package/dist/commands/backfill.d.ts +29 -7
  26. package/dist/commands/backfill.js +387 -109
  27. package/dist/commands/backfill.js.map +1 -1
  28. package/dist/commands/browser_credential.d.ts +4 -2
  29. package/dist/commands/browser_credential.js +5 -2
  30. package/dist/commands/browser_credential.js.map +1 -1
  31. package/dist/commands/connect.d.ts +10 -1
  32. package/dist/commands/connect.js +14 -9
  33. package/dist/commands/connect.js.map +1 -1
  34. package/dist/commands/connect_account.d.ts +135 -0
  35. package/dist/commands/connect_account.js +321 -0
  36. package/dist/commands/connect_account.js.map +1 -0
  37. package/dist/commands/connect_picker.js +204 -29
  38. package/dist/commands/connect_picker.js.map +1 -1
  39. package/dist/commands/device_credential.d.ts +3 -1
  40. package/dist/commands/device_credential.js +1 -0
  41. package/dist/commands/device_credential.js.map +1 -1
  42. package/dist/commands/finalize.d.ts +67 -0
  43. package/dist/commands/finalize.js +450 -114
  44. package/dist/commands/finalize.js.map +1 -1
  45. package/dist/commands/finish_mint.d.ts +28 -0
  46. package/dist/commands/finish_mint.js +16 -4
  47. package/dist/commands/finish_mint.js.map +1 -1
  48. package/dist/commands/scan.d.ts +28 -0
  49. package/dist/commands/scan.js +11 -0
  50. package/dist/commands/scan.js.map +1 -1
  51. package/dist/commands/status.d.ts +19 -1
  52. package/dist/commands/status.js +123 -14
  53. package/dist/commands/status.js.map +1 -1
  54. package/dist/commands/status_machine.d.ts +12 -3
  55. package/dist/commands/status_machine.js +111 -13
  56. package/dist/commands/status_machine.js.map +1 -1
  57. package/dist/commands/sync_reason.d.ts +17 -0
  58. package/dist/commands/sync_reason.js +25 -0
  59. package/dist/commands/sync_reason.js.map +1 -0
  60. package/dist/consent.d.ts +131 -3
  61. package/dist/consent.js +248 -14
  62. package/dist/consent.js.map +1 -1
  63. package/dist/copy/strings.d.ts +165 -4
  64. package/dist/copy/strings.js +190 -7
  65. package/dist/copy/strings.js.map +1 -1
  66. package/dist/folder_grants.d.ts +44 -0
  67. package/dist/folder_grants.js +101 -0
  68. package/dist/folder_grants.js.map +1 -0
  69. package/dist/git.d.ts +26 -4
  70. package/dist/git.js +82 -11
  71. package/dist/git.js.map +1 -1
  72. package/dist/grants_sync.d.ts +165 -0
  73. package/dist/grants_sync.js +289 -0
  74. package/dist/grants_sync.js.map +1 -0
  75. package/dist/grants_verb.d.ts +70 -0
  76. package/dist/grants_verb.js +121 -0
  77. package/dist/grants_verb.js.map +1 -0
  78. package/dist/hooks/entry.d.ts +30 -50
  79. package/dist/hooks/entry.js +601 -561
  80. package/dist/hooks/entry.js.map +1 -1
  81. package/dist/hooks/fire_lock.d.ts +31 -0
  82. package/dist/hooks/fire_lock.js +120 -12
  83. package/dist/hooks/fire_lock.js.map +1 -1
  84. package/dist/hooks/session_grants.d.ts +59 -0
  85. package/dist/hooks/session_grants.js +91 -0
  86. package/dist/hooks/session_grants.js.map +1 -0
  87. package/dist/hooks/session_start.d.ts +28 -7
  88. package/dist/hooks/session_start.js +55 -11
  89. package/dist/hooks/session_start.js.map +1 -1
  90. package/dist/index.js +22 -5
  91. package/dist/index.js.map +1 -1
  92. package/dist/machine_state.d.ts +44 -1
  93. package/dist/machine_state.js +75 -12
  94. package/dist/machine_state.js.map +1 -1
  95. package/dist/oauth/discovery.js +2 -1
  96. package/dist/oauth/discovery.js.map +1 -1
  97. package/dist/oauth/pkce.js +2 -1
  98. package/dist/oauth/pkce.js.map +1 -1
  99. package/dist/oauth/retire.d.ts +10 -0
  100. package/dist/oauth/retire.js +13 -0
  101. package/dist/oauth/retire.js.map +1 -1
  102. package/dist/paths.d.ts +54 -1
  103. package/dist/paths.js +88 -12
  104. package/dist/paths.js.map +1 -1
  105. package/dist/pending_revoke.d.ts +117 -0
  106. package/dist/pending_revoke.js +233 -0
  107. package/dist/pending_revoke.js.map +1 -0
  108. package/dist/pending_revoke_file.d.ts +55 -0
  109. package/dist/pending_revoke_file.js +74 -0
  110. package/dist/pending_revoke_file.js.map +1 -0
  111. package/dist/physical_path.d.ts +46 -0
  112. package/dist/physical_path.js +151 -0
  113. package/dist/physical_path.js.map +1 -0
  114. package/dist/picker.d.ts +82 -5
  115. package/dist/picker.js +183 -10
  116. package/dist/picker.js.map +1 -1
  117. package/dist/picker_checklist.d.ts +27 -3
  118. package/dist/picker_checklist.js +81 -6
  119. package/dist/picker_checklist.js.map +1 -1
  120. package/dist/policy.d.ts +24 -1
  121. package/dist/policy.js +30 -2
  122. package/dist/policy.js.map +1 -1
  123. package/dist/post.d.ts +55 -2
  124. package/dist/post.js +72 -22
  125. package/dist/post.js.map +1 -1
  126. package/dist/project.d.ts +89 -0
  127. package/dist/project.js +123 -0
  128. package/dist/project.js.map +1 -1
  129. package/dist/redact.d.ts +77 -2
  130. package/dist/redact.js +713 -47
  131. package/dist/redact.js.map +1 -1
  132. package/dist/session_targets.d.ts +713 -0
  133. package/dist/session_targets.js +2602 -0
  134. package/dist/session_targets.js.map +1 -0
  135. package/dist/spool.d.ts +12 -0
  136. package/dist/spool.js +29 -1
  137. package/dist/spool.js.map +1 -1
  138. package/dist/spool_union.d.ts +51 -0
  139. package/dist/spool_union.js +57 -0
  140. package/dist/spool_union.js.map +1 -0
  141. package/dist/state.d.ts +5 -0
  142. package/dist/state.js +19 -1
  143. package/dist/state.js.map +1 -1
  144. package/dist/system_message.d.ts +25 -2
  145. package/dist/system_message.js +29 -5
  146. package/dist/system_message.js.map +1 -1
  147. package/dist/text_tail.d.ts +8 -0
  148. package/dist/text_tail.js +19 -0
  149. package/dist/text_tail.js.map +1 -0
  150. package/dist/version.d.ts +1 -1
  151. package/dist/version.js +1 -1
  152. package/package.json +3 -3
@@ -22,7 +22,7 @@ export declare const HELP: {
22
22
  readonly tagline: "Record Claude Code sessions against the commits they produced.";
23
23
  readonly commands: {
24
24
  readonly 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.";
25
- readonly connect: "Choose the repositories to capture, from any folder, and connect them. On a machine with no credential it signs in first.";
25
+ readonly 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.";
26
26
  readonly scan: "List the repositories coding agents have been used in on this machine. Uploads nothing and changes no setting.";
27
27
  readonly backfill: "Upload earlier coding-agent sessions still on this machine, for the repositories you chose. Asks first; no commit links.";
28
28
  readonly status: "Show whether capture is on for this repo, or for this machine outside a repo, and when it last succeeded.";
@@ -144,11 +144,47 @@ export declare const STATUS: {
144
144
  readonly basisOwner: (owner: string) => string;
145
145
  readonly basisRepo: "repo grant";
146
146
  readonly basisClone: "per clone";
147
+ /**
148
+ * 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
149
+ * its own (R3-22): naming it here is `connect`'s picker, and the action row already offers `connect`.
150
+ */
151
+ readonly basisAccountOnly: "granted on your account, not on this machine";
152
+ /**
153
+ * Slice 2 (S4), brief §3.3: the machine screen's account rows, after `off` and before `held`. `on account` lists what the
154
+ * user's account grants that THIS machine has not named (an owner as `owner/`); the second line says what to do about it.
155
+ */
156
+ readonly accountLabel: "on account";
157
+ readonly accountNotHere: (connect: string) => string;
158
+ /** 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. */
159
+ readonly pendingLabel: "pending";
160
+ readonly pendingValue: (n: number) => string;
161
+ /**
162
+ * VQ's ruling (ii): a queued change the server refused with 400 is kept (a consent statement is never dropped) but a retry
163
+ * fails again, so this row does not say "the next command retries". The URL is the one the web app's error state uses.
164
+ */
165
+ readonly pendingRejectedValue: (n: number) => string;
166
+ /**
167
+ * 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).
168
+ * 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.
169
+ */
170
+ readonly accountSaving: (namesOnePerLine: string) => string;
147
171
  /** The machine-level screen (any folder that is not a work tree). */
148
172
  readonly offMachine: "Capture is not on for any repository on this machine.";
149
173
  readonly ownersLabel: "owners";
150
174
  readonly reposLabel: "repos";
151
175
  readonly offLabel: "off";
176
+ /** Folders outside every repository whose repo-free sessions are captured (R3). */
177
+ readonly foldersLabel: "folders";
178
+ /** A per-clone consent whose clone has no remote, or is gone: it is counted, so it is listed (Q3). */
179
+ readonly noRemoteClone: (path: string) => string;
180
+ readonly missingClone: (path: string) => string;
181
+ /** Repos and folders recent sessions named that nobody has chosen or turned off (R8 · D11): a list, never a hook line. */
182
+ /** A granted folder whose repository another member's machine uploaded first (C3): named here because the hook says nothing. */
183
+ readonly refusedFolderLabel: "refused";
184
+ readonly refusedFolder: (path: string) => string;
185
+ readonly refusedFolderWhy: "a folder at the same path was uploaded first from another member's machine, so this folder's sessions are not captured here";
186
+ readonly undecidedLabel: "not chosen";
187
+ readonly undecidedHint: (connect: string) => string;
152
188
  /**
153
189
  * Machine-level `status`: the repos whose own last upload was refused because their org has not approved capture. §10.2's held
154
190
  * vocabulary — the beat word `held`, and the backfill screen's "the org must approve capture" (Alexander, 2026-10-24 17:20).
@@ -328,7 +364,7 @@ export declare const CONNECT: {
328
364
  * ONE sentence, not a paragraph, added on the picker path only (design doc step 2, "The disclosure"): what the grant
329
365
  * covers beyond the repos named on screen. The four paragraphs under either heading stay byte-identical.
330
366
  */
331
- readonly consentScope: "The choice covers future clones of these repositories too, and adds commit hooks to each clone where it can.";
367
+ readonly 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.";
332
368
  /**
333
369
  * ⚠ "private-repo paths" IS THE APPROVED WORDING, and it is the reason
334
370
  * `private` is deliberately NOT registered in `claims.ts`: a substring gate on
@@ -849,6 +885,18 @@ export declare const BACKFILL: {
849
885
  readonly held: (count: number) => string;
850
886
  readonly failed: (count: number, reason: string) => string;
851
887
  readonly stopped: "stopped";
888
+ /**
889
+ * PARENT-FOLDER sessions (R4): sessions run from a folder outside every repository. Their repositories are not known until the
890
+ * transcript is read, which happens only after the yes, so the offer says what will be CHECKED and what the check can find.
891
+ */
892
+ readonly parentHeading: (sessions: number) => string;
893
+ readonly parentNote: "Each is read after you say yes, to find the repositories you chose that it touched, and the folder if you chose one. A session that touched none of them is skipped.";
894
+ readonly parentRowCounts: (sessions: number) => string;
895
+ /** The run line's label for the group: there is no repository to name. */
896
+ readonly parentLabel: "outside a repository";
897
+ readonly parentTouchedNothing: "touched nothing you chose";
898
+ readonly parentBusy: "another process was writing it";
899
+ readonly parentFolderRefused: "the folder's repository belongs to someone else";
852
900
  /** Skip reasons. */
853
901
  readonly overSize: "over 50 MiB";
854
902
  readonly folderDiffers: "folder differs";
@@ -912,6 +960,24 @@ export declare const PICKER: {
912
960
  readonly reasonOwner: "owner";
913
961
  readonly reasonRepo: "repo";
914
962
  readonly reasonPerClone: "per clone";
963
+ readonly reasonFolder: "folder";
964
+ /** The `Folders` group (R3): folders outside every repository that hold sessions. */
965
+ readonly folders: "folders outside a repository";
966
+ readonly moreFoldersTyped: (count: number) => string;
967
+ readonly moreFoldersChecklist: (count: number) => string;
968
+ readonly labelFolders: "folders";
969
+ /** The folder grant line (R7 · D9): the one claim about who can read a folder's sessions. */
970
+ readonly folderPrivate: "(private to you)";
971
+ readonly questionGrantFolders: "Upload transcripts from these folders?";
972
+ readonly questionGrantMixed: "Upload transcripts from these repositories and folders?";
973
+ readonly questionStopFolders: "Stop capturing these folders?";
974
+ readonly questionStopMixed: "Stop capturing these repositories and folders?";
975
+ /** After a folder grant (R3 · R7), once, under "Capture is on": which folders, and who can read what they capture. */
976
+ readonly onFolders: (names: string) => string;
977
+ /** `agents` names who a folder grant captures, built from the registry predicate (`folderCaptureAgents`), never typed here (C34). */
978
+ readonly onFoldersProof: (agents: string) => string;
979
+ readonly stoppedFolders: "Stopped capturing the folders above.";
980
+ readonly stoppedMixed: "Stopped capturing the repositories and folders above.";
915
981
  /** The owner row ALWAYS states how many repos `aN` covers (D9), so a hidden repo is never covered unseen. */
916
982
  readonly ownerAll: (count: number) => string;
917
983
  readonly ownerOnReposHere: (count: number) => string;
@@ -958,7 +1024,7 @@ export declare const PICKER: {
958
1024
  readonly questionStop: "Stop capturing these repositories?";
959
1025
  readonly stopped: "Stopped capturing the repositories above.";
960
1026
  /** "1 owner and 2 repos" — the on-now summary of Enter-with-grants, `auth`'s one line and the status heading. */
961
- readonly summary: (owners: number, repos: number) => string;
1027
+ readonly summary: (owners: number, repos: number, folders?: number) => string;
962
1028
  readonly authStays: (summary: string) => string;
963
1029
  /** The hooks step (D13 · 7A). */
964
1030
  readonly hooksBeat: (clones: number) => string;
@@ -985,6 +1051,70 @@ export declare const PICKER: {
985
1051
  readonly ownerApproval: (status: string) => string;
986
1052
  readonly onNewLaptop: (names: string) => string;
987
1053
  readonly onNewLaptopProof: "No clone of it is here yet. Clone one and open a coding agent in it; that session is captured.";
1054
+ /** The group of account grants this machine has not named (§2): numbered, unticked until chosen, like any other row. */
1055
+ readonly onAccount: "on your account";
1056
+ readonly onAccountHint: "choose it to capture it here";
1057
+ /** A `turn on` row whose repo was turned off on the ACCOUNT (not only here): the yes re-includes it everywhere. */
1058
+ readonly turnedOffOnAccount: "(turned off on your account)";
1059
+ /** Screen 1a: the second line of the prompt block, on a machine with no credential, a terminal and a browser to sign in with. */
1060
+ readonly gLine: "Or type g for the repositories your account already grants. It signs in first.";
1061
+ /** The one token a mixed answer is refused for; `notUnderstood` wraps it in D8's line. */
1062
+ readonly gMixedToken: "g can't be combined with other choices";
1063
+ /** Screen 1b: the grant block's heading when the list is the account's, not the person's picks. */
1064
+ readonly accountGrants: "Your account grants:";
1065
+ /** Screen 1b, N: the credential `g` signed in with is revoked. The removed-here variant is a revoke that could not reach the server. */
1066
+ readonly gDeclinedRevoked: "Nothing is captured on this machine. The credential made for this was revoked.";
1067
+ readonly 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.";
1068
+ /** Ctrl+C after a mint (VQ's correction): never "Nothing was changed", because a credential was made and then removed. */
1069
+ readonly gCancelledRevoked: "Cancelled. Nothing is captured on this machine. The credential made for this was revoked.";
1070
+ readonly 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.";
1071
+ /**
1072
+ * `g` stopped because this machine could not make the write-ahead record of the credential it had just been given (an unwritable,
1073
+ * unreadable or full queue). The credential is revoked at once (`REPLACE.declinedRevoked`), or `REPLACE.declinedOrphan` says where
1074
+ * to remove it. An insecure queue file gets the insecure-file block instead of this line.
1075
+ */
1076
+ readonly gNotRecorded: "g stopped: this machine couldn't keep a record of the credential it was just given.";
1077
+ /**
1078
+ * The yes found `g`'s own record of the credential UNUSABLE (the queue file became insecure or unreadable while the question was
1079
+ * open): nothing was revoked by anyone, this machine just cannot keep the record, so no credential is kept.
1080
+ */
1081
+ readonly gRecordUnusable: (connect: string) => string;
1082
+ /**
1083
+ * The yes found a `credentials.json` already there (another run saved one while the question was open): that one is kept, this run's
1084
+ * is not, and the grants the yes wrote are saved — capture IS on, so this never says nothing is captured. The fate of the
1085
+ * credential this run made follows on its own line (`REPLACE.declinedRevoked`, `gRemovedWillTell`, or `REPLACE.declinedOrphan`).
1086
+ */
1087
+ readonly gOccupied: (connect: string) => string;
1088
+ /** The fate line for `gOccupied` when a readable row still holds the credential this run made. */
1089
+ readonly gRemovedWillTell: "The credential just created was removed here; the server will be told on the next vibecommit command that contacts the server.";
1090
+ /**
1091
+ * The yes found `g`'s own record of the credential gone: another vibecommit command (a retry that ran while the question was open)
1092
+ * had already revoked it. No credential is kept, and the grants the yes wrote stay. `connect` is THIS install's command.
1093
+ */
1094
+ readonly gRevokedElsewhere: (connect: string) => string;
1095
+ /** Screen 1c: `g` found no grants on the account. The credential is kept and the person chooses here. */
1096
+ readonly gEmpty: "Your account doesn't grant any repositories yet. Choose them here instead:";
1097
+ /**
1098
+ * `g` signed in but the account's grants could not be read (the server did not answer, or answered off-contract). It must not
1099
+ * say the account is empty, and it says what to do: run `connect` again. `connect` is THIS install's command (a function of it).
1100
+ */
1101
+ readonly gUnavailable: (connect: string) => string;
1102
+ /** Screen 2: a machine joining an account by picking local repos is asked about the rest of the account's grants. */
1103
+ readonly joinAlso: (namesOnePerLine: string) => string;
1104
+ readonly joinQuestion: "Capture those here too?";
1105
+ /**
1106
+ * Under "Capture is on" when the sync after `connect`'s writes did not reach the server (VQ, on VT's question). Capture IS on —
1107
+ * local grants admit the repo — so this is not a warning; it only tells the truth about the account. Only `connect`, `status` and
1108
+ * `off` sync, so it names `status` (the install channel's command); a 400 and a 401 have their own lines (below and `ERRORS`).
1109
+ */
1110
+ readonly accountNotTold: (status: string) => string;
1111
+ /**
1112
+ * "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
1113
+ * holds the repository's sessions on this machine (`isCaptureAllowed`, step 3), so "Capture is on" needs the truth about it.
1114
+ * A machine that has never synced holds nothing and keeps `accountNotTold`.
1115
+ */
1116
+ readonly accountNotToldHeld: (status: string) => string;
1117
+ readonly joinDeclined: (status: string) => string;
988
1118
  };
989
1119
  /** `vibecommit off`. */
990
1120
  export declare const OFF: {
@@ -1002,6 +1132,18 @@ export declare const OFF: {
1002
1132
  readonly grantsUnreadableWhy: "Capture is already stopped for this repository while the file can't be read, and this clone's own consent was withdrawn.";
1003
1133
  readonly grantsUnreadableFixLabel: "Fix the file or move it aside, then run:";
1004
1134
  readonly alreadyOff: "Capture was already off for this repository.";
1135
+ /**
1136
+ * Slice 2 (S4), brief §2 and §5. On a machine with a credential the exclusion is also queued for the account and synced
1137
+ * (3 s), so the success line says where else it reaches; `unsyncedOthers` says who keeps capturing until they sync; and
1138
+ * `notSentYet` is the design's sentence for a write that could not reach the server (it stays queued).
1139
+ */
1140
+ readonly doneSlugAccount: (slug: string) => string;
1141
+ readonly unsyncedOthers: (n: number) => string;
1142
+ readonly notSentYet: "Stopped here. Your other machines running VibeCommit 0.8.0 or later don't know yet; it will retry.";
1143
+ /** 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. */
1144
+ readonly notSentStopped: "Stopped here. Your other machines running VibeCommit 0.8.0 or later don't know yet.";
1145
+ /** The account already had it turned off (another machine, or the dashboard); this machine now agrees. */
1146
+ readonly alreadyOffAccount: "Already off on your account; now off on this machine too.";
1005
1147
  readonly note: "Sessions already recorded are unaffected.";
1006
1148
  readonly unrecognizedArgWhat: "That is not something `vibecommit off` understands.";
1007
1149
  readonly unrecognizedArgWhy: "Nothing was changed. `off` takes no arguments.";
@@ -2030,7 +2172,14 @@ export declare const RUNTIME: {
2030
2172
  * 4. No URL longer than the line.
2031
2173
  */
2032
2174
  export declare const SYSTEM_MESSAGE: {
2033
- readonly notConnected: "VibeCommit is installed but not connected. Run `vibecommit connect` to start capturing.";
2175
+ /**
2176
+ * State 1. The command is the channel's (`installChannel`): `vibecommit connect` on npm, `node <path> connect` for the plugin
2177
+ * bundle, which has no `vibecommit` on PATH (slice 2, design Next Steps 6, the mechanism state 2 already uses). On npm the line is
2178
+ * the same bytes it always was.
2179
+ */
2180
+ readonly notConnected: (connectCommand: string) => string;
2181
+ /** State 1 when the channel's command would blow `credentialRejectedBudget` (a long plugin realpath). */
2182
+ readonly notConnectedFallback: "VibeCommit is installed but not connected. Connect it: vibecommit.ai/install";
2034
2183
  /**
2035
2184
  * State 2 — `11A`. House prefix kept, ONE line, and the cause is no longer named: the server's 401
2036
2185
  * body is opaque on purpose (it cannot say "revoked" vs "expired"), so "revoked" was a guess that
@@ -2082,6 +2231,18 @@ export declare const SYSTEM_MESSAGE: {
2082
2231
  readonly hooksPathSet: "VibeCommit captures sessions here, not commits: core.hooksPath is set. Run `vibecommit status` here for the fix.";
2083
2232
  readonly ownCommitHook: "VibeCommit didn't add its commit hook here: this clone has its own. Run `vibecommit connect` here to chain it.";
2084
2233
  readonly grantsUnreadable: "VibeCommit capture stopped on this machine: ~/.vibecommit/grants.json can't be read. Run `vibecommit status`.";
2234
+ /**
2235
+ * State 10 — slice 2 (`CR-259`, brief §3 item 4). Emitted by the `SessionStart` hook, once per (machine, account epoch), when its one
2236
+ * bounded `GET` found account grants this machine has not named. SessionStart-only like states 6-9 (the plugin registers no
2237
+ * SessionStart), so it names the CLI by its own name, in backticks as 7-9 do; ≤120 characters.
2238
+ *
2239
+ * The counts are of what `status` lists under "on account", split: `owners` are the account's owner grants this machine has not named
2240
+ * (`octocat/`), `repos` its repository grants not named here, not under a named owner, and not turned off. The forms: "2 repositories",
2241
+ * "1 owner and 3 repositories", "2 owners"; the singulars are "1 repository" and "1 owner".
2242
+ */
2243
+ readonly accountGrantsNotHere: (owners: number, repos: number) => string;
2244
+ /** State 10 when the counts would blow the ≤120 budget (an absurd number of grants): no counts, the same command. */
2245
+ readonly accountGrantsNotHereFallback: "Your account grants repositories not captured on this machine yet. Run `vibecommit status`.";
2085
2246
  };
2086
2247
  /**
2087
2248
  * The out-of-tree redaction marker — `CR-024d`, D57 §OV8d.
@@ -33,7 +33,7 @@ export const HELP = {
33
33
  // nothing yet. With `--stdin` or no terminal it saves the credential and stops there.
34
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
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.",
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
37
  // `D33 · 27A`. A public verb so an AI agent can list capturable repos without entering a consent
38
38
  // flow. It opens no session file and changes no setting, which is what lets it be run before `connect`.
39
39
  // ⚠ NOT "changes nothing" (PR 1 F1): opening Codex's index read-only makes SQLite create its own
@@ -238,11 +238,50 @@ export const STATUS = {
238
238
  basisOwner: (owner) => `owner grant ${owner}/`,
239
239
  basisRepo: "repo grant",
240
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
+ },
241
268
  /** The machine-level screen (any folder that is not a work tree). */
242
269
  offMachine: "Capture is not on for any repository on this machine.",
243
270
  ownersLabel: "owners",
244
271
  reposLabel: "repos",
245
272
  offLabel: "off",
273
+ /** Folders outside every repository whose repo-free sessions are captured (R3). */
274
+ foldersLabel: "folders",
275
+ /** A per-clone consent whose clone has no remote, or is gone: it is counted, so it is listed (Q3). */
276
+ noRemoteClone: (path) => `${path} (no remote)`,
277
+ missingClone: (path) => `${path} (clone not found)`,
278
+ /** Repos and folders recent sessions named that nobody has chosen or turned off (R8 · D11): a list, never a hook line. */
279
+ /** A granted folder whose repository another member's machine uploaded first (C3): named here because the hook says nothing. */
280
+ refusedFolderLabel: "refused",
281
+ refusedFolder: (path) => `${path} (not captured)`,
282
+ refusedFolderWhy: "a folder at the same path was uploaded first from another member's machine, so this folder's sessions are not captured here",
283
+ undecidedLabel: "not chosen",
284
+ undecidedHint: (connect) => `recent sessions touched these and nothing was captured · to choose, run: ${connect}`,
246
285
  /**
247
286
  * Machine-level `status`: the repos whose own last upload was refused because their org has not approved capture. §10.2's held
248
287
  * vocabulary — the beat word `held`, and the backfill screen's "the org must approve capture" (Alexander, 2026-10-24 17:20).
@@ -469,7 +508,7 @@ export const CONNECT = {
469
508
  * ONE sentence, not a paragraph, added on the picker path only (design doc step 2, "The disclosure"): what the grant
470
509
  * covers beyond the repos named on screen. The four paragraphs under either heading stay byte-identical.
471
510
  */
472
- consentScope: "The choice covers future clones of these repositories too, and adds commit hooks to each clone where it can.",
511
+ 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.",
473
512
  /**
474
513
  * ⚠ "private-repo paths" IS THE APPROVED WORDING, and it is the reason
475
514
  * `private` is deliberately NOT registered in `claims.ts`: a substring gate on
@@ -1014,6 +1053,18 @@ export const BACKFILL = {
1014
1053
  held: (count) => `${count} held, the org must approve capture`,
1015
1054
  failed: (count, reason) => `${count} failed, ${reason}`,
1016
1055
  stopped: "stopped",
1056
+ /**
1057
+ * PARENT-FOLDER sessions (R4): sessions run from a folder outside every repository. Their repositories are not known until the
1058
+ * transcript is read, which happens only after the yes, so the offer says what will be CHECKED and what the check can find.
1059
+ */
1060
+ parentHeading: (sessions) => `${sessions} ${sessions === 1 ? "session was" : "sessions were"} run from folders outside a repository.`,
1061
+ parentNote: "Each is read after you say yes, to find the repositories you chose that it touched, and the folder if you chose one. A session that touched none of them is skipped.",
1062
+ parentRowCounts: (sessions) => `${sessions} to check`,
1063
+ /** The run line's label for the group: there is no repository to name. */
1064
+ parentLabel: "outside a repository",
1065
+ parentTouchedNothing: "touched nothing you chose",
1066
+ parentBusy: "another process was writing it",
1067
+ parentFolderRefused: "the folder's repository belongs to someone else",
1017
1068
  /** Skip reasons. */
1018
1069
  overSize: "over 50 MiB",
1019
1070
  folderDiffers: "folder differs",
@@ -1085,6 +1136,24 @@ export const PICKER = {
1085
1136
  reasonOwner: "owner",
1086
1137
  reasonRepo: "repo",
1087
1138
  reasonPerClone: "per clone",
1139
+ reasonFolder: "folder",
1140
+ /** The `Folders` group (R3): folders outside every repository that hold sessions. */
1141
+ folders: "folders outside a repository",
1142
+ moreFoldersTyped: (count) => `${count} more ${count === 1 ? "folder" : "folders"} · type more to list them`,
1143
+ moreFoldersChecklist: (count) => `${count} more ${count === 1 ? "folder" : "folders"}`,
1144
+ labelFolders: "folders",
1145
+ /** The folder grant line (R7 · D9): the one claim about who can read a folder's sessions. */
1146
+ folderPrivate: "(private to you)",
1147
+ questionGrantFolders: "Upload transcripts from these folders?",
1148
+ questionGrantMixed: "Upload transcripts from these repositories and folders?",
1149
+ questionStopFolders: "Stop capturing these folders?",
1150
+ questionStopMixed: "Stop capturing these repositories and folders?",
1151
+ /** After a folder grant (R3 · R7), once, under "Capture is on": which folders, and who can read what they capture. */
1152
+ onFolders: (names) => `Capture is on for the folders ${names} on this machine.`,
1153
+ /** `agents` names who a folder grant captures, built from the registry predicate (`folderCaptureAgents`), never typed here (C34). */
1154
+ onFoldersProof: (agents) => `A ${agents} session started there that touches no repository is captured when it ends. It is private to you: no one else in your organization can read it unless you share it. A session that touches a repository goes to that repository when it is on.`,
1155
+ stoppedFolders: "Stopped capturing the folders above.",
1156
+ stoppedMixed: "Stopped capturing the repositories and folders above.",
1088
1157
  /** The owner row ALWAYS states how many repos `aN` covers (D9), so a hidden repo is never covered unseen. */
1089
1158
  ownerAll: (count) => `all ${count} ${count === 1 ? "repo" : "repos"} here, including repos you clone later`,
1090
1159
  ownerOnReposHere: (count) => `${count} ${count === 1 ? "repo" : "repos"} here`,
@@ -1133,12 +1202,14 @@ export const PICKER = {
1133
1202
  questionStop: "Stop capturing these repositories?",
1134
1203
  stopped: "Stopped capturing the repositories above.",
1135
1204
  /** "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) => {
1205
+ summary: (owners, repos, folders = 0) => {
1137
1206
  const o = `${owners} ${owners === 1 ? "owner" : "owners"}`;
1138
1207
  const r = `${repos} ${repos === 1 ? "repo" : "repos"}`;
1139
- if (owners > 0 && repos > 0)
1140
- return `${o} and ${r}`;
1141
- return owners > 0 ? o : r;
1208
+ const base = owners > 0 && repos > 0 ? `${o} and ${r}` : owners > 0 ? o : repos > 0 || folders === 0 ? r : "";
1209
+ if (folders === 0)
1210
+ return base;
1211
+ const f = `${folders} ${folders === 1 ? "folder" : "folders"}`;
1212
+ return base === "" ? f : `${base} and ${f}`;
1142
1213
  },
1143
1214
  authStays: (summary) => `Capture stays on for ${summary} on this machine.`,
1144
1215
  /** The hooks step (D13 · 7A). */
@@ -1169,6 +1240,79 @@ export const PICKER = {
1169
1240
  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
1241
  onNewLaptop: (names) => `Capture is on for ${names} on this machine.`,
1171
1242
  onNewLaptopProof: "No clone of it is here yet. Clone one and open a coding agent in it; that session is captured.",
1243
+ // --- Slice 2 (S4 part 3, capture 0.8.0): the account's grants in `connect`. Brief §2-§3, VQ's rulings 2026-10-24. ---
1244
+ /** The group of account grants this machine has not named (§2): numbered, unticked until chosen, like any other row. */
1245
+ onAccount: "on your account",
1246
+ onAccountHint: "choose it to capture it here",
1247
+ /** A `turn on` row whose repo was turned off on the ACCOUNT (not only here): the yes re-includes it everywhere. */
1248
+ turnedOffOnAccount: "(turned off on your account)",
1249
+ /** Screen 1a: the second line of the prompt block, on a machine with no credential, a terminal and a browser to sign in with. */
1250
+ gLine: "Or type g for the repositories your account already grants. It signs in first.",
1251
+ /** The one token a mixed answer is refused for; `notUnderstood` wraps it in D8's line. */
1252
+ gMixedToken: "g can't be combined with other choices",
1253
+ /** Screen 1b: the grant block's heading when the list is the account's, not the person's picks. */
1254
+ accountGrants: "Your account grants:",
1255
+ /** Screen 1b, N: the credential `g` signed in with is revoked. The removed-here variant is a revoke that could not reach the server. */
1256
+ gDeclinedRevoked: "Nothing is captured on this machine. The credential made for this was revoked.",
1257
+ 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.",
1258
+ /** Ctrl+C after a mint (VQ's correction): never "Nothing was changed", because a credential was made and then removed. */
1259
+ gCancelledRevoked: "Cancelled. Nothing is captured on this machine. The credential made for this was revoked.",
1260
+ 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.",
1261
+ /**
1262
+ * `g` stopped because this machine could not make the write-ahead record of the credential it had just been given (an unwritable,
1263
+ * unreadable or full queue). The credential is revoked at once (`REPLACE.declinedRevoked`), or `REPLACE.declinedOrphan` says where
1264
+ * to remove it. An insecure queue file gets the insecure-file block instead of this line.
1265
+ */
1266
+ gNotRecorded: "g stopped: this machine couldn't keep a record of the credential it was just given.",
1267
+ /**
1268
+ * The yes found `g`'s own record of the credential UNUSABLE (the queue file became insecure or unreadable while the question was
1269
+ * open): nothing was revoked by anyone, this machine just cannot keep the record, so no credential is kept.
1270
+ */
1271
+ 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.`,
1272
+ /**
1273
+ * The yes found a `credentials.json` already there (another run saved one while the question was open): that one is kept, this run's
1274
+ * is not, and the grants the yes wrote are saved — capture IS on, so this never says nothing is captured. The fate of the
1275
+ * credential this run made follows on its own line (`REPLACE.declinedRevoked`, `gRemovedWillTell`, or `REPLACE.declinedOrphan`).
1276
+ */
1277
+ 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.`,
1278
+ /** The fate line for `gOccupied` when a readable row still holds the credential this run made. */
1279
+ gRemovedWillTell: "The credential just created was removed here; the server will be told on the next vibecommit command that contacts the server.",
1280
+ /**
1281
+ * The yes found `g`'s own record of the credential gone: another vibecommit command (a retry that ran while the question was open)
1282
+ * had already revoked it. No credential is kept, and the grants the yes wrote stay. `connect` is THIS install's command.
1283
+ */
1284
+ 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.`,
1285
+ /** Screen 1c: `g` found no grants on the account. The credential is kept and the person chooses here. */
1286
+ gEmpty: "Your account doesn't grant any repositories yet. Choose them here instead:",
1287
+ /**
1288
+ * `g` signed in but the account's grants could not be read (the server did not answer, or answered off-contract). It must not
1289
+ * say the account is empty, and it says what to do: run `connect` again. `connect` is THIS install's command (a function of it).
1290
+ */
1291
+ gUnavailable: (connect) => `The repositories your account grants couldn't be read right now. Run \`${connect}\` again to choose from them.`,
1292
+ /** Screen 2: a machine joining an account by picking local repos is asked about the rest of the account's grants. */
1293
+ joinAlso: (namesOnePerLine) => {
1294
+ const names = namesOnePerLine.split("\n");
1295
+ const head = names.length > 3
1296
+ ? `${names.slice(0, 2).join(", ")} and ${names.length - 2} more`
1297
+ : names.length > 1
1298
+ ? `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`
1299
+ : names[0];
1300
+ return `Your account also grants ${head}.`;
1301
+ },
1302
+ joinQuestion: "Capture those here too?",
1303
+ /**
1304
+ * Under "Capture is on" when the sync after `connect`'s writes did not reach the server (VQ, on VT's question). Capture IS on —
1305
+ * local grants admit the repo — so this is not a warning; it only tells the truth about the account. Only `connect`, `status` and
1306
+ * `off` sync, so it names `status` (the install channel's command); a 400 and a 401 have their own lines (below and `ERRORS`).
1307
+ */
1308
+ accountNotTold: (status) => `Your account hasn't been told yet; running \`${status}\` retries.`,
1309
+ /**
1310
+ * "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
1311
+ * holds the repository's sessions on this machine (`isCaptureAllowed`, step 3), so "Capture is on" needs the truth about it.
1312
+ * A machine that has never synced holds nothing and keeps `accountNotTold`.
1313
+ */
1314
+ 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.`,
1315
+ joinDeclined: (status) => `They stay granted on your account and aren't captured on this machine. ${status} lists them.`,
1172
1316
  };
1173
1317
  /** `vibecommit off`. */
1174
1318
  export const OFF = {
@@ -1186,6 +1330,21 @@ export const OFF = {
1186
1330
  grantsUnreadableWhy: "Capture is already stopped for this repository while the file can't be read, and this clone's own consent was withdrawn.",
1187
1331
  grantsUnreadableFixLabel: "Fix the file or move it aside, then run:",
1188
1332
  alreadyOff: "Capture was already off for this repository.",
1333
+ /**
1334
+ * Slice 2 (S4), brief §2 and §5. On a machine with a credential the exclusion is also queued for the account and synced
1335
+ * (3 s), so the success line says where else it reaches; `unsyncedOthers` says who keeps capturing until they sync; and
1336
+ * `notSentYet` is the design's sentence for a write that could not reach the server (it stays queued).
1337
+ */
1338
+ 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.`,
1339
+ // ⛔ 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).
1340
+ unsyncedOthers: (n) => n === 1
1341
+ ? "1 of your machines hasn't synced and keeps capturing it until it syncs on VibeCommit 0.8.0 or later."
1342
+ : `${n} of your machines haven't synced and keep capturing it until they sync on VibeCommit 0.8.0 or later.`,
1343
+ notSentYet: "Stopped here. Your other machines running VibeCommit 0.8.0 or later don't know yet; it will retry.",
1344
+ /** 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. */
1345
+ notSentStopped: "Stopped here. Your other machines running VibeCommit 0.8.0 or later don't know yet.",
1346
+ /** The account already had it turned off (another machine, or the dashboard); this machine now agrees. */
1347
+ alreadyOffAccount: "Already off on your account; now off on this machine too.",
1189
1348
  note: "Sessions already recorded are unaffected.",
1190
1349
  // `TODOS[184]`. `off` used to ignore `argv` entirely and always run — a stray
1191
1350
  // argument, including `--help`'s own flag landing here by a bug, must never
@@ -2130,7 +2289,14 @@ export const RUNTIME = {
2130
2289
  * 4. No URL longer than the line.
2131
2290
  */
2132
2291
  export const SYSTEM_MESSAGE = {
2133
- notConnected: "VibeCommit is installed but not connected. Run `vibecommit connect` to start capturing.",
2292
+ /**
2293
+ * State 1. The command is the channel's (`installChannel`): `vibecommit connect` on npm, `node <path> connect` for the plugin
2294
+ * bundle, which has no `vibecommit` on PATH (slice 2, design Next Steps 6, the mechanism state 2 already uses). On npm the line is
2295
+ * the same bytes it always was.
2296
+ */
2297
+ notConnected: (connectCommand) => `VibeCommit is installed but not connected. Run \`${connectCommand}\` to start capturing.`,
2298
+ /** State 1 when the channel's command would blow `credentialRejectedBudget` (a long plugin realpath). */
2299
+ notConnectedFallback: "VibeCommit is installed but not connected. Connect it: vibecommit.ai/install",
2134
2300
  /**
2135
2301
  * State 2 — `11A`. House prefix kept, ONE line, and the cause is no longer named: the server's 401
2136
2302
  * body is opaque on purpose (it cannot say "revoked" vs "expired"), so "revoked" was a guess that
@@ -2182,6 +2348,23 @@ export const SYSTEM_MESSAGE = {
2182
2348
  hooksPathSet: "VibeCommit captures sessions here, not commits: core.hooksPath is set. Run `vibecommit status` here for the fix.",
2183
2349
  ownCommitHook: "VibeCommit didn't add its commit hook here: this clone has its own. Run `vibecommit connect` here to chain it.",
2184
2350
  grantsUnreadable: "VibeCommit capture stopped on this machine: ~/.vibecommit/grants.json can't be read. Run `vibecommit status`.",
2351
+ /**
2352
+ * State 10 — slice 2 (`CR-259`, brief §3 item 4). Emitted by the `SessionStart` hook, once per (machine, account epoch), when its one
2353
+ * bounded `GET` found account grants this machine has not named. SessionStart-only like states 6-9 (the plugin registers no
2354
+ * SessionStart), so it names the CLI by its own name, in backticks as 7-9 do; ≤120 characters.
2355
+ *
2356
+ * The counts are of what `status` lists under "on account", split: `owners` are the account's owner grants this machine has not named
2357
+ * (`octocat/`), `repos` its repository grants not named here, not under a named owner, and not turned off. The forms: "2 repositories",
2358
+ * "1 owner and 3 repositories", "2 owners"; the singulars are "1 repository" and "1 owner".
2359
+ */
2360
+ accountGrantsNotHere: (owners, repos) => {
2361
+ const o = `${owners} ${owners === 1 ? "owner" : "owners"}`;
2362
+ const r = `${repos} ${repos === 1 ? "repository" : "repositories"}`;
2363
+ const what = owners > 0 && repos > 0 ? `${o} and ${r}` : owners > 0 ? o : r;
2364
+ return `Your account grants ${what} not captured on this machine yet. Run \`vibecommit status\`.`;
2365
+ },
2366
+ /** State 10 when the counts would blow the ≤120 budget (an absurd number of grants): no counts, the same command. */
2367
+ accountGrantsNotHereFallback: "Your account grants repositories not captured on this machine yet. Run `vibecommit status`.",
2185
2368
  };
2186
2369
  /**
2187
2370
  * The out-of-tree redaction marker — `CR-024d`, D57 §OV8d.