@vibe-commit/capture 0.1.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 (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/agents/claude_code.d.ts +11 -0
  4. package/dist/agents/claude_code.js +162 -0
  5. package/dist/agents/claude_code.js.map +1 -0
  6. package/dist/agents/codex_cli.d.ts +15 -0
  7. package/dist/agents/codex_cli.js +67 -0
  8. package/dist/agents/codex_cli.js.map +1 -0
  9. package/dist/agents/cursor.d.ts +21 -0
  10. package/dist/agents/cursor.js +89 -0
  11. package/dist/agents/cursor.js.map +1 -0
  12. package/dist/agents/registry.d.ts +184 -0
  13. package/dist/agents/registry.js +212 -0
  14. package/dist/agents/registry.js.map +1 -0
  15. package/dist/agents/types.d.ts +279 -0
  16. package/dist/agents/types.js +67 -0
  17. package/dist/agents/types.js.map +1 -0
  18. package/dist/commands/connect.d.ts +36 -0
  19. package/dist/commands/connect.js +499 -0
  20. package/dist/commands/connect.js.map +1 -0
  21. package/dist/commands/context.d.ts +45 -0
  22. package/dist/commands/context.js +14 -0
  23. package/dist/commands/context.js.map +1 -0
  24. package/dist/commands/report.d.ts +105 -0
  25. package/dist/commands/report.js +430 -0
  26. package/dist/commands/report.js.map +1 -0
  27. package/dist/commands/status.d.ts +29 -0
  28. package/dist/commands/status.js +236 -0
  29. package/dist/commands/status.js.map +1 -0
  30. package/dist/commands/why.d.ts +349 -0
  31. package/dist/commands/why.js +937 -0
  32. package/dist/commands/why.js.map +1 -0
  33. package/dist/consent.d.ts +59 -0
  34. package/dist/consent.js +111 -0
  35. package/dist/consent.js.map +1 -0
  36. package/dist/copy/claims.d.ts +79 -0
  37. package/dist/copy/claims.js +343 -0
  38. package/dist/copy/claims.js.map +1 -0
  39. package/dist/copy/grades.d.ts +117 -0
  40. package/dist/copy/grades.js +96 -0
  41. package/dist/copy/grades.js.map +1 -0
  42. package/dist/copy/index.d.ts +12 -0
  43. package/dist/copy/index.js +13 -0
  44. package/dist/copy/index.js.map +1 -0
  45. package/dist/copy/strings.d.ts +1328 -0
  46. package/dist/copy/strings.js +1243 -0
  47. package/dist/copy/strings.js.map +1 -0
  48. package/dist/copy/time.d.ts +27 -0
  49. package/dist/copy/time.js +43 -0
  50. package/dist/copy/time.js.map +1 -0
  51. package/dist/credential.d.ts +211 -0
  52. package/dist/credential.js +298 -0
  53. package/dist/credential.js.map +1 -0
  54. package/dist/exit.d.ts +59 -0
  55. package/dist/exit.js +60 -0
  56. package/dist/exit.js.map +1 -0
  57. package/dist/git.d.ts +299 -0
  58. package/dist/git.js +653 -0
  59. package/dist/git.js.map +1 -0
  60. package/dist/hooks/entry.d.ts +277 -0
  61. package/dist/hooks/entry.js +1092 -0
  62. package/dist/hooks/entry.js.map +1 -0
  63. package/dist/hooks/post_commit.d.ts +59 -0
  64. package/dist/hooks/post_commit.js +160 -0
  65. package/dist/hooks/post_commit.js.map +1 -0
  66. package/dist/hooks/post_rewrite.d.ts +59 -0
  67. package/dist/hooks/post_rewrite.js +161 -0
  68. package/dist/hooks/post_rewrite.js.map +1 -0
  69. package/dist/hooks/session_end.d.ts +101 -0
  70. package/dist/hooks/session_end.js +120 -0
  71. package/dist/hooks/session_end.js.map +1 -0
  72. package/dist/index.d.ts +34 -0
  73. package/dist/index.js +360 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/install.d.ts +153 -0
  76. package/dist/install.js +284 -0
  77. package/dist/install.js.map +1 -0
  78. package/dist/json.d.ts +100 -0
  79. package/dist/json.js +94 -0
  80. package/dist/json.js.map +1 -0
  81. package/dist/oauth/discovery.d.ts +96 -0
  82. package/dist/oauth/discovery.js +163 -0
  83. package/dist/oauth/discovery.js.map +1 -0
  84. package/dist/oauth/lock.d.ts +100 -0
  85. package/dist/oauth/lock.js +287 -0
  86. package/dist/oauth/lock.js.map +1 -0
  87. package/dist/oauth/loopback.d.ts +85 -0
  88. package/dist/oauth/loopback.js +124 -0
  89. package/dist/oauth/loopback.js.map +1 -0
  90. package/dist/oauth/pkce.d.ts +58 -0
  91. package/dist/oauth/pkce.js +77 -0
  92. package/dist/oauth/pkce.js.map +1 -0
  93. package/dist/oauth/read.d.ts +120 -0
  94. package/dist/oauth/read.js +144 -0
  95. package/dist/oauth/read.js.map +1 -0
  96. package/dist/oauth/session.d.ts +80 -0
  97. package/dist/oauth/session.js +112 -0
  98. package/dist/oauth/session.js.map +1 -0
  99. package/dist/oauth/signin.d.ts +178 -0
  100. package/dist/oauth/signin.js +225 -0
  101. package/dist/oauth/signin.js.map +1 -0
  102. package/dist/oauth/token.d.ts +139 -0
  103. package/dist/oauth/token.js +183 -0
  104. package/dist/oauth/token.js.map +1 -0
  105. package/dist/paths.d.ts +366 -0
  106. package/dist/paths.js +460 -0
  107. package/dist/paths.js.map +1 -0
  108. package/dist/policy.d.ts +195 -0
  109. package/dist/policy.js +244 -0
  110. package/dist/policy.js.map +1 -0
  111. package/dist/post.d.ts +302 -0
  112. package/dist/post.js +384 -0
  113. package/dist/post.js.map +1 -0
  114. package/dist/project.d.ts +99 -0
  115. package/dist/project.js +192 -0
  116. package/dist/project.js.map +1 -0
  117. package/dist/redact.d.ts +175 -0
  118. package/dist/redact.js +700 -0
  119. package/dist/redact.js.map +1 -0
  120. package/dist/runtime.d.ts +30 -0
  121. package/dist/runtime.js +37 -0
  122. package/dist/runtime.js.map +1 -0
  123. package/dist/spawn_budget.d.ts +101 -0
  124. package/dist/spawn_budget.js +124 -0
  125. package/dist/spawn_budget.js.map +1 -0
  126. package/dist/spool.d.ts +390 -0
  127. package/dist/spool.js +556 -0
  128. package/dist/spool.js.map +1 -0
  129. package/dist/state.d.ts +109 -0
  130. package/dist/state.js +217 -0
  131. package/dist/state.js.map +1 -0
  132. package/dist/system_message.d.ts +101 -0
  133. package/dist/system_message.js +141 -0
  134. package/dist/system_message.js.map +1 -0
  135. package/dist/term.d.ts +112 -0
  136. package/dist/term.js +134 -0
  137. package/dist/term.js.map +1 -0
  138. package/dist/version.d.ts +16 -0
  139. package/dist/version.js +17 -0
  140. package/dist/version.js.map +1 -0
  141. package/package.json +50 -0
@@ -0,0 +1,1243 @@
1
+ /**
2
+ * The copy catalogue — CR-090 precondition (1).
3
+ *
4
+ * EVERY user-facing string in this package lives here. Command bodies contain
5
+ * NO inline string literals. This is not a style preference: the claims gate can
6
+ * only check copy it can find, and a literal inside a command body is invisible
7
+ * to it. The capture client is greenfield exactly once, so this constraint is
8
+ * free now and a refactor later.
9
+ *
10
+ * Commit headers are NOT here — they come from `grades.ts`, which is the single
11
+ * renderer that takes the grade. Adding a header string to this file would
12
+ * create the second path that the structural adjacency rule exists to forbid.
13
+ *
14
+ * @provenance vibecommit-mcp src/scrubber/index.ts — [REDACTED:<kind>] marker shape, borrowed
15
+ * @provenance vibecommit-web app/app/commits/page.tsx — route existence, verified
16
+ * @provenance vibecommit-schema capture_turns column list — MEASURED ABSENCE, cited (CR-086)
17
+ */
18
+ import { EXIT } from "../exit.js";
19
+ /** `--help` copy. The `why` line is verbatim-approved (D61 §PS6) — do not reword. */
20
+ export const HELP = {
21
+ usage: "vibecommit <command> [options]",
22
+ tagline: "Record Claude Code sessions against the commits they produced.",
23
+ commands: {
24
+ connect: "Connect this repo and capture the current session.",
25
+ status: "Show whether capture is on for this repo and when it last succeeded.",
26
+ off: "Stop capturing for this repo.",
27
+ // VERBATIM-APPROVED (D61 §PS6). Any reword needs a new claims-register row.
28
+ why: "Show the conversation turns recorded against the commit that last changed this line.",
29
+ report: "Summarise capture coverage over a time window.",
30
+ },
31
+ docsUrl: "https://vibecommit.ai/docs/what-we-upload",
32
+ };
33
+ /**
34
+ * The ENTRY POINT's own chrome and its two usage failures — `CR-090d`.
35
+ *
36
+ * These five strings were inline literals in `src/index.ts` (`:74`, `:76`, `:87`,
37
+ * `:146`, `:163`) — user-facing, and invisible to the claims gate for six waves
38
+ * because the gate can only check copy it can find.
39
+ *
40
+ * ## Why a separate object rather than three more fields on `HELP`
41
+ *
42
+ * `HELP` is the help SCREEN's content — the tagline, the usage line, the per-verb
43
+ * descriptions, the docs URL — and the claims corpus tags the whole object with
44
+ * one surface, `help`. Only three of these five render there. `unknownCommand` is
45
+ * the usage-error path, so folding it into `HELP` would tag a §13.6 error
46
+ * message as help copy and check it against the wrong surface.
47
+ *
48
+ * ⚠ `notImplemented` LIVED HERE AND IS GONE, REMOVED BY `CR-108`. It was what
49
+ * `why` and `report` printed while unbuilt, and `report` was its last caller —
50
+ * **every verb in `VERBS` is now implemented**, so the string had no route to a
51
+ * screen. Copy that cannot be reached is copy no gate can check, and leaving it
52
+ * would have invited the next reader to wire a sixth verb to it silently. The surface map is only as
53
+ * honest as the objects it keys on, so the entry point gets its own.
54
+ *
55
+ * ⚠ The three labels are LABELS, not sentences: `renderHelp()` composes each with
56
+ * a single space and the result must stay byte-identical at 80 columns
57
+ * (`DESIGN.md` §13.4, `test/help.golden.txt`). A trailing space moved in here
58
+ * would be invisible in review and would move the golden file.
59
+ */
60
+ export const USAGE = {
61
+ usageLabel: "Usage:",
62
+ commandsLabel: "Commands:",
63
+ docsLabel: "Docs:",
64
+ /** The unknown-verb line. Help follows it, so this says only what went wrong. */
65
+ unknownCommand: (verb) => `Unknown command: ${verb}`,
66
+ };
67
+ /** `vibecommit status` (CR-021). */
68
+ export const STATUS = {
69
+ onForRepo: "Capture is on for this repository.",
70
+ offForRepo: "Capture is off for this repository.",
71
+ neverSent: "No session has been recorded yet.",
72
+ /**
73
+ * The §13.5 gutter keys. LOWERCASE because style guide §10.3 draws them that
74
+ * way, and §10.3 is the ruling (D65) — `lastSuccessLabel` and
75
+ * `dashboardLabel` were title-case and referenced by nothing, so `CR-021`
76
+ * changed them to match the screen rather than changing the screen.
77
+ */
78
+ repoLabel: "repo",
79
+ lastSuccessLabel: "last sent",
80
+ dashboardLabel: "dashboard",
81
+ /** `<slug> (git toplevel <path>)` — §10.3's repo row. */
82
+ repoToplevel: (slug, path) => `${slug} (git toplevel ${path})`,
83
+ /** `4 minutes ago · seq 41`. No turn count — see the module note in status.ts. */
84
+ lastSuccessValue: (age, seq) => `${age} · seq ${seq}`,
85
+ fixCommandLabel: "To reconnect, run",
86
+ /** §10.3's two trailing actions, rendered as an aligned pair. */
87
+ turnOffLabel: "Turn capture off for this repo",
88
+ revokeLabel: "Revoke this machine's credential",
89
+ };
90
+ /**
91
+ * `vibecommit connect` (CR-025 owns the live-capture ending).
92
+ *
93
+ * ⚠ THE DISCLOSURE PARAGRAPHS ARE NOW HERE — `CR-111d` landed them, transcribed
94
+ * from style guide §10.2 rather than paraphrased. They replaced three
95
+ * placeholder one-liners. D64 falsified the trust argument the earlier draft
96
+ * leaned on: this repo is PRIVATE with MIT intact, so "read the source" is false
97
+ * and BLOCKED by the register. What survives is `Vibe-Commit/claude-plugin`,
98
+ * which vendors the exact binary — and "inspect the build" is WEAKER than "read
99
+ * the source" and may not be dressed up as it (D65 §DR7).
100
+ *
101
+ * The MECHANISM copy sits beside them and belongs to other rows: the prompt, the
102
+ * two answers, the refusals, `CR-025`'s ending and `CR-110`'s fifth beat.
103
+ *
104
+ * Also locked and enforced by the mechanism, not by these strings: default `N`,
105
+ * non-TTY refuses with exit 2, and no reassurance adjectives (*secure, safe,
106
+ * protected, private, encrypted*) anywhere on the screen (D65 §DR7).
107
+ */
108
+ export const CONNECT = {
109
+ // --- THE DISCLOSURE (CR-111d). Transcribed from style guide §10.2. ---
110
+ /**
111
+ * ⚠ NOT PARAPHRASED. §10.2's rule 4 — *every noun names something actually
112
+ * uploaded, in the USER's vocabulary* — is the one rule no gate can check, so
113
+ * the nouns are transcribed rather than reworded: *"the output of commands it
114
+ * ran"*, never *"Bash tool results"*.
115
+ */
116
+ consentHeading: "VibeCommit uploads your Claude Code transcripts for this repo.",
117
+ /**
118
+ * ⚠ "private-repo paths" IS THE APPROVED WORDING, and it is the reason
119
+ * `private` is deliberately NOT registered in `claims.ts`: a substring gate on
120
+ * it would fire on this very sentence on day one, and the register's own
121
+ * §Lintable-unit section says a gate dies at its third false positive. So
122
+ * `private`-as-reassurance is a HUMAN review item here. This use is
123
+ * descriptive — it names what the paths ARE — and is the opposite of a
124
+ * reassurance.
125
+ */
126
+ consentDetail: "That means the full text of the session: your prompts, the agent's replies, the arguments to every tool call, the file contents inside Edit records, and the output of commands it ran. File paths include private-repo paths.",
127
+ /**
128
+ * ⚠ THE NUMBER AND ITS QUALIFIER ARE ONE UNIT.
129
+ *
130
+ * **12 is `CR-042`'s counted figure** (merged, `6d8ec68`), taken rather than
131
+ * recounted: the patterns live in `vibecommit-mcp/src/scrubber/patterns.ts`,
132
+ * which is closed source and another chain's repo. `CR-042` exists precisely
133
+ * because the draft said 10 — a number is a claim, and this one is counted
134
+ * from code.
135
+ *
136
+ * The qualifier is as load-bearing as the number. Without *"it is a pattern
137
+ * list, not a general secret detector"* and *"a secret it has no pattern for
138
+ * is uploaded"*, the count reads as PROTECTION — which is the reassurance D65
139
+ * §DR7 bans, arriving as arithmetic instead of as an adjective.
140
+ */
141
+ consentScrubber: "A scrubber removes 12 known secret formats (AWS keys, GitHub tokens, and similar). It is a pattern list, not a general secret detector: a secret it has no pattern for is uploaded. Command output is not path-filtered.",
142
+ /**
143
+ * ⛔ **THE SECOND DATA CLASS, AND THE DISCLOSURE DID NOT MENTION IT AT ALL —
144
+ * `T15`, `D190`.**
145
+ *
146
+ * The three paragraphs above enumerate TRANSCRIPT CONTENT. They were the whole
147
+ * disclosure, and commit SHAs have gone on the wire since `CR-170` without a
148
+ * word here. `D190` adds two more headers, one of which is a genuinely new
149
+ * class, so the copy is extended rather than the send being scoped — scoping
150
+ * the send would gut the feature.
151
+ *
152
+ * ## ⛔ WHAT IS HERE IS MEASURED FROM `buildIngestHeaders`, NOT FROM THE PLAN
153
+ *
154
+ * | said here | header | since |
155
+ * |---|---|---|
156
+ * | the commit ID | `x-commits` | `CR-170` |
157
+ * | the branch name | `x-head-branch` | `CR-170` |
158
+ * | the session it was linked to | `x-session-id` | pre-`CR-170` |
159
+ * | how that link was made | `x-commit-attributions` | `D190`, NEW |
160
+ * | the replaced commit's ID | `x-rewrites` | `D190`, NEW |
161
+ *
162
+ * ⛔ **AND WHAT IS DELIBERATELY NOT HERE, BECAUSE IT DOES NOT LEAVE THE
163
+ * MACHINE.** `post_commit.ts` gathers the changed-file list and the committer
164
+ * date because they are free at that moment, and `capSpool` returns only
165
+ * `{shas, attributions, count}`. **Describing them as uploaded would be a FALSE
166
+ * DISCLOSURE, which is as much a claims defect as a missing one** — and it is
167
+ * the easier mistake, because over-disclosing feels safe.
168
+ *
169
+ * ⚠ **`SpoolEntry.branch` and `delta.head.branch` are NOT the same noun.** The
170
+ * per-commit one stays local; the repo's CURRENT branch goes out on every
171
+ * delta as `x-head-branch` (`post.ts:228`, `entry.ts:612`). The branch name is
172
+ * named here on the strength of the second, never the first.
173
+ *
174
+ * ## ⛔ THE REWRITE SENTENCE IS THE ONE THIS STRING EXISTS FOR
175
+ *
176
+ * `x-rewrites` carries LOCAL, UNPUSHED REWRITE HISTORY — `ancestor:successor`
177
+ * pairs whose ancestor is unreachable from every branch and gone after
178
+ * `gc --prune=now`. Telling a user "we upload your transcripts" does not cover
179
+ * "we upload a record of the commits you rewrote before pushing". **Someone who
180
+ * squashes to bury a false start is entitled to know that the false start's ID
181
+ * left the machine.**
182
+ *
183
+ * ⚠ *"may never have left this machine"*, not *"was never pushed"*: you can
184
+ * amend a commit that WAS pushed and force-push it, so the stronger sentence is
185
+ * false in a real case.
186
+ *
187
+ * ## ⛔ NO NEGATIVE LIST, AND THAT IS A DECISION
188
+ *
189
+ * An earlier draft closed with *"commit messages, authors, dates and the
190
+ * changed-file list are not uploaded."* True today (`post.ts:166-174`), and
191
+ * CUT: `D190 §6` says this wire has **no compiler, no schema and no shared
192
+ * type**, so a negative claim in shipping copy goes false the first time anyone
193
+ * adds a header — silently, with no gate able to see it. Positive claims only.
194
+ *
195
+ * ⚠ Opens *"Not only the transcript"* because `consentHeading` still says
196
+ * *"uploads your Claude Code transcripts"*, which is now narrower than the
197
+ * truth. Repairing the scope where a skimmer meets it; the heading itself is a
198
+ * bigger copy change than this task was scoped for.
199
+ */
200
+ consentCommits: "Not only the transcript. Commits you make while a session is recording are uploaded too: the commit ID, the branch name, the session it was linked to, and how that link was made. If you amend or rebase, the ID of the commit you replaced goes with them — that commit may never have left this machine, and git will eventually delete it from here.",
201
+ /** §10.2's first labelled link. Points at `HELP.docsUrl` — one definition. */
202
+ consentDocsLabel: "Read the full list:",
203
+ /**
204
+ * ⚠ THE REPLACEMENT FOR THE OPEN-SOURCE CLAIM, and the ceiling on what it may
205
+ * say. D64 created this repo PRIVATE with MIT intact, so *"read the source"*
206
+ * is FALSE — the register's own signed-receipts failure mode, copy describing
207
+ * a mechanism that does not exist. What survives is `claude-plugin`, which D20
208
+ * keeps public non-negotiably and which vendors the exact binary that runs.
209
+ *
210
+ * **"Inspect the build" is WEAKER than "read the source" and may not be
211
+ * dressed up as it** (D65 §DR7). This line offers the artefact and claims
212
+ * nothing about readable source. All four phrasings are registered BLOCKED on
213
+ * the `connect` surface, so the gate refuses the smuggled version too.
214
+ */
215
+ consentBundleLabel: "The binary that runs is here:",
216
+ consentPrompt: "Continue?",
217
+ declined: "Not connected. Nothing was uploaded.",
218
+ capturing: "Recording this session…",
219
+ doneHeading: "Connected.",
220
+ doneUrlLabel: "View this session",
221
+ // --- Mechanism copy (CR-016). ---
222
+ /** The `[y/N]` suffix. The capital is the default and the default is NO. */
223
+ consentAnswers: "[y/N]",
224
+ /** Non-TTY refusal — DESIGN.md §13.7 exit 2, D65 §DR7. */
225
+ nonInteractiveWhat: "Consent needs an interactive terminal.",
226
+ nonInteractiveWhy: "Standard input is not a terminal, so the consent prompt cannot be answered. Nothing was read and nothing was uploaded.",
227
+ nonInteractiveFix: "Run this in a terminal:",
228
+ alreadyConnected: "Capture is already on for this repository.",
229
+ /**
230
+ * The browser sign-in beat (PKCE loopback → minted ingest credential) is the
231
+ * oauth lane's, not this task's. Until it lands, the supported path is the one
232
+ * `VIBECOMMIT_TOKEN` exists for. This copy states a mechanism, not a promise.
233
+ */
234
+ credentialNeededWhat: "This machine has no ingest credential.",
235
+ credentialNeededWhy: "Consent is recorded for this repository, but nothing can be uploaded without a credential for this machine.",
236
+ credentialNeededFix: "Set one, then run connect again:",
237
+ credentialNeededCommand: "export VIBECOMMIT_TOKEN=…",
238
+ // --- The ending (CR-025). Style guide §10.2's last three lines. ---
239
+ /** `Capture is on for <repo>. Nothing else to do.` */
240
+ doneForRepo: (repo) => `Capture is on for ${repo}. Nothing else to do.`,
241
+ stopLabel: "To stop",
242
+ /**
243
+ * No transcript to record. ONE state — `CR-112` (W8) owns the five-absence
244
+ * grammar as a set, and a sixth phrasing invented here is what that task
245
+ * exists to prevent.
246
+ */
247
+ noTranscriptWhat: "No Claude Code session was found for this repository.",
248
+ noTranscriptWhy: "Consent is recorded, but connect had nothing to record: no transcript exists yet for this project. Capture starts on its own the next time Claude Code runs here.",
249
+ noTranscriptFix: "Nothing to fix. Start a session, then check:",
250
+ // --- The FIFTH beat: org approval required (CR-110, D65 §DR6, D81). ---
251
+ /**
252
+ * ⚠ `warn`, NEVER `bad`, and **EXIT 0, never 1**. That 403 means *later*, not
253
+ * *failed*: the offset holds and the backlog uploads in order on approval. A
254
+ * red exit tells a correctly-installed user they broke something, which moves
255
+ * the confusion report's T+9:00 give-up to T+0:45. `held` and `armed` are the
256
+ * two words doing the work — both mean *ready, not running*.
257
+ *
258
+ * `capture_policy` defaults to `approval_required`, so a member of a
259
+ * multi-member org sees this every time; solo orgs auto-approve inside the
260
+ * insert and never meet it.
261
+ */
262
+ pendingWhat: "Your org needs to approve capture first.",
263
+ /**
264
+ * ⚠ NAMES THE REPO, NOT THE ORG — and that is a deviation from the style
265
+ * guide's drawn example, made deliberately. §10.2 renders *"acohan/repo
266
+ * belongs to the `vibecommit` org"*, and the client cannot produce that name:
267
+ * the 403 body discloses nothing (no org id, no owner list, by design), and
268
+ * `resolveRepoSlug` yields the GIT owner, which is a different thing from the
269
+ * VibeCommit org that holds the capture policy. Inventing one would be a
270
+ * confident lie; the link resolves it, which is what the link is for.
271
+ */
272
+ pendingWhy: (repo) => `${repo} belongs to an org that requires owner approval before capture runs.`,
273
+ /**
274
+ * ⚠ THE SHARED CLAUSE, and it renders on EVERY branch. D81: `owners_notified`
275
+ * `null` = NOT DETERMINABLE, `0` = determinable and nobody was told, `> 0` =
276
+ * N notified — and **`> 0` ADDS to this sentence rather than replacing it**.
277
+ * One shared clause is why the two branches cannot drift apart later; a pair
278
+ * of independent paragraphs is how the false version gets reintroduced.
279
+ *
280
+ * It is also TRUE, and checked rather than inherited: the server upserts a
281
+ * `capture_requests` row BEFORE it emits the 403.
282
+ */
283
+ pendingRecorded: "Your request is recorded.",
284
+ /**
285
+ * ⚠ THE ONLY BRANCH THAT MAY CLAIM A NOTIFICATION, and it is reachable only
286
+ * when the server says `> 0`. Today it never does — the field is hardcoded
287
+ * `null` until `CR-036`, which is provisioning-blocked — so this sentence is
288
+ * built and unreachable ON PURPOSE. **Never render it on the null branch.**
289
+ * D81: a hardcoded *"we notified the owners"* installs the most damaging
290
+ * possible mental model at the moment of highest trust — *the owner knows, so
291
+ * I just wait* — so the pending state would suppress its own workaround. A
292
+ * user told nothing at least asks in Slack.
293
+ *
294
+ * The branch is PERMANENT, not a stopgap: addresses bounce, domains land on
295
+ * suppression lists, and a sole owner's address can be dead. `CR-036` landing
296
+ * does not delete the other branch; it only makes this one reachable.
297
+ */
298
+ pendingNotified: (owners) => owners === 1 ? "We notified the owner." : `We notified the ${owners} owners.`,
299
+ /** True by construction: 403 is `later`, so the offset holds and bytes stay on disk. */
300
+ pendingBuffered: "Nothing is lost while you wait — turns are buffered on this machine and upload in order once an owner approves.",
301
+ pendingTrackLabel: "Track it:",
302
+ /** `armed` = ready, not running. The install is correct; capture is waiting. */
303
+ pendingArmed: (repo) => `Capture is armed for ${repo}.`,
304
+ };
305
+ /**
306
+ * The PATH clash — `CR-025`, and `DESIGN.md` §13.6's worked example is the spec.
307
+ *
308
+ * D55 accepted a residual bin collision as unlikely; the register corrects that
309
+ * to *closer to certain*, because the squatting package (`vibecommit@1.0.6`) is
310
+ * itself a commit CLI, so the overlap population is exactly developers who
311
+ * install commit tooling. **This detects the collision D55 accepted. It does not
312
+ * reopen the naming decision** — the bin name stays.
313
+ *
314
+ * ⚠ The plugin path in the fix line depends on `CR-027e` (renames the plugin
315
+ * directory to `vibecommit`) and `CR-028` (creates `bin/` at all), both W6, both
316
+ * in `claude-plugin`, both another builder's. Written as §13.6 draws it rather
317
+ * than softened to dodge the dependency.
318
+ */
319
+ /**
320
+ * The `post-commit` install report — `CR-170`, D154.
321
+ *
322
+ * ⛔ **EVERY OUTCOME SAYS SOMETHING, INCLUDING THE GOOD ONE.** Three of this
323
+ * path's failure modes are silent no-ops (D98's class): `core.hooksPath` makes
324
+ * `.git/hooks` dead, an existing hook must not be clobbered, and neither is
325
+ * visible without a line of output. A user who is told nothing cannot tell a
326
+ * working install from one that observes no commits for the life of the clone.
327
+ *
328
+ * ⚠ **AND IT SAYS "THIS CLONE", EVERY TIME.** Per-clone installation is the real
329
+ * cost of this design and the one thing a user will be surprised by later, so it
330
+ * is stated at the moment they can act on it rather than buried in documentation.
331
+ */
332
+ export const COMMIT_HOOK = {
333
+ 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.`,
334
+ 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.`,
335
+ already: "Commit capture was already on for this clone.",
336
+ perClone: "Run `vibecommit connect` in each clone you want commits recorded for — git hooks are per-clone, not per-account.",
337
+ hooksPathWhat: "Commit capture could NOT be installed for this clone.",
338
+ 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.`,
339
+ hooksPathFixLabel: "Fix, either one:",
340
+ hooksPathFixUnset: "git config --unset core.hooksPath",
341
+ hooksPathFixManual: "or add `vibecommit post-commit` to your existing post-commit hook",
342
+ failedWhat: "Commit capture could NOT be installed for this clone.",
343
+ failedWhy: (why) => `Writing the \`post-commit\` hook failed: ${why}. Transcript capture is unaffected; only commit observation is.`,
344
+ };
345
+ export const PATH_CLASH = {
346
+ foreignWhat: "Another program owns the `vibecommit` command.",
347
+ foreignWhy: (resolved) => `\`vibecommit\` on your PATH resolves to ${resolved}, which belongs to the npm package \`vibecommit\` (a different project). Ours is \`@vibe-commit/capture\`. Hook capture is unaffected — it runs by path — but \`why\`, \`report\` and \`status\` will run the other program.`,
348
+ absentWhat: "The `vibecommit` command is not on your PATH.",
349
+ absentWhy: "Hook capture is unaffected — the plugin runs the binary by path — but typing `vibecommit status`, `why` or `report` will not find it.",
350
+ fixLabel: "Fix, either one:",
351
+ /**
352
+ * FOREIGN only. The uninstall half is wrong for the absent case — telling
353
+ * someone to uninstall a package they demonstrably do not have is the kind of
354
+ * fix line that teaches users to stop reading fix lines. Found by running it.
355
+ */
356
+ fixReplace: "npm uninstall -g vibecommit && npm i -g @vibe-commit/capture",
357
+ /** ABSENT: there is nothing to remove, only something to add. */
358
+ fixInstall: "npm i -g @vibe-commit/capture",
359
+ fixDirectLabel: "or invoke ours directly:",
360
+ /**
361
+ * `node …/bin/index.js`, not `…/bin/vibecommit`, and the difference is not
362
+ * cosmetic: `claude-plugin`'s `bin/` contains `index.js` and no file named
363
+ * `vibecommit`, and `index.js` carries a shebang but is NOT executable — so
364
+ * the shorter form fails twice over. `hooks.json` invokes it exactly this
365
+ * way, which is the shape that demonstrably works.
366
+ *
367
+ * This line asked for something impossible until DESIGN.md §13.6 was
368
+ * corrected — the same defect as the absent case above telling users to
369
+ * uninstall a package they do not have. **A fix line must be runnable, and
370
+ * the way you know is that you ran it.**
371
+ */
372
+ fixDirect: "node ~/.claude/plugins/vibecommit/bin/index.js connect",
373
+ };
374
+ /**
375
+ * Browser sign-in — `CR-084d`, the beat `connect.ts:175-176` deferred to "the
376
+ * oauth lane" and this task is that lane.
377
+ *
378
+ * ⚠ Two rules these strings are written to, both from the claims register:
379
+ *
380
+ * 1. **No reassurance adjective anywhere** (D65 §DR7 registers *secure, safe,
381
+ * protected, encrypted* on the `connect` surface, which is where every
382
+ * string below renders). A sign-in screen is exactly where the temptation
383
+ * is strongest, and "we'll securely sign you in" says nothing a user can
384
+ * check.
385
+ * 2. **The `what` line never has "you" as its subject** and never carries a
386
+ * bare HTTP status (DESIGN.md §13.6). "Sign-in did not finish", not "you
387
+ * failed to sign in"; the CAUSE goes in the `why` line, in a noun the user
388
+ * recognises.
389
+ *
390
+ * `browserDone` and `browserRefused` are the only strings in this package a
391
+ * BROWSER renders rather than a terminal, which is why they carry no §13.5
392
+ * gutter and no §13.2 glyph: there is no terminal to degrade to. They are still
393
+ * copy, so they still live here and are still scanned by the claims gate.
394
+ */
395
+ export const SIGNIN = {
396
+ /** Printed before the browser opens, so the user knows what just happened. */
397
+ opening: "Opening your browser to sign in…",
398
+ /**
399
+ * The headless fallback. NOT an error: a machine with no browser is an
400
+ * ordinary place to run this, and the URL is a complete substitute.
401
+ */
402
+ manualLabel: "Open this URL to finish signing in:",
403
+ waiting: "Waiting for the browser…",
404
+ done: "Signed in.",
405
+ /** What the browser tab shows. One line each — nothing is styled. */
406
+ browserDone: "Signed in. You can close this tab and return to your terminal.",
407
+ browserRefused: "This request did not match the sign-in waiting in your terminal.",
408
+ // --- §13.6 error blocks, one per named outcome of `signIn`. ---
409
+ noServerWhat: "VibeCommit could not find the sign-in service.",
410
+ /** `unreachable` — nothing answered. Says what was tried, not what is wrong. */
411
+ noServerUnreachableWhy: "The server did not answer the request that says where to sign in. It may be down, or this machine may have no route to it.",
412
+ /** Any `malformed` fault. One sentence, because the user's move is the same. */
413
+ noServerMalformedWhy: "The server answered, but the document that says where to sign in is not one this version can use.",
414
+ /**
415
+ * The override was refused before anything was sent. Names the variable and
416
+ * the rule, because this one is entirely in the user's hands to fix.
417
+ */
418
+ noServerRefusedWhy: "VIBECOMMIT_MCP_URL is set to an address this client will not send a sign-in to. Only https, or http on the loopback, is allowed.",
419
+ noServerFix: "Check the service, then run:",
420
+ deniedWhat: "Sign-in was declined.",
421
+ deniedWhy: "The sign-in screen returned without granting access. Nothing was changed on this machine.",
422
+ deniedFix: "To try again, run:",
423
+ timeoutWhat: "Sign-in did not finish in time.",
424
+ timeoutWhy: "The browser did not come back before the deadline. Nothing was changed on this machine.",
425
+ timeoutFix: "To try again, run:",
426
+ rejectedWhat: "The sign-in service refused this client.",
427
+ /**
428
+ * ⚠ `unauthorized_client` is the expected answer TODAY: this CLI's client id
429
+ * is not in the server's registered set (see `src/oauth/token.ts`). The copy
430
+ * names the state without promising when it changes — a date here would be a
431
+ * claim the register would have to carry.
432
+ */
433
+ rejectedWhy: "The service did not accept this client's sign-in request.",
434
+ rejectedFix: "Report this, with the command you ran:",
435
+ unreachableWhat: "VibeCommit could not reach the sign-in service.",
436
+ unreachableWhy: "The request did not complete. Nothing was changed on this machine.",
437
+ unreachableFix: "Check the connection, then run:",
438
+ malformedWhat: "The sign-in service returned an unexpected answer.",
439
+ malformedWhy: "The response was not in the form this version can use. No session was stored.",
440
+ malformedFix: "Report this, with the command you ran:",
441
+ /** A session already exists, so sign-in is a no-op. */
442
+ alreadySignedIn: "Already signed in on this machine.",
443
+ // --- `authorizedAccessToken`'s last two outcomes — `CR-086`, W9. ---
444
+ /**
445
+ * ⚠ THE COPY THIS OBJECT DEFERRED BY TASK NUMBER, now written by the first
446
+ * renderer. `why` is that renderer; `report` (`CR-108`, W10) is the second and
447
+ * inherits these rather than inventing a second phrasing.
448
+ *
449
+ * ⚠ AND THIS EXPORT NOW RENDERS ON TWO SURFACES. `SURFACES` in
450
+ * `test/copy-claims.test.ts` gained `why` beside `connect` for exactly these
451
+ * four strings — a surface map that still said `connect` alone would be
452
+ * checking them against the wrong blocked set.
453
+ *
454
+ * `expired` — the refresh grant was REFUSED. The session is over; only a new
455
+ * sign-in fixes it. Distinct from `busy`, which is a wait.
456
+ */
457
+ expiredWhat: "This machine's sign-in has ended.",
458
+ expiredWhy: "The stored sign-in could not be renewed, so nothing could be read. It may have been revoked, or it may simply have run out.",
459
+ expiredFix: "Sign in again, then run the command again:",
460
+ /**
461
+ * `busy` — another process holds the refresh lock and did not release it in
462
+ * time. NOT a failure of the session: the fix is to run it again, and saying
463
+ * "sign in again" here would send a user to re-authenticate over a lock.
464
+ *
465
+ * ⚠ The `what` line's subject is the state, never "you" (DESIGN.md §13.6).
466
+ */
467
+ busyWhat: "Another VibeCommit command is renewing this machine's sign-in.",
468
+ 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.",
469
+ busyFix: "Run it again in a moment:",
470
+ };
471
+ /** `vibecommit off`. */
472
+ export const OFF = {
473
+ done: "Capture is off for this repository.",
474
+ alreadyOff: "Capture was already off for this repository.",
475
+ note: "Sessions already recorded are unaffected.",
476
+ };
477
+ /**
478
+ * `vibecommit why` — `CR-086`, W9.
479
+ *
480
+ * The screen is drawn in style guide §10.4 (layout), §10.5 (sub-agent
481
+ * attribution) and §10.6-A/B (the two absences this verb owns), and the copy is
482
+ * NORMATIVELY §R3's decision table in `docs/designs/claims-audit-why.md`. §10.4
483
+ * is explicit that §R3 governs and that it must not be paraphrased here, so the
484
+ * per-grade sentences live in `grades.ts` behind the one header renderer and
485
+ * this object holds only what §R3 tables SEPARATELY from the grade.
486
+ *
487
+ * ## ⚠ THE HOLE THIS SURFACE SHIPS WITH, HELD OPEN AND NAMED — `CR-129`
488
+ *
489
+ * §R3's sub-agent attribution table has three rows and **two of them read data
490
+ * nothing produces**. Measured at `vibecommit-schema` `2c77400`,
491
+ * `vibecommit-mcp` `eeda878` and here at `4dcf47d`, every tracked blob read as
492
+ * utf8 (never `grep` — `src/redact.ts` carries literal NUL bytes and `grep -I`
493
+ * skips it silently):
494
+ *
495
+ * - `capture_turns.subagent_count` — **0 hits in all three repos** at those
496
+ * SHAs. `TODOS[74]` assumed the column; nothing had added it.
497
+ * ⚠ **UPDATED BY `CR-129d` (W10): THE COLUMN NOW EXISTS AND STILL HAS NO
498
+ * PRODUCER.** `CR-129a` added it at `vibecommit-schema` `f3022c5` —
499
+ * `integer`, nullable, no default. Nothing writes it (mcp's
500
+ * `insertCaptureTurns` is a seven-key literal) and nothing emits it
501
+ * (`blame_commit` selects `position, turn_hash`), so the marker still
502
+ * cannot render from real data. The measurement above is kept because it
503
+ * is dated and was true; **this note is the correction, not a rewrite.**
504
+ * ⚠ A six-column tally stood here and is gone — it was an exact
505
+ * transcription of the original `create table` that three later
506
+ * migrations emptied, including one renaming `workspace_id` to `org_id`.
507
+ * D101: write the predicate, never the count.
508
+ * - `commit_file_attribution` EXISTS (`CR-070`,
509
+ * `20260814000000_cr070_commit_graph.sql:278`) and **has no writer**:
510
+ * `CR-072` shipped the edge only and said so
511
+ * (`vibecommit-mcp/src/conversation/edge_derivation.ts:56-64`). That table
512
+ * is where `agent_id` lives.
513
+ *
514
+ * So `agent_id` is a column on a table nothing writes, and `subagent_count` is
515
+ * not a column at all. `TODOS[74]` says of that minimum: *"E1 cannot ship
516
+ * without it… silence about a delegated edit is an overclaim about who wrote
517
+ * the code."* Both halves are true, and the resolution is neither to omit the
518
+ * requirement nor to fake it: **the absence renders, in the absence grammar,
519
+ * naming what it is and what would change it.**
520
+ *
521
+ * ⚠ `agent_id` NULL is SEMANTIC — the schema comment says *"NULL = the main
522
+ * thread. The empty string is refused because '' would render as an unnamed
523
+ * sub-agent — a plausible wrong answer rather than an error."* **`null` and
524
+ * ABSENT are therefore different facts**: `null` means we know it was the main
525
+ * thread; absent means we know nothing. An unmarked turn rendered from an
526
+ * absent field is the overclaim, so `why.ts` requires the field to be PRESENT
527
+ * before it renders a turn unmarked, and marks every turn otherwise.
528
+ *
529
+ * ## What is deliberately NOT here
530
+ *
531
+ * No grade word. §13.3: *"Grade words appear in `--json` only. Prose never
532
+ * prints a bare grade"* — the two-tier structural summary plus the per-edge
533
+ * evidence sentence render instead, and the evidence sentence is `grades.ts`'s.
534
+ *
535
+ * No `⛓` / `◇` glyphs. §10.5 draws them for the WEB surface; `DESIGN.md` §13.2's
536
+ * `GLYPH` table has exactly three entries (`ok` / `warn` / `bad`) and neither is
537
+ * in it. The CLI's binding requirement is §R3's prose evidence line, which
538
+ * renders. Inventing a fourth glyph is a report to the orchestrator, not an edit
539
+ * to `term.ts`.
540
+ *
541
+ * No colour on the attribution marker or on the grade (D65 §DR3, §DR4).
542
+ * `agent_id` is an ACTOR IDENTITY, the same class as a branch name, and a
543
+ * green/yellow/red grade ramp asserts a ranking §R3 forbids.
544
+ */
545
+ export const WHY = {
546
+ // --- §R3: the turn-list framing. IDENTICAL AT ALL THREE GRADES. ---
547
+ /**
548
+ * ⚠ THIS is where D61 §PS6's file scope lives, and the reason it moved here.
549
+ * `commit_file_attribution` has no line ranges, so every sentence that
550
+ * INTRODUCES TURNS is file-scoped — and §PS6 requires the file scope to render
551
+ * on the same screen as the line number, which it does: the identity line
552
+ * above carries the line, this sentence carries the file.
553
+ *
554
+ * ⚠ §R3 bans a causal vocabulary on this sentence at all grades — *produced,
555
+ * caused, wrote this line, because, the reason, responsible for, explains, led
556
+ * to*. "Touched" is the approved verb and it is the weakest true one.
557
+ */
558
+ turnsIntro: (file) => `Turns that touched ${file} in this commit:`,
559
+ /**
560
+ * §R3's `match_kind` table, the `probable` row — `CR-138d` / `CR-109d`.
561
+ *
562
+ * ⚠ **THE ONLY DIFFERENCE BETWEEN THE TWO VARIANTS, AND IT MUST STAY VISIBLE.**
563
+ * §R3: `exact` renders the squash copy *unqualified* — the pair came from a
564
+ * recorded successor row — while `probable` renders it **plus a stated
565
+ * basis**. Rendering them identically is the folding D67 forbids: *"the
566
+ * disclosure would become the thing that hides the distinction."*
567
+ *
568
+ * ⚠ D105 makes this permanent rather than cosmetic: `match_kind` is in
569
+ * `commit_sha_successors`' primary key and `reject_history_mutation` raises
570
+ * on every UPDATE including `service_role`, **so a wrong row cannot be
571
+ * corrected.** A guess labelled as a guess is the only honest rendering.
572
+ *
573
+ * ⚠ `witnessed` is §R3's own word and the choice is deliberate. The obvious
574
+ * phrasing would have reached for the `observed` family, which is BLOCKED on
575
+ * this surface because it names a mechanism that exists in no repo (D113 §1)
576
+ * — a blocked claim arriving through a synonym nobody was checking. This
577
+ * comment names the trap without spelling the phrase, so a source-text scan
578
+ * does not fire on the note explaining the avoidance.
579
+ */
580
+ squashProbableBasis: "These two commits were matched by patch id rather than by a recorded successor row, so the pairing is read off the commits themselves and is not something capture witnessed.",
581
+ /**
582
+ * ⚠ WHY THE TWO SHAS ON THE SQUASH SCREEN ARE DIFFERENT LENGTHS.
583
+ *
584
+ * Two builders — this one and `CR-101`'s on the web surface — read the
585
+ * asymmetry as a bug, and **the ruling was to explain it rather than remove
586
+ * it** (D120 §1 as revised). Web keeps parity by carrying the full value in
587
+ * the DOM; a terminal line has nowhere to put it, so the line says why.
588
+ *
589
+ * ⚠ **THE SENTENCE DOES NOT TELL THE READER TO RUN `git show` ON IT**, and
590
+ * that restraint is measured rather than cautious: cloned over the git
591
+ * transport after a squash-merge with branch delete, the original object is
592
+ * ABSENT and `git show` fails on the full sha as surely as on a short one.
593
+ * What the full value keeps is that it stays a complete identifier somebody
594
+ * else can look up. Promising a local command that fails in exactly the case
595
+ * this state exists for would be the plausible-wrong-answer class in copy.
596
+ */
597
+ squashShaLengths: "The recorded commit is written out in full because a squashed commit is often missing from a working copy, and a shortened id cannot be looked up once the commit is gone.",
598
+ /** §10.4's turn row: `turn 12 [Edit] session.ts`. */
599
+ turnLabel: "turn",
600
+ /** §10.4: excerpts are `muted` and `>`-prefixed — verbatim, never a summary. */
601
+ excerptPrefix: ">",
602
+ // --- §R3: the sub-agent attribution table (§10.5). ---
603
+ /** Row 1 — `agent_id IS NOT NULL`. An identity, rendered plain (D65 §DR3). */
604
+ subAgentMarker: (agentId) => `· sub-agent ${agentId}`,
605
+ /**
606
+ * Row 2 — `capture_turns.subagent_count > 0`.
607
+ *
608
+ * ⚠ **NO PRODUCER EXISTS FOR THIS AND THE COLUMN IS NOT IN ANY SCHEMA.** It is
609
+ * implemented against a validated OPTIONAL payload field so that the day the
610
+ * column exists the row renders, and it is exercised in the suite against a
611
+ * synthetic payload — but nothing on the wire can populate it today, and this
612
+ * comment is the only honest place to say so. It is NOT rendered from a zero
613
+ * and NOT rendered from an absent field.
614
+ */
615
+ subAgentCountMarker: (count) => count === 1 ? "· includes 1 sub-agent record" : `· includes ${count} sub-agent records`,
616
+ /**
617
+ * The marker for a turn whose actor we cannot name — `CR-129`'s hole, on the
618
+ * turn itself rather than only in a footnote. §10.5: *"the turn header and the
619
+ * file-attribution table ALWAYS name the actor… hiding an actor"* is the thing
620
+ * that makes a claim. When we cannot name one, we say that, per turn.
621
+ */
622
+ actorUnrecordedMarker: "· actor not recorded",
623
+ /**
624
+ * The `CR-129` note, in the absence grammar — absence, cause, remedy.
625
+ *
626
+ * ⚠ NOT an `ABSENCE` state and it may not become one. Those five are
627
+ * whole-screen empty states that all exit 4; this is a caveat on a screen that
628
+ * HAS content, and `test/copy-absence.test.ts` asserts the set is exactly
629
+ * five. Same ruling the `ABSENCE` docblock already makes for the
630
+ * shallow-clone refusal: not every absence sentence is an absence STATE.
631
+ */
632
+ attributionWhat: "Sub-agent attribution is not recorded for these turns.",
633
+ /**
634
+ * ⚠ "on this screen", not "below" — caught by rendering it. The note sits
635
+ * AFTER the turn list, so a positional word was pointing the wrong way. A
636
+ * sentence that describes its own layout breaks silently when the layout
637
+ * moves; one that names the screen does not.
638
+ */
639
+ attributionWhy: "The per-turn attribution this marker reads has no writer yet, so no turn on this screen can be shown as main-thread work or as delegated work.",
640
+ /**
641
+ * ⚠ THE SECOND SENTENCE IS THE LOAD-BEARING ONE. Without it, an unmarked turn
642
+ * reads as a finding that the main thread wrote it — which is the inversion
643
+ * the register permanently BLOCKS, arriving through silence instead of through
644
+ * a sentence.
645
+ */
646
+ attributionNotAFinding: "An unmarked turn here is a missing record, not a finding about who wrote the code.",
647
+ attributionFix: "Nothing to fix on this machine. The marker renders once attribution is recorded.",
648
+ // --- §R3: the truncation notice, and the L2 no-silent-drops line. ---
649
+ /**
650
+ * §R3, verbatim. ⚠ **The selection rule is ORDINAL AND STATED** — *"in turn
651
+ * order"*, never *"most relevant"*, which is a ranking, which is server-side
652
+ * analysis and reverses D48–D51.
653
+ *
654
+ * ⚠ **Every number here comes from the PAYLOAD.** D60 §D1a: *"the client may
655
+ * render; it may not analyze."* Windowing an already-ordered list for display
656
+ * is rendering; DERIVING the numbers is not, so `why.ts` never counts the
657
+ * array it was handed.
658
+ */
659
+ truncation: (from, to, total) => `Showing turns ${from}-${to} of ${total} that touched this file in this commit, in turn order.`,
660
+ /** §R3's L2: files in the commit whose `turn_hash` is NULL are COUNTED, never dropped. */
661
+ filesWithoutTurn: (count) => `${count} more files in this commit have no linked turn.`,
662
+ /** The one-file form. `1 more files` is the tell that a count was never read. */
663
+ fileWithoutTurn: "1 more file in this commit has no linked turn.",
664
+ // --- Usage. §13.7: a bad argument is exit 2, never exit 1. ---
665
+ usageWhat: "This command needs a file and a line number.",
666
+ usageWhy: "`why` answers for one line, so it cannot run without one. Nothing was read and nothing was sent.",
667
+ usageFix: "Name the file and the line:",
668
+ /**
669
+ * `--json` — the REFUSAL COPY THAT STOOD HERE IS GONE, and its removal is the
670
+ * deliverable rather than a tidy-up (`CR-149`, D122 §2).
671
+ *
672
+ * It read *"`why` cannot produce JSON yet"*, and its docblock argued the
673
+ * refusal was *"the honest minimum rather than a gap"*. That was honest as a
674
+ * STOPGAP and false as a claim about design intent: **§13.3 REQUIRES `--json`
675
+ * on the read verbs**, so the flag was an UNBUILT FEATURE wearing the shape of
676
+ * a designed position — which is the harder defect to see, because a refusal
677
+ * reads as a decision. `src/json.ts` is the document; `status`, `why` and
678
+ * `report` all emit one.
679
+ *
680
+ * Nothing replaces these three constants. A verb that CAN answer the flag has
681
+ * no refusal to word.
682
+ */
683
+ /**
684
+ * The shallow-clone REFUSAL (D67's posture, D98's hazard class).
685
+ *
686
+ * ⚠ A REFUSAL IS NOT AN ABSENCE. `git blame` on a shallow clone attributes to
687
+ * the graft boundary and returns a plausible WRONG commit — a call that fails
688
+ * announces itself; a call that returns a plausible answer is absorbed by the
689
+ * caller. Shallow clones live in CI containers, which is exactly where nobody
690
+ * is watching. The `ABSENCE` docblock already rules on this for `report`: it
691
+ * is not a sixth state and one may not be written there.
692
+ */
693
+ shallowWhat: "This is a shallow clone, so `git blame` cannot be trusted here.",
694
+ shallowWhy: "A shallow clone stops at its graft boundary and attributes every older line to the oldest commit it holds. That answer looks ordinary and is wrong, so it is refused rather than shown.",
695
+ shallowFix: "Fetch the full history, then run this again:",
696
+ shallowFixCommand: "git fetch --unshallow",
697
+ /** `git blame` itself failed — no such file, no such line, or git said no. */
698
+ blameWhat: "`git blame` could not attribute that line.",
699
+ blameWhy: "git did not return a commit for it. The path may not be tracked in this repository, or the file may have fewer lines than the one named.",
700
+ blameFix: "Check the path and the line, then run this again:",
701
+ // --- The read lane. Every arm renders; none exits 0 silent (D61 round 3 §D8). ---
702
+ /**
703
+ * ⚠ A READ-LANE FAILURE MUST PRINT AND EXIT NON-ZERO. The whole reason the
704
+ * exit contract was split is that the hook rule — exit 0, stdout empty — would
705
+ * have made this verb fail SILENTLY on a 5xx. `src/exit.ts` says so at the top.
706
+ *
707
+ * ⚠ And never a bare HTTP status (§13.6). The status is what we know; it is
708
+ * not what the user can act on.
709
+ */
710
+ unreachableWhat: "VibeCommit could not reach the record service.",
711
+ unreachableWhy: "The request did not complete, so no answer was read. The service may be down, or this machine may have no route to it.",
712
+ serverErrorWhat: "The record service could not answer that.",
713
+ serverErrorWhy: "The request reached the service and it returned a failure instead of a record. Nothing on this machine was changed.",
714
+ malformedWhat: "The record service returned an answer this version cannot read.",
715
+ malformedWhy: "The response was not in the form this version expects, so none of it was rendered rather than part of it being guessed at.",
716
+ readFix: "Try again, and report it if it continues:",
717
+ /**
718
+ * ⛔ VERSION SKEW — DISTINCT FROM `malformed`, AND THE DISTINCTION IS THE
719
+ * WHOLE POINT (`U1`, 2026-08-30).
720
+ *
721
+ * `parseBlameCommitPayload` is a STRICT ALLOWLIST on `state`: a value it does
722
+ * not know returns null, and null rendered as "malformed". So the day the
723
+ * server added a state, every client older than that day told the user its
724
+ * ANSWER WAS BROKEN — when the answer was fine and the CLIENT was old.
725
+ * `edge_unreadable` is the first such addition and it would have been the
726
+ * first casualty.
727
+ *
728
+ * ⚠ THE ROOT CAUSE, NOT THE SYMPTOM. Without this, every future state the
729
+ * server adds needs a lockstep client release FOREVER, and the coupling is
730
+ * invisible until a user hits it — worse here than in most protocols, because
731
+ * the vendored bundle in `claude-plugin/bin/` lags this source (10 commits at
732
+ * the time of writing) so "old client" is the NORMAL case, not the edge one.
733
+ *
734
+ * ## Why this is not a sixth ABSENCE state
735
+ *
736
+ * It is not an absence. Nothing is missing from the record — the server
737
+ * answered, and this build cannot render what it said. Same reason the
738
+ * shallow-clone refusal is not one: a REFUSAL and a SKEW are both about this
739
+ * client's limits, and `ABSENCE` is about the record's contents.
740
+ *
741
+ * ⚠ It still follows CR-112's grammar (absence · cause · remedy), because
742
+ * that grammar is what makes a dead end actionable, and here — unlike most of
743
+ * `ABSENCE` — the remedy is real: upgrading genuinely fixes it.
744
+ *
745
+ * ⚠ THE COPY MAY NOT NAME THE STATE IT COULD NOT READ. The whole premise is
746
+ * that this build does not know what that value means; printing it would
747
+ * invite the reader to interpret a token this client cannot interpret either.
748
+ */
749
+ skewWhat: "The record service sent an answer this version is too old to show.",
750
+ 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.",
751
+ skewFix: "Reinstall the VibeCommit plugin to pick up the current client, then run this again:",
752
+ /** The bearer is dead after one refresh and one retry. Exit 3, not 1. */
753
+ signedOutWhat: "This machine is not signed in to VibeCommit.",
754
+ signedOutWhy: "Reading a record needs a signed-in user, and no usable sign-in was found on this machine.",
755
+ signedOutFix: "Sign in, then run the command again:",
756
+ };
757
+ /**
758
+ * `vibecommit report` (CR-108).
759
+ *
760
+ * NOTE: this surface ships NO durability copy until the E2 metric is defined
761
+ * (D61 §PS6, and the claims-register `Durable / survives / durability` row).
762
+ *
763
+ * ⚠ THE TWO EMPTY-WINDOW STATES MOVED. They were `emptyNoCommits` and
764
+ * `emptyNoCapture` here, and they were the absence clause and NOTHING ELSE — no
765
+ * cause, no remedy. `CR-112` re-homed both into `ABSENCE` below, where they
766
+ * carry the full grammar and sit beside the three `why` states they have to
767
+ * agree with. Do not reintroduce a short form here: two half-states in one
768
+ * module and five whole ones in another is the drift `CR-112` exists to stop.
769
+ */
770
+ export const REPORT = {
771
+ coverageLabel: "Commits with a recorded session",
772
+ gradeMixLabel: "Evidence",
773
+ // --- D67's THREE-PART UNIT. They render TOGETHER OR NOT AT ALL. ---------
774
+ /**
775
+ * ⚠ **THE DEFINITION IS PART OF THE NUMBER, NOT A PREFACE.** D67 cleared
776
+ * §PS6's durability block on stated terms, and the first is that the
777
+ * definition, the percentage and the grade mix render together — *"without
778
+ * them, `observed` correlation-in-time is laundered into a KPI."* A build
779
+ * that ships the number alone does not lift the block.
780
+ *
781
+ * ⚠ **THE REF IS IN THE SENTENCE, ALWAYS** — *"a bare percentage is
782
+ * meaningless because the number is ref-relative."* `commit_coverage` makes
783
+ * `ref` a REQUIRED payload field for exactly this reason.
784
+ */
785
+ definition: (ref) => `Coverage is the share of the commits we hold a recorded session for that are still reachable from ${ref}.`,
786
+ rate: (reachable, held, ref, percent) => `${reachable} of ${held} commits with a recorded session are still reachable from ${ref} (${percent}%).`,
787
+ /** §10.3 draws `Of those:` and then the per-grade split. */
788
+ gradeMixIntro: "Of those:",
789
+ /**
790
+ * ⚠ **THE GRADE WORDS THEMSELVES MAY NOT REACH THE SCREEN**, so the mix is
791
+ * rendered through these labels — DESIGN.md §13.3 (grade words in `--json`
792
+ * only), D65 §DR4 (no grade word on screen), and `test/why.test.ts` already
793
+ * enforces it for the other read verb.
794
+ *
795
+ * ⚠ §10.3's DRAWING VIOLATES THAT RULE and this deliberately departs from it:
796
+ * it draws *"41 observed during a session"*, which prints the bare grade
797
+ * word. D113 §1 makes it worse than a style slip — `observed` is BLOCKED
798
+ * because it names **a mechanism that exists in no repo**, and its listed
799
+ * phrasings include *seen during the session* and *watched*, which are the
800
+ * two shortenings anyone would reach for next. The `observed` label below
801
+ * mirrors `grades.ts`'s own approved sentence, *"This commit appeared while
802
+ * the session was running"*, which says the same fact and claims no witness.
803
+ * The §10.3 fix is owed in `code/` and is not this package's to write.
804
+ */
805
+ // ⚠ FLAT KEYS, NOT A NESTED OBJECT, AND THAT IS A GATE CONSTRAINT RATHER
806
+ // THAN TASTE. `src/copy/claims.ts`'s walker calls `toLowerCase()` on every
807
+ // value it reaches, so a nested object under an exported copy block throws
808
+ // `text.toLowerCase is not a function` and takes the whole claims suite with
809
+ // it. Found by running it, not by reading the walker. Flat keys stay inside
810
+ // the gate; `claims.ts` is fenced this wave (`CR-132d`, W11) and the copy is
811
+ // what moves, not the checker.
812
+ gradeDerivedLabel: "recorded in the transcript",
813
+ gradeObservedLabel: "appeared while the session ran",
814
+ gradeDeclaredLabel: "reported by the agent",
815
+ /** `<n> <label>`, joined by the renderer. Singular and plural read alike. */
816
+ gradeCount: (count, label) => `${count} ${label}`,
817
+ /**
818
+ * ⚠ **THE SEPARATE LABELLED LINE, IN OUR WORDS** — D67: *"only exact
819
+ * `commit_sha_successors` rows count toward the numerator; patch-id matches
820
+ * render as a SEPARATE LABELLED 'probable' LINE and are never folded into the
821
+ * rate."*
822
+ *
823
+ * ⚠ **AND IT IS NOT THE SERVER'S SENTENCE.** `vibecommit-mcp`
824
+ * `src/read/coverage.ts` emits `probable_line.label` — a user-facing string
825
+ * minted in another repository, on this payload. D112 §3 ruled on exactly
826
+ * that shape: **the wire carries the fact, the client owns the words.** That
827
+ * ruling was measured in `src/read/blame.ts` and the class is NOT closed one
828
+ * file over, so this renders here and the wire's label is never printed.
829
+ */
830
+ probable: (count) => `${count} more matched by patch id, reported separately and not counted above.`,
831
+ probableOne: "1 more matched by patch id, reported separately and not counted above.",
832
+ // --- The refusals. A refusal is not an absence and not a sixth state. ---
833
+ /**
834
+ * ⚠ **A SEPARATE PHRASING FROM `WHY.shallowWhat`, AND THE REASON IS THAT IT
835
+ * IS A DIFFERENT FACT.** `why`'s says *"`git blame` cannot be trusted here"*,
836
+ * which is true of blame and says nothing about reachability. On a shallow
837
+ * clone the commits that would answer *"is this sha still reachable"* may
838
+ * simply not be present, so the rate would be wrong rather than missing. The
839
+ * REMEDY is genuinely the same fact, so `shallowFix` and `shallowFixCommand`
840
+ * are SHARED rather than re-worded — that is the duplication to avoid.
841
+ */
842
+ shallowWhat: "This is a shallow clone, so coverage cannot be computed here.",
843
+ shallowWhy: "Coverage asks which recorded commits are still reachable, and a shallow clone does not hold the history that answers it. The number would be wrong rather than missing.",
844
+ /**
845
+ * ⚠ FAILS CLOSED. If git cannot answer reachability for a commit we hold, the
846
+ * rate would be computed over a set we do not actually know — so it refuses.
847
+ * Counting an unanswerable probe as unreachable would DEFLATE the number
848
+ * silently, which is D98's class: no obviously-broken shape.
849
+ */
850
+ unknownReachabilityWhat: "Coverage could not be computed for every recorded commit.",
851
+ unknownReachabilityWhy: "Git could not say whether some recorded commits are still reachable from this ref. A share computed over the rest would be a smaller number presented as a complete one.",
852
+ unknownReachabilityFix: "Fetch the full history, then run this again:",
853
+ refWhat: "This command needs a ref it can name.",
854
+ refWhy: "Coverage is relative to a ref, so the answer is meaningless without one, and HEAD is detached here. Nothing was read and nothing was sent.",
855
+ refFix: "Name the ref:",
856
+ /**
857
+ * The window, as the two empty states name it.
858
+ *
859
+ * ⚠ **THE WINDOW BOUNDS THE LOCAL HALF ONLY.** `commit_coverage` accepts no
860
+ * `since`, so the server's totals are repository-wide and the RATE claims no
861
+ * window — see `report.ts`. This string appears only on the two empty states,
862
+ * which are answered from the clone.
863
+ */
864
+ window: (since) => `the window since ${since}`,
865
+ usageWhat: "This command needs a start date.",
866
+ usageWhy: "The window bounds which commits are counted, so the answer is not defined without one. Nothing was read and nothing was sent.",
867
+ usageFix: "Name the date the window starts:",
868
+ sinceWhat: "That start date could not be read.",
869
+ sinceWhy: "The window must be a calendar date, written as YYYY-MM-DD. A date git cannot read would silently widen the window.",
870
+ sinceFix: "Write the date as YYYY-MM-DD:",
871
+ };
872
+ export const ABSENCE = {
873
+ /**
874
+ * 1 — COLD START. Owner: `CR-086` (W9), and `CR-086` CORRECTED ITS SCOPE.
875
+ *
876
+ * ## ⚠ It said MACHINE; the state it names is a REPOSITORY
877
+ *
878
+ * The shipped text was *"No sessions have been recorded on this MACHINE
879
+ * yet"*, and neither half of that survived measurement. `why`'s answer is
880
+ * repository-scoped — the payload that selects this state answers for one
881
+ * repo — and a developer with capture running in three repos and none here
882
+ * would have been told capture had never run at all. Nothing in the client
883
+ * holds a machine-wide "never captured" fact either: `state.ts` keys sends by
884
+ * project.
885
+ *
886
+ * ## Why it does not render §10.6-A's two extra facts, and that is deliberate
887
+ *
888
+ * §10.6-A and §R3 draw *"That commit predates capture on this repo (first
889
+ * capture 2026-08-08). … 14 commits captured since."* **Both numbers are
890
+ * facts this client does not hold.** `state.ts` records `lastSendForRepo` —
891
+ * the LAST send, never a FIRST-capture date — and the commit count is
892
+ * `commit_coverage`'s, a server figure behind `CR-085`. D81's order applies:
893
+ * make the claim honest before building the mechanism.
894
+ *
895
+ * So the state says the thing that IMPLIES §10.6-A's headline without
896
+ * asserting either number: **if no session was ever recorded for this
897
+ * repository, then every commit in it predates capture** — including this
898
+ * one. That is §10.6-A's meaning, carried by a fact the payload can state.
899
+ * The date and the count are registered as owed (see the PR) rather than
900
+ * approximated, because a first-capture date guessed from a last-send record
901
+ * is D98's plausible-wrong-answer class exactly.
902
+ *
903
+ * ⚠ NO SIXTH STATE WAS ADDED, and none may be: `test/copy-absence.test.ts`
904
+ * asserts the EXACT set of five.
905
+ */
906
+ coldStart: {
907
+ what: "No session has been recorded for this repository yet.",
908
+ why: "Capture has not sent anything for this repository, so no commit in it has a conversation record — including this one.",
909
+ fix: "Connect this repository, then work as usual:",
910
+ exit: EXIT.empty,
911
+ },
912
+ /**
913
+ * 2 — NO-EDGE EMPTY. Capture is working; the commit `git blame` returned has
914
+ * no edge. Owner: `CR-086` (W9).
915
+ *
916
+ * ⚠ The `why` states a DEFINITION, not a diagnosis. "This commit was made
917
+ * without an agent" is the permanently BLOCKED claim (see `exit.ts`), and a
918
+ * sentence guessing at the cause of one absent record is one edit away from
919
+ * it. What is safe is what makes a record exist at all.
920
+ */
921
+ noEdge: {
922
+ what: "No recorded session is linked to this commit.",
923
+ why: "A commit is linked to a session only when capture was running for the session that produced it. This is a missing record, not a finding about the commit.",
924
+ fix: "Nothing to fix. A record cannot be added to a commit after the fact.",
925
+ exit: EXIT.empty,
926
+ },
927
+ /**
928
+ * 3 — `report` EMPTY WINDOW A: NO COMMITS. Owner: `CR-108` (W10).
929
+ *
930
+ * Names the WINDOW and the REF, because the answer is relative to both: there
931
+ * may be commits on another branch, or outside these dates, and a sentence
932
+ * that named neither would read as a claim about the repository.
933
+ *
934
+ * Written unqualified — see the clone contract above.
935
+ */
936
+ reportNoCommits: {
937
+ what: (ref, window) => `No commits on ${ref} in ${window}.`,
938
+ why: (ref) => `The window and the ref bound this answer. Commits on a branch other than ${ref}, or outside these dates, were not counted.`,
939
+ fix: "Widen the window, or name a different ref, and run this again.",
940
+ exit: EXIT.empty,
941
+ },
942
+ /**
943
+ * 4 — `report` EMPTY WINDOW B: COMMITS, NO SESSIONS. Owner: `CR-108` (W10).
944
+ *
945
+ * ⚠ 3 and 4 are DIFFERENT FACTS and collapsing them reads as a coverage claim
946
+ * we cannot make. The counted commits are the whole difference: this window
947
+ * has them and has no session for them, which is not the same as having
948
+ * nothing in it.
949
+ */
950
+ reportNoSessions: {
951
+ what: (count, window) => `No recorded sessions for the ${count} commits in ${window}.`,
952
+ why: "A commit has a recorded session only when capture was running for the session that produced it. The window bounds this answer.",
953
+ fix: "Work with capture on, then run this again over a window that includes those commits.",
954
+ exit: EXIT.empty,
955
+ },
956
+ /**
957
+ * 5 — SQUASH-RESOLVED. `git blame` returns the squashed SHA on `main` while
958
+ * every edge we hold binds a pre-squash original. Owner: `CR-109`.
959
+ *
960
+ * ⚠ D65 §DR5: if the blamed SHA is not a SHA we hold an edge for, the header
961
+ * MAY NOT claim the transcript recorded it. **Both SHAs render** — a
962
+ * single-SHA sentence here is exactly the overclaim this state exists to
963
+ * prevent. This is GitHub's DEFAULT merge button, so it is the common path.
964
+ */
965
+ squashResolved: {
966
+ what: (blamed, recorded) => `This line is attributed to ${blamed}; the session we hold was recorded against ${recorded}.`,
967
+ why: (blamed) => `The original commit was squashed when it merged, so ${blamed} is not a commit that capture recorded. The session below is the record of the original, and it is not a record of ${blamed}.`,
968
+ fix: "Nothing to fix. Read the session as the record of the original commit.",
969
+ exit: EXIT.empty,
970
+ },
971
+ /**
972
+ * 6 — EDGE UNREADABLE. ⛔ THE SET IS NOW SIX, AND THIS IS THE FIRST ADDITION.
973
+ * Owner: `U1`. ⛔ **FOUNDER-RATIFIED, 2026-08-31 — `D203`.** Not an
974
+ * implementer's judgement call: three independent tripwires forbade a sixth
975
+ * state, they all fired, and the decision to clear them was put to the
976
+ * founder and ruled rather than argued past. `D203` carries the reasoning and
977
+ * the cost that was spent (reversibility — a field promotes to a state later,
978
+ * a shipped state does not un-ship).
979
+ *
980
+ * ⚠ The tripwires are CLOSURE TRIPWIRES, not a cap, and they did exactly what
981
+ * they exist for: this state could not enter the set without a reviewer being
982
+ * made to read the argument for it.
983
+ *
984
+ * ## ⚠ THIS IS THE ONE ABSENCE STATE THAT IS NOT AN ABSENCE OF RECORD
985
+ *
986
+ * The other five say some version of "we hold nothing here". This one says
987
+ * the OPPOSITE: **we hold a link for this commit and could not render the
988
+ * conversation behind it.** Reading it as "no session" inverts it.
989
+ *
990
+ * It exists because mcp used to return `no_edge` for this case, which
991
+ * reported "this repository is captured and this commit has no linked
992
+ * session" about a commit we demonstrably hold a link for — a false negative
993
+ * on the only question `why` asks. `blame.ts` now emits `edge_unreadable`
994
+ * and this is its rendering.
995
+ *
996
+ * ⚠ IT IS THE DIRECT-EDGE TWIN OF `squashResolved`, which already carries
997
+ * exactly this shape one hop later (a mapping we resolved whose evidence will
998
+ * not render). Same fact, different route to it. That symmetry is the reason
999
+ * this is a sixth state rather than a caveat note: `attributionNote`'s shape
1000
+ * is a caveat on a screen WITH content, and this screen has none.
1001
+ *
1002
+ * ## ⚠ THE `fix` MAY NOT PROMISE A REPAIR
1003
+ *
1004
+ * CR-112 requires the remedy to say what would CHANGE the state, and here the
1005
+ * honest answer is often "nothing the reader can do": if the turn content was
1006
+ * erased, or was never uploaded, the evidence is gone and the link stays.
1007
+ * Saying "re-run capture" would be a plausible-wrong-answer (D98) — it
1008
+ * repairs neither case. So the line states the two conditions and stops.
1009
+ *
1010
+ * ⚠ NOT "corrupt", "lost", or "missing data" — all three are diagnoses of a
1011
+ * cause this client did not measure. What we know is that the server held a
1012
+ * link and returned no readable turns; the reason is the server's and is not
1013
+ * on the wire.
1014
+ */
1015
+ edgeUnreadable: {
1016
+ what: "A recorded session is linked to this commit, but its conversation could not be read.",
1017
+ why: "The link between this commit and a session is on record. The turns behind that link came back without readable content, so there is a record here and no conversation to show for it. This is a gap in what can be displayed, not a missing link.",
1018
+ fix: "Nothing to fix from here. Turn content can be unreadable because it was erased on request or was never uploaded, and neither is repaired by re-running capture.",
1019
+ exit: EXIT.empty,
1020
+ },
1021
+ };
1022
+ // ⚠ NO `ALL_ABSENCE_STATES` EXPORT HERE, DELIBERATELY. The expected set lives in
1023
+ // `test/copy-absence.test.ts`, because a list exported from this file and then
1024
+ // compared against this file's own keys asserts nothing — it is the tautology
1025
+ // the brief for this task names. `test/copy-grades.test.ts` keeps its expected
1026
+ // table in the test for the same reason.
1027
+ /** Failure-class copy (CR-018). Three classes: later / never / fatal. */
1028
+ export const ERRORS = {
1029
+ notConnected: "This repository is not connected. Run `vibecommit connect`.",
1030
+ networkLater: "Could not reach VibeCommit. This session will be recorded on the next turn.",
1031
+ authFatal: "VibeCommit could not authenticate. Run `vibecommit connect` to reconnect.",
1032
+ payloadNever: "This session was too large to record and has been skipped.",
1033
+ notAGitRepo: "Not a git repository.",
1034
+ };
1035
+ /**
1036
+ * The interactive top-level catch (CR-022).
1037
+ *
1038
+ * **The hook half of the contract does not apply here** (D61 round 3 §D8). A hook
1039
+ * swallows everything and exits 0 because a non-zero hook derails the developer's
1040
+ * live turn; an interactive verb that did the same would report success for work
1041
+ * it did not do. So an unexpected exception gets §13.6's shape — what, why, fix —
1042
+ * and exit 1.
1043
+ *
1044
+ * ⚠ The `why` line says only what we actually know. "Nothing was uploaded" would
1045
+ * be a claim, not a fact: an exception can land after a send, and copy that
1046
+ * guesses in the reassuring direction is the failure the claims register exists
1047
+ * to stop.
1048
+ */
1049
+ export const INTERNAL = {
1050
+ unexpectedWhat: "VibeCommit stopped on an unexpected error.",
1051
+ unexpectedWhy: "The command did not finish.",
1052
+ unexpectedFix: "Run it again with the error detail shown:",
1053
+ unexpectedDebugCommand: "VIBECOMMIT_DEBUG=1 vibecommit <command>",
1054
+ };
1055
+ /**
1056
+ * Credential-loading failures (CR-016).
1057
+ *
1058
+ * §13.6's shape — what, why, fix, fix last. **The `what` line never has "you" as
1059
+ * its subject** and never carries a bare HTTP status. None of these strings
1060
+ * contains any part of a credential, and none can: the plaintext only leaves
1061
+ * `IngestCredential` through `expose()`, which no copy path calls.
1062
+ */
1063
+ export const CREDENTIAL = {
1064
+ wrongClassWhat: "The credential set for this machine is not an ingest credential.",
1065
+ wrongClassEnvWhy: "VIBECOMMIT_TOKEN is set, but its value is not a VibeCommit ingest credential. It was not sent anywhere.",
1066
+ wrongClassFileWhy: "The credential on disk is not a VibeCommit ingest credential. It was not sent anywhere.",
1067
+ wrongClassFix: "Mint a fresh one:",
1068
+ insecureFileWhat: "The credential file is readable by other accounts on this machine.",
1069
+ insecureFileWhyLabel: "file",
1070
+ insecureFileModeLabel: "mode",
1071
+ insecureFileWhy: "It was not read. A credential file must be readable only by its owner.",
1072
+ insecureFileFix: "Restore the mode, then reconnect:",
1073
+ unreadableWhat: "The credential file could not be read.",
1074
+ unreadableWhy: "It exists but does not contain a credential in the expected form.",
1075
+ unreadableFix: "Replace it by reconnecting:",
1076
+ };
1077
+ /**
1078
+ * The Node floor (D57 plan §DX11). `NODE_FLOOR_TEXT` and the running version are
1079
+ * interpolated by the caller so there is still exactly one copy of the sentence.
1080
+ */
1081
+ export const RUNTIME = {
1082
+ floorWhat: "VibeCommit needs a newer Node.",
1083
+ floorWhyPrefix: "This package requires Node",
1084
+ floorWhyMiddle: "or newer. It is running on Node",
1085
+ floorFix: "Upgrade Node, then run:",
1086
+ };
1087
+ /**
1088
+ * The `systemMessage` channel — style guide §10.1, three states, once per
1089
+ * session (D57 plan §DX3).
1090
+ *
1091
+ * ⚠ Four rules that are easy to break and expensive to notice:
1092
+ * 1. ONE LINE each. Two lines in an agent transcript reads as tool noise and
1093
+ * gets tuned out, which returns us to the failure DX3 exists to fix.
1094
+ * 2. NO SGR ESCAPES, ever. This is a JSON string field rendered by Claude
1095
+ * Code's own UI; embedded escapes are undefined behaviour (DESIGN.md §13.9).
1096
+ * 3. All three are FAILURE states. The positive fourth state (`capture
1097
+ * confirmed`) is `CR-023`, and it is a two-clause amendment — it inverts
1098
+ * "stdout empty on the happy path". Do not add it here as if it were a
1099
+ * shape-preserving addition.
1100
+ * 4. No URL longer than the line.
1101
+ */
1102
+ export const SYSTEM_MESSAGE = {
1103
+ notConnected: "VibeCommit is installed but not connected. Run `vibecommit connect` to start capturing.",
1104
+ credentialRevoked: "VibeCommit capture stopped: this machine's credential was revoked. Run `vibecommit connect`.",
1105
+ /** Templated on both versions — style guide §10.1 state 3. */
1106
+ unsupportedRuntime: (floor, running) => `VibeCommit needs Node ${floor} or newer; hooks are running on Node ${running}. Nothing is being captured.`,
1107
+ /**
1108
+ * State 4 — style guide §10.1, and the ONLY positive one. `CR-023d`.
1109
+ *
1110
+ * Templated on the URL rather than inlining it, for the same reason
1111
+ * `unsupportedRuntime` is templated: `URLS` is declared below this object, so
1112
+ * a direct reference would read it in its temporal dead zone.
1113
+ *
1114
+ * ⚠ The style guide's example ends `.../app/commits/a8f2c91`, which is
1115
+ * ILLUSTRATIVE and not constructible here. A hook cannot know which commit a
1116
+ * session will resolve to — that mapping happens server-side and
1117
+ * asynchronously, long after this process has exited — so the generic
1118
+ * dashboard URL is substituted. See the PR.
1119
+ *
1120
+ * Claims-register: "captured" is not a blocked phrase, and it is a weaker
1121
+ * claim than the register's own boundary line ("the record is append-only for
1122
+ * users and its hash chain detects post-write tampering").
1123
+ */
1124
+ captureConfirmed: (dashboardUrl) => `VibeCommit captured this session. ${dashboardUrl}`,
1125
+ };
1126
+ /**
1127
+ * The out-of-tree redaction marker — `CR-024d`, D57 §OV8d.
1128
+ *
1129
+ * User-visible: it lands in the record a customer reads back, so it clears
1130
+ * `docs/claims-register.md`. It says WHAT HAPPENED and nothing about how safe
1131
+ * anything is — no *secure / safe / protected / private / encrypted* — and it
1132
+ * claims no completeness, because OV8d's redaction is explicitly PARTIAL
1133
+ * (`capture-refactor-three-buyers.md`:387) and "complete / all / every" is a
1134
+ * blocked claim unless the count and the selection rule render inline.
1135
+ *
1136
+ * The byte count IS the count rendering inline, and "outside this repository"
1137
+ * is the selection rule. Both are literally true of the span replaced.
1138
+ *
1139
+ * Shape borrowed from the server scrubber's `[REDACTED:<kind>]` — read for its
1140
+ * shape only, never imported: this package is MIT and `vibecommit-mcp` is
1141
+ * closed, so lifting code across would be a licensing violation (D60 §D1a).
1142
+ */
1143
+ export const REDACTION = {
1144
+ /** Padded to the exact byte length of what it replaces — see `redact.ts`. */
1145
+ head: (bytes) => `[OUT-OF-TREE: ${bytes} bytes withheld`,
1146
+ /** Fill between head and tail. ASCII, one byte per column. */
1147
+ fill: ".",
1148
+ tail: "]",
1149
+ /** For spans too short for the counted form. */
1150
+ short: "[OUT-OF-TREE]",
1151
+ };
1152
+ /** Commands the copy points at. One definition, so a rename cannot half-land. */
1153
+ export const COMMANDS = {
1154
+ connect: "vibecommit connect",
1155
+ /** `CR-084d`. A FLAG, not a verb — `--help`'s verb list and its golden file do not move. */
1156
+ signIn: "vibecommit connect --sign-in",
1157
+ status: "vibecommit status",
1158
+ /**
1159
+ * `CR-108`. The ARGUMENT SHAPE, shown once so the usage error and the retry
1160
+ * fix line cannot drift apart — the same convention `why` uses.
1161
+ */
1162
+ report: "vibecommit report --since 2026-07-08",
1163
+ /** The ref form, for the detached-HEAD refusal. */
1164
+ reportRef: "vibecommit report --since 2026-07-08 --ref main",
1165
+ off: "vibecommit off",
1166
+ /**
1167
+ * `CR-086`. The ARGUMENT SHAPE, shown once so the usage error and the retry
1168
+ * fix line cannot drift apart. There is no argument parser in this package —
1169
+ * `connect` reads `argv.includes("--sign-in")` and that is the whole
1170
+ * convention — so `file:line` is the smallest shape that satisfies the
1171
+ * verbatim-approved `--help` sentence, and it is the form editors and `grep`
1172
+ * already print.
1173
+ */
1174
+ why: "vibecommit why src/auth/session.ts:47",
1175
+ chmodCredentials: "chmod 600 ~/.vibecommit/credentials.json",
1176
+ /**
1177
+ * ⛔ THE INSTALL COMMAND, TRANSCRIBED FROM `claude-plugin/README.md:12` — and
1178
+ * it is deliberately NOT an invented update verb (`U1`, 2026-08-30).
1179
+ *
1180
+ * The skew remedy wants "get a newer client". There is **no documented update
1181
+ * command anywhere in this product**: grepped `claude-plugin/README.md`, its
1182
+ * manifests, and this package — the README documents `marketplace add` and
1183
+ * `install`, and nothing else. Printing `/plugin update vibecommit` because it
1184
+ * reads plausibly is D98's class exactly: a command-shaped string that may not
1185
+ * exist, handed to a user who is already stuck.
1186
+ *
1187
+ * So the fix line names the install command that IS documented, and
1188
+ * `WHY.skewFix` is worded as REINSTALL rather than update, which is true of
1189
+ * this string. ⚠ If a real update verb is ever documented, change both
1190
+ * together — the wording and the command are one unit.
1191
+ */
1192
+ pluginInstall: "/plugin install vibecommit@vibecommit-capture",
1193
+ };
1194
+ /**
1195
+ * Every URL the CLI prints. Here rather than inline for the same reason as the
1196
+ * rest: a link is user-visible copy, the claims gate can only check what it can
1197
+ * find, and `CR-114` is moving `/app`'s information architecture underneath us.
1198
+ */
1199
+ export const URLS = {
1200
+ dashboard: "https://vibecommit.ai/app",
1201
+ settings: "https://vibecommit.ai/app/settings",
1202
+ /**
1203
+ * The captures INDEX — `CR-025`. Style guide §10.2 draws
1204
+ * `/app/commits/<a-sha>`, and that route DOES NOT EXIST: `CR-114` (W3) created
1205
+ * `app/app/commits/page.tsx`, a flat index, with no `[sha]` segment and no
1206
+ * rewrite, so a per-commit URL 404s. Verified against `vibecommit-web`
1207
+ * `origin/main` rather than assumed.
1208
+ *
1209
+ * Printing a URL that 404s at the moment of highest trust is the specific
1210
+ * failure this ending exists to avoid. `CR-101` (W10) adds the detail page;
1211
+ * deepening this constant is that task's, not a later guess.
1212
+ */
1213
+ commits: "https://vibecommit.ai/app/commits",
1214
+ /**
1215
+ * Where an org member goes to see their pending capture request — `CR-110`.
1216
+ *
1217
+ * ⚠ VERIFIED BY FILE, NOT BY PROBE. Every `/app/*` path on that site returns
1218
+ * an identical 307 whether or not the route exists, so a 307 is NEVER evidence
1219
+ * a route is real. This one was confirmed by reading
1220
+ * `vibecommit-web/app/app/settings/capture-access/page.tsx` on `origin/main`,
1221
+ * with `app/api/capture-access/{policy,decide}/route.ts` beside it (`CR-035c`).
1222
+ *
1223
+ * Deeper than the style guide's drawn `/app`, deliberately: the beat's whole
1224
+ * job is to be actionable, and the generic dashboard does not show the request.
1225
+ */
1226
+ captureAccess: "https://vibecommit.ai/app/settings/capture-access",
1227
+ /**
1228
+ * The public bundle that vendors the exact binary — `CR-111d`, D20, D64.
1229
+ *
1230
+ * ⚠ THE ONLY HONEST TRUST OFFER LEFT. D64 made `vibecommit-capture` PRIVATE
1231
+ * (MIT intact — the licence is the invariant, the visibility is not), so
1232
+ * linking this repo would be linking a 404 and claiming readable source would
1233
+ * be false. D20 keeps `claude-plugin` public non-negotiably.
1234
+ *
1235
+ * ⚠ AND THE SENTENCE BESIDE IT IS ONLY TRUE ONCE `CR-029e` LANDS. Measured at
1236
+ * `claude-plugin` `1349896`, `bin/` was vendored from `vibecommit-capture`
1237
+ * `8e772f1` — nine commits and eight tasks behind, with no `redact.js` at all.
1238
+ * `CR-029e` re-vendors it. Written as §10.2 draws it, with the dependency
1239
+ * disclosed in the PR rather than silently shipped.
1240
+ */
1241
+ pluginBundle: "https://github.com/Vibe-Commit/claude-plugin",
1242
+ };
1243
+ //# sourceMappingURL=strings.js.map