@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,390 @@
1
+ /**
2
+ * The commit spool — where `post-commit` leaves what it OBSERVED, for the next
3
+ * Claude Code hook to deliver (`CR-170`, D154).
4
+ *
5
+ * ## Why a spool at all, rather than sending from `post-commit`
6
+ *
7
+ * `post-commit` runs inside `git commit`. A network call there would put our
8
+ * latency on the user's own commit, and a failure would surface as noise in a
9
+ * command that has nothing to do with us. So the commit hook does the one thing
10
+ * only it can do — witness the commit — and writes a line. Delivery stays on the
11
+ * hook path that already has a credential, a budget and a retry story.
12
+ *
13
+ * ## ⛔ APPEND, THEN TRUNCATE ON 2xx — NEVER READ-AND-DELETE
14
+ *
15
+ * A send that fails must leave the commit in the spool so the NEXT hook retries
16
+ * it. Deleting on read would lose a commit to a single 500, permanently, and
17
+ * `capture_commits` rows cannot be retracted (D105/D108). So the truncation is
18
+ * the LAST step and it is conditional on the server's 2xx.
19
+ *
20
+ * ## ⛔ THE SESSION GATE IS ABOUT WHICH SESSION, NOT ABOUT WHO TYPED IT
21
+ *
22
+ * ⚠ **This block used to say the gate is what keeps a HUMAN'S commits out. That
23
+ * has been FALSE since `CR-170` shipped** — `activeSessionFor` has never
24
+ * inspected authorship, and `post_commit.ts` says three lines below its own copy
25
+ * of the claim that the hook *"fires for every commit in this work tree,
26
+ * including ones no agent was involved in."* Corrected as a comment fix; no
27
+ * behaviour moved with it (`D5`).
28
+ *
29
+ * What the gate actually asks is whether a capture session is demonstrably live
30
+ * for this repo, and — since the ladder below — **WHICH ONE**. A line is written
31
+ * only when that question has an answer, and the answer carries the rung that
32
+ * produced it so the server can tell an observation from a guess.
33
+ *
34
+ * ## The line is deliberately small, and the file list stays here
35
+ *
36
+ * `{sha, branch, at, files}`. Only the SHA reaches the wire in wave 1 (`CR-170`
37
+ * §4): commit messages were MEASURED at 9/20, 8/20 and 5/20 commits over 4096
38
+ * bytes across the three repos, largest 12,112, so a quarter of real commits
39
+ * would land permanently truncated in a header. Metadata is backfillable from
40
+ * the sha; the observation is not. `files` is carried for wave 2 and never put
41
+ * in a header.
42
+ */
43
+ import { type SessionKey } from "./paths.js";
44
+ /**
45
+ * ⛔ **WHICH RUNG OF THE LADDER ANSWERED — the difference between an observation,
46
+ * a guess, and a refusal (`D190`).**
47
+ *
48
+ * Two of these four may reach the wire. The other two may not, and the asymmetry
49
+ * is the whole point:
50
+ *
51
+ * - `env_session_id` — the committing process told us, `M1`. An OBSERVATION.
52
+ * - `sole_live_session` — one session is live in this clone and nothing
53
+ * contradicts it. Weaker, but unambiguous.
54
+ * - ⛔ `recency_heuristic` — **two or more candidates and nothing to choose
55
+ * between them.** The pre-ladder answer, and the defect this wave exists to
56
+ * close: `cr071:157` makes a wrong edge PERMANENT, UPDATE and DELETE both
57
+ * raise, `service_role` included. **HELD, never sent.**
58
+ * - ⛔ `env_session_unmatched` — **the committing process named a session, and
59
+ * no live session here is it.** A REFUSAL, and a different one from *"no
60
+ * session is live at all"*. **Never sent, and never a `commit_attributions`
61
+ * row either.**
62
+ *
63
+ * ## ⛔ WHY THE LAST TWO ARE NOT ONE VALUE, AND IT IS STRUCTURAL
64
+ *
65
+ * The first draft of this ladder called the unmatched-env case
66
+ * `recency_heuristic`, and that is **wrong by a server constraint this repository
67
+ * cannot see**: `commit_attributions_ambiguity_is_plural` requires
68
+ * `cardinality(candidate_session_ids) >= 2`, so a `recency_heuristic` row naming
69
+ * ONE candidate is refused **23514**.
70
+ *
71
+ * ⛔ **CITED TO `D197 §3`, WHICH IS WHERE THAT ARGUMENT LIVES IN FULL** — including
72
+ * the constraint's own comment, verbatim: *"ONE live candidate is the
73
+ * `sole_live_session` rung and writes an edge, ZERO is the refusal that writes
74
+ * nothing anywhere. A writer that mis-walks the ladder gets a 23514 rather than a
75
+ * hold nobody can resolve."*
76
+ *
77
+ * ⚠ **I have NOT read the constraint in the closed schema**, so it is cited to a
78
+ * decision entry anyone here can open rather than characterised from a repository
79
+ * this package must not depend on (`feedback_characterization_is_not_citation`).
80
+ *
81
+ * ⛔ **And citing the DECISION rather than the sibling repo is deliberate, not
82
+ * cosmetic.** `test/provenance.test.ts` reads a sibling-repo name in `src/` as an
83
+ * undeclared closed-core reference, and a new declaration is a LICENSING REVIEW
84
+ * EVENT (D60 §D1a) requiring a human in the PR body. Nothing here was retyped out
85
+ * of closed source, so a declaration would assert a reading that did not happen —
86
+ * and pointing at `D197 §3` removes the event instead of adding one.
87
+ *
88
+ * ⚠ Both of that guard's tripwires fired on the prose of this docblock while it
89
+ * was being written — the repo name, and then the marker tag's own literal name
90
+ * inside a sentence disclaiming it. ⛔ A grep over a repo name cannot tell a
91
+ * citation from a disclaimer (`feedback_gate_on_the_phrase_not_the_token`); the
92
+ * wording moved and the guards were left alone.
93
+ *
94
+ * ⛔ And the old ladder really could produce that row: an unmatched env id with
95
+ * **exactly one** live candidate fell through to `recency_heuristic`, cardinality
96
+ * one. ⚠ Not reachable from this client TODAY — no held commit is ever
97
+ * transmitted — so this is a defect in the VOCABULARY rather than a live 23514.
98
+ * It would have become one the moment a later wave sent held rows, which is
99
+ * precisely when nobody would be looking at this file.
100
+ *
101
+ * ⚠ **The two also differ in meaning, not just in cardinality.**
102
+ * `recency_heuristic` says *we could not tell which of several*;
103
+ * `env_session_unmatched` says *we were told, and the answer names nobody here*.
104
+ * The second is the more certain statement of the two, and collapsing it into the
105
+ * first records a doubt we do not have.
106
+ *
107
+ * ⚠ **The wire vocabulary is the first two** — `WIRE_RUNGS`, below, and `capSpool`
108
+ * is where the other two stop.
109
+ */
110
+ export type AttributionRung = "env_session_id" | "sole_live_session" | "recency_heuristic" | "env_session_unmatched";
111
+ /** One observed commit. */
112
+ export interface SpoolEntry {
113
+ /** Full 40- or 64-hex. ⛔ Never abbreviated — see `capSpool`. */
114
+ readonly sha: string;
115
+ /** Null on a detached HEAD. */
116
+ readonly branch: string | null;
117
+ /** ISO-8601, from git's own `%cI`. */
118
+ readonly at: string;
119
+ /** Paths touched. Wave 2. ⛔ Never a header. */
120
+ readonly files: readonly string[];
121
+ /**
122
+ * ⛔ **HOW THE SESSION ON THIS LINE WAS DECIDED.** Recorded at OBSERVATION
123
+ * time, because the evidence is gone by the time the next hook fires: the env
124
+ * variable belonged to the committing process, and the mtimes have moved on.
125
+ */
126
+ readonly attribution: AttributionRung;
127
+ }
128
+ /**
129
+ * How many SHAs one hook may put on the wire.
130
+ *
131
+ * A header has a practical size bound and the server pins its own; 40 hex plus a
132
+ * separator is 41 bytes, so 32 is ~1.3 KB and comfortably inside any of them.
133
+ * ⛔ **The rest stay spooled** rather than being dropped — a burst of commits is
134
+ * delivered across several hooks, and hooks are frequent.
135
+ */
136
+ export declare const MAX_SPOOLED_SHAS = 32;
137
+ /**
138
+ * A session is "live" for this repo if its state file was touched inside this
139
+ * window.
140
+ *
141
+ * ⚠ **A JUDGEMENT CALL, AND IT IS THE ONE KNOB IN THIS MODULE.** The Claude Code
142
+ * hook writes session state on every fire, so during active work this file is
143
+ * touched every turn. Too short and a long turn's commit is missed; too long and
144
+ * a human's commit hours later is attributed to a session that is morally over.
145
+ *
146
+ * The asymmetry decides it, the same way it did in `spawn_budget.ts`: a MISSED
147
+ * commit is recoverable — the sha is still in git and wave 2 can backfill it —
148
+ * while a WRONGLY ATTRIBUTED commit is a permanent row linking work to a session
149
+ * that did not do it. So this is sized to the length of a plausible single turn,
150
+ * not to the length of a working day.
151
+ */
152
+ export declare const SESSION_LIVE_WINDOW_MS: number;
153
+ /**
154
+ * `<repo>/<session>.spool.jsonl`, beside the session's state file.
155
+ *
156
+ * ⚠ **`.jsonl`, NOT `.json`, and that is load-bearing.** `lastSendForRepo`
157
+ * scans this same directory and filters on `entry.endsWith(".json")`; a spool
158
+ * named `.json` would be parsed as session state, fail, and silently degrade
159
+ * every `vibecommit status` for the repo. Checked rather than assumed — there is
160
+ * a test for exactly that.
161
+ *
162
+ * Null on no repo identity, for the reason `sessionStatePath` is: there is no
163
+ * repo-less bucket and a placeholder key is a cross-repo bleed with an extra step.
164
+ */
165
+ export declare function spoolPath(home: string, key: SessionKey): string | null;
166
+ /**
167
+ * Append one observed commit. Returns false on any failure.
168
+ *
169
+ * ⛔ **NEVER THROWS.** This runs inside `git commit`: an exception here would
170
+ * surface as a failed hook on the user's own commit, which is exactly the noise
171
+ * `post-commit` must not create. A dropped line is strictly better.
172
+ *
173
+ * `appendFileSync` with `a` is a single positioned write, so two commits racing
174
+ * in one repo interleave whole lines rather than corrupting each other.
175
+ */
176
+ export declare function appendSpool(home: string, key: SessionKey, entry: SpoolEntry): boolean;
177
+ /**
178
+ * Every well-formed entry in the spool, oldest first.
179
+ *
180
+ * A malformed line is SKIPPED rather than fatal: the file is appended to by a
181
+ * different process than reads it, so a torn final line is a real possibility
182
+ * and losing one observation beats losing the whole spool.
183
+ */
184
+ export declare function readSpool(home: string, key: SessionKey): SpoolEntry[];
185
+ /**
186
+ * Drop the first `count` entries — called ONLY after the server's 2xx.
187
+ *
188
+ * ⚠ **RE-READS BEFORE REWRITING, and that is not defensive tidiness.**
189
+ * `post-commit` may have appended while the send was in flight. Slicing the
190
+ * array we read BEFORE the send would rewrite the file without those lines and
191
+ * lose them silently. Appends only ever go to the END, so the first `count`
192
+ * lines are still the same ones we delivered, and dropping exactly that many is
193
+ * correct against the current file rather than against a stale snapshot.
194
+ *
195
+ * ⚠ **A residual race remains and is accepted:** a commit landing between this
196
+ * re-read and the write is lost. The window is one `readFileSync` plus one
197
+ * `writeFileSync` on a small file, the cost is one un-delivered commit row, and
198
+ * closing it properly means a lock file on a path that must never block
199
+ * (`post-commit` runs inside the user's `git commit`). Recorded rather than
200
+ * hidden.
201
+ */
202
+ export declare function dropSpooled(home: string, key: SessionKey, count: number): boolean;
203
+ /** Which session answered, and which rung answered it. */
204
+ export interface ActiveSession {
205
+ readonly sessionId: string;
206
+ readonly attribution: AttributionRung;
207
+ }
208
+ /**
209
+ * Which session, if any, is live for this repo right now — ⛔ **AND HOW WE KNOW.**
210
+ *
211
+ * ## The defect this replaces
212
+ *
213
+ * Until the ladder, this function returned *the most recently mtime'd state file
214
+ * within 30 minutes* and said nothing about its own confidence. Two agents in one
215
+ * clone: session A commits, B's state file is newer, **A's commit enters B's
216
+ * spool** — and `cr071:157` makes the resulting edge PERMANENT, with UPDATE and
217
+ * DELETE both raising for `service_role` too. A wrong edge is forever, so the
218
+ * guess had to stop being indistinguishable from the answer.
219
+ *
220
+ * ## The ladder
221
+ *
222
+ * ```
223
+ * env id names a live session ─────────> env_session_id WRITE
224
+ * no live session at all ──────────────> null REFUSE (no row)
225
+ * env id names NONE of them ───────────> env_session_unmatched REFUSE (named)
226
+ * exactly ONE candidate ───────────────> sole_live_session WRITE (D5)
227
+ * TWO OR MORE candidates ──────────────> recency_heuristic HOLD
228
+ * ```
229
+ *
230
+ * ⛔ **`sole_live_session` ALWAYS WRITES, and that is not a change.** It never
231
+ * inspected authorship and does not start now: a human's `git commit` in a clone
232
+ * with one live session produces a spool line today, at HEAD, and
233
+ * `test/post-commit-spool.test.ts` has a green arm asserting exactly that. What
234
+ * is new is that the server will write a PERMANENT row off this rung, so the rung
235
+ * has to be named on the wire (`D190`).
236
+ *
237
+ * ## ⛔ `env_session_unmatched` — THE THIRD ARM, AND IT IS A REFUSAL WITH A NAME
238
+ *
239
+ * An env id that names **none** of the live candidates is a shape the plan's
240
+ * ladder does not draw, and the code must answer it. It is REAL rather than
241
+ * hypothetical: `agents/registry.ts:11-13` rejects an environment variable as the
242
+ * agent selector precisely because **Codex exports `CLAUDE_PLUGIN_ROOT` itself**,
243
+ * so a Codex agent running under an outer Claude session carries a
244
+ * `CLAUDE_CODE_SESSION_ID` that corroborates nothing here.
245
+ *
246
+ * - ⛔ `sole_live_session` there would name a session we have POSITIVE evidence
247
+ * did not commit — the permanent wrong edge, arrived at from the other
248
+ * direction. Never.
249
+ * - ⛔ `recency_heuristic` is what this ladder used first, and it is wrong for
250
+ * the reason `AttributionRung` records: cardinality one, refused **23514**,
251
+ * and a doubt we do not actually have.
252
+ * - ⛔ **A silent refusal is worse than either in one respect** — nobody can
253
+ * afterwards ask *why did that commit vanish*. That is the failure class this
254
+ * whole wave exists to close, so the refusal is RECORDED and NAMED.
255
+ *
256
+ * ⚠ **The line goes in the recency winner's spool bucket, because that is the
257
+ * only bucket that exists** — an uncorroborated env id deliberately does not open
258
+ * one of its own. The bucket is a FILE LOCATION and the rung is the attribution:
259
+ * the line says, in terms, *this commit is attributed to nobody, and here is
260
+ * why*. It never reaches `x-commits` and never becomes a `commit_attributions`
261
+ * row.
262
+ *
263
+ * ⚠ **So `recency_heuristic` is reachable ONLY with two or more candidates**,
264
+ * which is what `commit_attributions_ambiguity_is_plural` demands. That is a
265
+ * property of the ladder's SHAPE now rather than of a caller's care, and
266
+ * `test/post-commit-spool.test.ts` asserts it directly.
267
+ *
268
+ * ⚠ **A commit made before a session's FIRST hook has fired is still NOT
269
+ * captured**, because the state file it would key on does not exist yet. The env
270
+ * id could key a bucket that has no state file, and deliberately does not: the
271
+ * ladder's first rung requires CORROBORATION, so a stale or inherited variable
272
+ * cannot open a spool of its own.
273
+ *
274
+ * `envSessionId` is a PARAMETER rather than a read of `process.env` here, and it
275
+ * has no default: the compiler is the only thing that will notice a caller which
276
+ * forgot the env path, and this seam has no other check.
277
+ */
278
+ export declare function activeSessionFor(home: string, repoKey: string, envSessionId: string | null, nowMs?: number, windowMs?: number): ActiveSession | null;
279
+ /**
280
+ * ⛔ **THE TWO RUNGS THAT MAY GO ON `x-commit-attributions` (`D190`).**
281
+ *
282
+ * The server's vocabulary is exactly these two (`edge_derivation.ts:178`) and a
283
+ * rung outside it is **skipped silently** — no edge, no error, no log. So the
284
+ * refusal lives here, on the producing side, rather than being discovered as a
285
+ * zero where edges should have been.
286
+ *
287
+ * ⚠ **This constant is NOT what the wire cell asserts against.** There is no
288
+ * shared type across that seam and inventing one would couple an MIT client to a
289
+ * closed server, so the cell types the literals itself — round-tripping this
290
+ * constant through our own code would only prove it equals itself (`D190 §5`).
291
+ */
292
+ export declare const WIRE_RUNGS: readonly AttributionRung[];
293
+ /** What one hook may put on the wire, and how many spool LINES that consumed. */
294
+ export interface Observed {
295
+ /** ⛔ Same length as `attributions`, same order. Wire rungs only. */
296
+ readonly shas: readonly string[];
297
+ /** ⛔ Same length as `shas`, same order. Wire rungs only. */
298
+ readonly attributions: readonly AttributionRung[];
299
+ /** Spool lines consumed — ⛔ **held ones INCLUDED, so they drain.** */
300
+ readonly count: number;
301
+ }
302
+ /**
303
+ * The SHAs this hook will carry, their rungs, and how many lines to drop after a
304
+ * 2xx.
305
+ *
306
+ * ⛔ **FULL WIDTH, ALWAYS.** `capture_commits.commit_sha` is 7..40 because the
307
+ * server's `expandShortSha` can fail, so an abbreviated sha makes the web join
308
+ * SILENTLY EMPTY — a wrong answer with no error, D98's class exactly. Anything
309
+ * that is not a full sha is dropped by `parseEntry` rather than sent.
310
+ *
311
+ * ⛔ **`shas` AND `attributions` ARE THE SAME LENGTH BY CONSTRUCTION** — one
312
+ * `push` each, in one iteration, or neither. That is not style.
313
+ * `parseObservedCommits` returns `[]` when the two lists differ in length: **the
314
+ * ENTIRE BATCH is dropped, not the odd entry**, with no edge, no error and no
315
+ * log (`D190 §1`). A `filter` and a `map` over the same array would be the same
316
+ * thing until someone changed one of them.
317
+ *
318
+ * ⛔ **`count` IS LINES CONSUMED, NOT `shas.length`, AND THEY DIVERGE.** A held
319
+ * entry is consumed and never sent. `dropSpooled` drops **the first N lines**, so
320
+ * a caller truncating by `shas.length` would delete a held line at the head and
321
+ * re-send the commit that was actually delivered — forever.
322
+ *
323
+ * ⚠ **A held entry IS dropped on the 2xx, and the observation is lost.** Stated
324
+ * rather than hidden: this wave gives the client no way to transmit a held
325
+ * commit, and leaving it in the file would make every subsequent hook re-read a
326
+ * line that can never be sent. The asymmetry this module already runs on decides
327
+ * it — a MISSED commit is recoverable, the sha is still in git, while a WRONGLY
328
+ * ATTRIBUTED one is a permanent row (`cr071:157`).
329
+ */
330
+ export declare function capSpool(entries: readonly SpoolEntry[]): Observed;
331
+ /** One rewrite: the sha that was destroyed, and the one that replaced it. */
332
+ export interface RewritePair {
333
+ readonly ancestor: string;
334
+ readonly successor: string;
335
+ }
336
+ /**
337
+ * How many rewrite pairs one hook may put on the wire.
338
+ *
339
+ * ⛔ **ITS OWN CONSTANT, AND SHARING `MAX_SPOOLED_SHAS` WOULD MAKE ONE OF THE TWO
340
+ * WRONG.** The units differ: a sha plus a separator is 41 bytes, a pair plus a
341
+ * separator is **82** (40 + `:` + 40 + `,`). 16 pairs is 1,312 bytes — the same
342
+ * header budget `MAX_SPOOLED_SHAS = 32` was sized against, arrived at through the
343
+ * arithmetic of this header rather than inherited from the other one.
344
+ *
345
+ * ⛔ **The rest stay spooled**, exactly as the commit spool's remainder does.
346
+ */
347
+ export declare const MAX_SPOOLED_PAIRS = 16;
348
+ /** `<repo>/<session>.rewrites.jsonl`, beside the commit spool. */
349
+ export declare function rewriteSpoolPath(home: string, key: SessionKey): string | null;
350
+ /**
351
+ * Append rewrite pairs, skipping any already spooled. Returns how many were
352
+ * WRITTEN — ⛔ not how many were offered.
353
+ *
354
+ * ## ⛔ THE DEDUP IS ONE OF T4'S TWO GUARDS, AND IT IS NOT REDUNDANCY
355
+ *
356
+ * `M6` measured the squash hazard and it is **SIZE-DEPENDENT**. A 3→1 squash
357
+ * fires `post-rewrite` twice — `amend A→F`, then `rebase A→F; B→F` — so **the
358
+ * pair `A→F` arrives twice**. A 4→1 squash instead produces rows naming an
359
+ * INTERMEDIATE sha that never existed on any branch; that one is killed by the
360
+ * in-progress-rebase suppression in `hooks/post_rewrite.ts`, which cannot see the
361
+ * duplicate, exactly as this cannot see the intermediate. **One squash size
362
+ * cannot test both.**
363
+ *
364
+ * ⚠ **Dedup is against the FILE, not against the batch**, because the two fires
365
+ * are two processes: a batch-local check would see one pair each time and dedup
366
+ * nothing. ⚠ The read-then-append is not atomic — two rewrites racing in one
367
+ * clone could both miss — but `post-rewrite` runs inside git's own serialised
368
+ * rebase, and the cost of the residual race is one duplicate pair rather than a
369
+ * wrong one.
370
+ *
371
+ * ⛔ **NEVER THROWS.** This runs inside the user's `git rebase`.
372
+ */
373
+ export declare function appendRewrites(home: string, key: SessionKey, pairs: readonly RewritePair[]): number;
374
+ /** Every well-formed pair in the rewrite spool, oldest first. */
375
+ export declare function readRewrites(home: string, key: SessionKey): RewritePair[];
376
+ /** Drop the first `count` pairs — called ONLY after the server's 2xx. */
377
+ export declare function dropRewrites(home: string, key: SessionKey, count: number): boolean;
378
+ /**
379
+ * The pairs this hook will carry, and how many to drop after a 2xx.
380
+ *
381
+ * ⛔ **`ancestor:successor`, COLON-SEPARATED.** `parseRewritePairs` drops
382
+ * silently on a wrong separator, on a third colon-field, on a bad sha and on a
383
+ * self-pair — no edge, no error, no log (`D190 §1`).
384
+ */
385
+ export declare function capSuccessors(pairs: readonly RewritePair[]): {
386
+ pairs: string[];
387
+ count: number;
388
+ };
389
+ /** Full width, both widths git uses. ⛔ Never abbreviated. */
390
+ export declare function isFullSha(value: unknown): value is string;