@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
package/dist/policy.js ADDED
@@ -0,0 +1,244 @@
1
+ /**
2
+ * The failure policy — CR-018, D58 (plan §ER22/§ER10/§ER11/§ER4).
3
+ *
4
+ * The defect this replaces, verbatim from the register: *"5xx was unclassified
5
+ * and fell through to advance-and-discard; 401/413 never succeed."* Every failed
6
+ * POST used to reach the same code, so the offset moved forward as if the bytes
7
+ * had been delivered. They had not.
8
+ *
9
+ * ## Three classes, and why `never` and `fatal` are not the same class
10
+ *
11
+ * later Hold the offset. The NEXT hook invocation retries from the same
12
+ * point. Transient: 5xx, no-response, 429, and 403 — a 403 here is
13
+ * org-approval-pending, which means later, not failed (D60 §D12).
14
+ * never Advance PAST the failing bytes and stamp a gap, so the record shows
15
+ * an honest hole instead of pretending nothing happened. Per-PAYLOAD:
16
+ * resending the identical bytes cannot work (413, structurally
17
+ * malformed 4xx).
18
+ * fatal Stop attempting further sends for this session until the user
19
+ * reconnects. Per-CREDENTIAL, which is the whole difference from
20
+ * `never`: 401 does not describe these bytes, it describes the token.
21
+ *
22
+ * **`later` never means a synchronous retry inside one hook invocation.** D56
23
+ * plan §D8 is a founder override that is not to be re-argued: the hook must never
24
+ * block or slow a developer's turn. "Later" means the bytes stay in the
25
+ * transcript file — which is where they already are — and the hook that fires on
26
+ * the next turn picks them up from the same offset.
27
+ *
28
+ * ## The table is data on purpose
29
+ *
30
+ * The title's word "table" is load-bearing. The classification is a literal data
31
+ * structure, so it is auditable by reading it and testable without exercising a
32
+ * real network failure. The original bug was an UNCLASSIFIED status silently
33
+ * defaulting to lossy behaviour, so the default here is explicit and it is
34
+ * `later` — the safe direction. A status nobody enumerated holds its bytes; it
35
+ * never discards them.
36
+ *
37
+ * This module is PURE. No fs, no network, no clock — `state.ts` persists, and the
38
+ * caller supplies `nowMs`. That is what lets both backlog caps be tested at their
39
+ * boundaries without waiting a day for one of them.
40
+ */
41
+ import { ERRORS } from "./copy/index.js";
42
+ /**
43
+ * THE TABLE. Status codes named one by one, because a family rule that swept up
44
+ * a whole `4xx` would be the implicit fallthrough this task exists to remove.
45
+ *
46
+ * Adding a row is how a new structurally-invalid status becomes `never`. Until
47
+ * someone does, an unnamed `4xx` resolves to `DEFAULT_CLASS` and HOLDS its bytes:
48
+ * a status we have not reasoned about is not one we discard data for.
49
+ */
50
+ export const STATUS_CLASSES = new Map([
51
+ // Structurally malformed request. Identical bytes will be rejected identically.
52
+ [400, "never"],
53
+ // The credential, not the payload. Terminal until the user reconnects.
54
+ [401, "fatal"],
55
+ // Org approval pending (D60 §D12). Capture has not stopped; it is waiting.
56
+ [403, "later"],
57
+ // Payload too large. Retrying the same bytes cannot work.
58
+ [413, "never"],
59
+ // Rate limited. The definition of transient.
60
+ [429, "later"],
61
+ ]);
62
+ /** `5xx` — the server's problem, not the payload's. */
63
+ export const SERVER_ERROR_CLASS = "later";
64
+ /** No HTTP response at all: DNS, refused, TLS, timeout, abort. */
65
+ export const NETWORK_CLASS = "later";
66
+ /**
67
+ * The explicit default. **It is `later` and it must stay `later`.** An implicit
68
+ * fallthrough to advance-and-discard is the original defect at a smaller scale.
69
+ */
70
+ export const DEFAULT_CLASS = "later";
71
+ /** Classify one HTTP status. `2xx` is the only success. */
72
+ export function classifyStatus(status) {
73
+ if (status >= 200 && status < 300)
74
+ return "ok";
75
+ const named = STATUS_CLASSES.get(status);
76
+ if (named !== undefined)
77
+ return named;
78
+ if (status >= 500 && status < 600)
79
+ return SERVER_ERROR_CLASS;
80
+ return DEFAULT_CLASS;
81
+ }
82
+ /** Classify one attempt, response or not. */
83
+ export function classify(outcome) {
84
+ return outcome.kind === "unreachable" ? NETWORK_CLASS : classifyStatus(outcome.status);
85
+ }
86
+ /**
87
+ * The §13.6 copy for a class.
88
+ *
89
+ * `ERRORS` was scaffolded for this task and carries exactly these three lines, so
90
+ * this is the one place a class becomes a sentence. Nothing in the HOOK path
91
+ * calls it — a hook speaks only through `systemMessage` (§13.7) — but `CR-021`'s
92
+ * `status` needs a class-to-copy mapping and this is where it belongs rather than
93
+ * as a fourth `switch` in a verb body.
94
+ */
95
+ export function failureCopy(cls) {
96
+ switch (cls) {
97
+ case "later":
98
+ return ERRORS.networkLater;
99
+ case "never":
100
+ return ERRORS.payloadNever;
101
+ case "fatal":
102
+ return ERRORS.authFatal;
103
+ }
104
+ }
105
+ export const EMPTY_FILE_STATE = {
106
+ sentOffset: 0,
107
+ backlog: [],
108
+ gapBytes: 0,
109
+ gapCount: 0,
110
+ lastSentAt: 0,
111
+ };
112
+ /**
113
+ * The caps.
114
+ *
115
+ * ⚠ **Neither the register nor `DECISIONS.md` specifies an eviction policy once a
116
+ * cap is hit.** What is decided is only that both caps exist. The rule below is
117
+ * this task's choice, stated rather than smuggled: **drop the oldest span,
118
+ * advance past it, stamp a gap, continue.** It is the `never` class's own shape
119
+ * applied to our own storage, and it is the only option that neither grows
120
+ * unbounded nor blocks — the two things D56 §D8 forbids outright.
121
+ *
122
+ * `entries` is not a third policy. It is the same drop-oldest rule guarding the
123
+ * degenerate shape where a file grows one byte at a time: the byte cap alone
124
+ * would permit `MAX_BACKLOG_BYTES` single-byte entries.
125
+ */
126
+ export const BACKLOG_CAPS = {
127
+ bytes: 8 * 1024 * 1024,
128
+ ageMs: 24 * 60 * 60 * 1000,
129
+ entries: 512,
130
+ };
131
+ /**
132
+ * Read the caps from the environment.
133
+ *
134
+ * Same shape as `hookBudgetMs`: an unset or unparseable value takes the default
135
+ * rather than disabling the cap, because a cap that an empty string can switch
136
+ * off is not a cap. Present for operators with unusual transcript volumes and
137
+ * for the tests, which have to reach a boundary they cannot otherwise reach.
138
+ */
139
+ export function resolveCaps(env) {
140
+ return {
141
+ bytes: positiveOr(env.VIBECOMMIT_BACKLOG_MAX_BYTES, BACKLOG_CAPS.bytes),
142
+ ageMs: positiveOr(env.VIBECOMMIT_BACKLOG_MAX_AGE_MS, BACKLOG_CAPS.ageMs),
143
+ entries: positiveOr(env.VIBECOMMIT_BACKLOG_MAX_ENTRIES, BACKLOG_CAPS.entries),
144
+ };
145
+ }
146
+ function positiveOr(raw, fallback) {
147
+ if (raw === undefined)
148
+ return fallback;
149
+ const parsed = Number(raw);
150
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
151
+ }
152
+ /** Undelivered bytes currently held. */
153
+ export function pendingBytes(state) {
154
+ if (state.backlog.length === 0)
155
+ return 0;
156
+ return state.backlog[state.backlog.length - 1].to - state.backlog[0].from;
157
+ }
158
+ /**
159
+ * The span the next attempt should carry: everything from the offset to `eof`.
160
+ *
161
+ * A file SHORTER than the offset means the transcript was replaced or truncated —
162
+ * a new file at a path we have state for. Resync from 0 rather than reading a
163
+ * negative-length slice; the server is idempotent on `(session, offset)`.
164
+ */
165
+ export function nextSpan(state, eof) {
166
+ const from = state.sentOffset > eof ? 0 : state.sentOffset;
167
+ return eof > from ? { from, to: eof } : null;
168
+ }
169
+ /**
170
+ * Everything up to `to` arrived. The backlog clears, and the success is STAMPED.
171
+ *
172
+ * `nowMs` is a parameter rather than a `Date.now()` call because this module is
173
+ * pure — that is what lets both backlog caps be tested at their boundaries
174
+ * without waiting a day, and the same property now covers the `last sent` row.
175
+ */
176
+ export function markDelivered(state, to, nowMs) {
177
+ return { ...state, sentOffset: to, backlog: [], lastSentAt: nowMs };
178
+ }
179
+ /**
180
+ * `never` — advance PAST the attempted span exactly once, stamping a gap.
181
+ *
182
+ * Exactly once is the point: the offset lands at `to`, so the next invocation
183
+ * starts on the next, unrelated chunk. It neither loops on the poisoned bytes nor
184
+ * swallows what comes after them.
185
+ */
186
+ export function markSkipped(state, from, to) {
187
+ const skipped = Math.max(0, to - from);
188
+ return {
189
+ // `lastSentAt` is carried, not cleared: skipping past bytes that could never
190
+ // be delivered does not un-deliver what already was.
191
+ lastSentAt: state.lastSentAt,
192
+ sentOffset: to,
193
+ backlog: [],
194
+ gapBytes: state.gapBytes + skipped,
195
+ gapCount: state.gapCount + (skipped > 0 ? 1 : 0),
196
+ };
197
+ }
198
+ /**
199
+ * `later` — hold the offset and remember when this span first failed.
200
+ *
201
+ * The span is appended only when the file has actually grown past what is already
202
+ * held. A hook that fires twice on an unchanged file retries the same span rather
203
+ * than minting a second entry with a newer timestamp, which would keep resetting
204
+ * the age cap and make it unreachable.
205
+ */
206
+ export function markHeld(state, span, nowMs, caps) {
207
+ const base = state.sentOffset === span.from
208
+ ? state
209
+ : // The offset moved under us (a resync). Start the backlog over from here.
210
+ { ...state, sentOffset: span.from, backlog: [] };
211
+ const last = base.backlog[base.backlog.length - 1];
212
+ const heldTo = last?.to ?? base.sentOffset;
213
+ const backlog = span.to > heldTo
214
+ ? [...base.backlog, { from: heldTo, to: span.to, at: nowMs }]
215
+ : base.backlog;
216
+ return enforceCaps({ ...base, backlog }, nowMs, caps);
217
+ }
218
+ /**
219
+ * Shed the oldest spans until every cap holds.
220
+ *
221
+ * Never blocks, never grows. Each shed span advances the offset past itself and
222
+ * stamps a gap, so what the server sees is a discontinuity in `byteOffset` — an
223
+ * honest hole — and not a silent renumbering.
224
+ */
225
+ export function enforceCaps(state, nowMs, caps) {
226
+ let sentOffset = state.sentOffset;
227
+ let backlog = state.backlog;
228
+ let gapBytes = state.gapBytes;
229
+ let gapCount = state.gapCount;
230
+ while (backlog.length > 0) {
231
+ const oldest = backlog[0];
232
+ const overBytes = backlog[backlog.length - 1].to - backlog[0].from > caps.bytes;
233
+ const overAge = nowMs - oldest.at > caps.ageMs;
234
+ const overEntries = backlog.length > caps.entries;
235
+ if (!overBytes && !overAge && !overEntries)
236
+ break;
237
+ sentOffset = oldest.to;
238
+ gapBytes += oldest.to - oldest.from;
239
+ gapCount += 1;
240
+ backlog = backlog.slice(1);
241
+ }
242
+ return { sentOffset, backlog, gapBytes, gapCount, lastSentAt: state.lastSentAt };
243
+ }
244
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
package/dist/post.d.ts ADDED
@@ -0,0 +1,302 @@
1
+ /**
2
+ * The ingest request — the ONE place the credential's plaintext is used.
3
+ *
4
+ * `send()` still makes exactly one attempt and decides nothing; `deliver()`, at
5
+ * the bottom of this file, is `CR-018`'s application of the failure policy around
6
+ * it. The classification itself is data in `policy.ts` and the ledger is in
7
+ * `state.ts`, so what lives here is only the wiring between them and the wire.
8
+ *
9
+ * `X-Repo-Slug` is derived in `git.ts` (`CR-019d`) and arrives here already
10
+ * resolved. It is REQUIRED on the wire: the merged server 400s a request without
11
+ * it, deliberately, because a NULL `repository_id` is fail-closed-INVISIBLE
12
+ * rather than merely wrong (D57 §OV1).
13
+ *
14
+ * Wire shape retyped from the plan and from reading the closed-source server. No
15
+ * code is imported or copied from `vibecommit-mcp` (D60 §D1a): this package is
16
+ * MIT, and lifting closed code into it relicenses that code by accident.
17
+ *
18
+ * @provenance vibecommit-mcp src/conversation/ingest_session.ts — wire shape, retyped
19
+ */
20
+ import { type SelectedAgentId } from "./agents/types.js";
21
+ import type { IngestCredential } from "./credential.js";
22
+ import { type Disposition } from "./policy.js";
23
+ import { type Observed } from "./spool.js";
24
+ /** Production data plane. Overridable for tests and for a self-hosted server. */
25
+ export declare const DEFAULT_INGEST_URL = "https://api.vibecommit.ai/ingest/v1/session";
26
+ /**
27
+ * `/cso` finding 2 (2026-08-09), MEDIUM. The override used to be passed to
28
+ * `fetch` unexamined, so `http://` put `Authorization: Bearer vcik_…` on the
29
+ * wire in cleartext — and the ingest credential is long-lived with no rotation
30
+ * by design, so one interception is a durable compromise rather than a
31
+ * 15-minute one.
32
+ */
33
+ export declare function isAllowedIngestUrl(raw: string): boolean;
34
+ /** The ingest URL, or null when the override is one we refuse to send to. */
35
+ export declare function ingestUrl(env: NodeJS.ProcessEnv): string | null;
36
+ /** Which transcript file a delta came from: the main one, or one sub-agent's. */
37
+ export type FileKey = "main" | `agent-${string}`;
38
+ export interface IngestDelta {
39
+ readonly sessionId: string;
40
+ /**
41
+ * Monotonic per session. A gap is detected server-side and reported back
42
+ * on the ingest response — never stamped onto a row.
43
+ */
44
+ readonly seq: number;
45
+ /** Offset the delta STARTS at, so the server can detect a resync. */
46
+ readonly byteOffset: number;
47
+ readonly fileKey: FileKey;
48
+ /**
49
+ * `owner/repo`, or `local:<sha256(abspath)[0:12]>` — `git.ts`'s
50
+ * `resolveRepoSlug` (`CR-019d`). REQUIRED and never empty: the merged server
51
+ * (`CR-019b`) 400s an absent or blank header.
52
+ *
53
+ * ⚠ NOT `DeliveryContext.repoKey`. That one is a filesystem path used for
54
+ * `~/.vibecommit/` state filenames and never leaves the machine; this one is
55
+ * the tenant-resolvable identity the server looks up. See `git.ts`'s header.
56
+ */
57
+ readonly repoSlug: string;
58
+ /**
59
+ * ⛔ **WHO PRODUCED THESE BYTES — `X-Agent` (`CR-204`, D177 §7).**
60
+ *
61
+ * The server binds the session's `agent` from this header and picks the
62
+ * extractor from it. It had **no client writer at all** until `CR-204`: nine
63
+ * headers went out and this was not among them, so every capture ever taken
64
+ * bound `unknown`, and `DIALECTS.unknown` is the Claude extractor. Measured
65
+ * cost on a non-Claude transcript: **32 of 32 records discarded**, against a
66
+ * correct extraction of 7 turns, with an empty tail — so the offset advances
67
+ * and the bytes are never read again (D177 §11).
68
+ *
69
+ * ⛔ **DERIVED FROM THE CONTAINING REGISTRY ROOT, never from `--agent=`**, and
70
+ * typed as `SelectedAgentId` rather than `string` so the flag's value is not
71
+ * even assignable here. `deliver()` below is the one place it is computed.
72
+ *
73
+ * ⛔ **`unknown` IS NOT SENT — it OMITS the header.** See `buildIngestHeaders`.
74
+ *
75
+ * ⚠ **This changes the digest's VALUE, not its FORMULA.** `agent` is already
76
+ * in `ingestContentDigest`'s preimage server-side, so there is nothing to
77
+ * migrate and nothing to backfill — and every stream pinned `unknown` rewinds
78
+ * exactly once when this lands, which is mismatch-as-rewind working as
79
+ * designed (D164 §4) and free at zero production rows.
80
+ */
81
+ readonly agent: SelectedAgentId;
82
+ /**
83
+ * Where `HEAD` is right now — the REF MOVE (`CR-170`, D154).
84
+ *
85
+ * ⚠ **This is not a commit.** A `pull`, a `checkout` or a `reset --hard` moves
86
+ * a ref without creating one, so no commit hook fires and this is the only
87
+ * record that it happened. Absent when git could not be read (an empty
88
+ * repository has no `HEAD`), which is a state rather than an error.
89
+ */
90
+ readonly head?: {
91
+ readonly sha: string;
92
+ readonly branch: string | null;
93
+ } | null;
94
+ /**
95
+ * Commits OBSERVED by `post-commit` since the last delivery, oldest first —
96
+ * ⛔ **and the rung each was attributed by, positionally aligned.**
97
+ *
98
+ * ⛔ **FULL 40-CHAR SHAs, ALWAYS.** `capture_commits.commit_sha` is 7..40
99
+ * because the server's `expandShortSha` can fail; an abbreviated sha makes the
100
+ * web join SILENTLY EMPTY, which is a wrong answer with no error (D98).
101
+ *
102
+ * ⛔ **THE WHOLE `Observed` OBJECT, not the sha list.** The two lists are only
103
+ * safe as a pair: `parseObservedCommits` drops the ENTIRE BATCH when their
104
+ * lengths differ (`D190 §1`), so a signature that let a caller supply one
105
+ * without the other would be a signature that lets a caller silently delete
106
+ * every edge in the request.
107
+ */
108
+ readonly commits?: Observed;
109
+ /**
110
+ * Rewrite pairs OBSERVED by `post-rewrite` — `ancestor:successor`, already in
111
+ * wire spelling (`capSuccessors`).
112
+ *
113
+ * ⚠ **NO ATTRIBUTION, and that is not an omission.** A pair asserts a relation
114
+ * between two SHAs and claims nothing about who did it, so there is no rung to
115
+ * be wrong about.
116
+ */
117
+ readonly rewrites?: readonly string[];
118
+ }
119
+ /**
120
+ * Build the request headers.
121
+ *
122
+ * `expose()` is called here and in no other module. That is the point of the
123
+ * opaque wrapper: the single call site is greppable, reviewable, and sits at the
124
+ * boundary where the secret has to be plaintext anyway.
125
+ *
126
+ * ## ⛔ SHAs ON THE WIRE, NEVER COMMIT METADATA (`CR-170` §4)
127
+ *
128
+ * No message, no author, no dates. MEASURED over the last 20 commits per repo:
129
+ * **9/20 schema, 8/20 web and 5/20 capture exceed 4096 bytes, largest 12,112** —
130
+ * so a quarter of real commits would land PERMANENTLY TRUNCATED in a header, and
131
+ * `capture_commits` rows cannot be retracted. Metadata is backfillable from a
132
+ * sha by anyone holding the repository; the observation that a commit happened
133
+ * at all is not. So the wire carries the irreplaceable half.
134
+ */
135
+ export declare function buildIngestHeaders(credential: IngestCredential, delta: IngestDelta): Record<string, string>;
136
+ /**
137
+ * The wire's error code for org-approval-pending — `CR-128`, D81.
138
+ *
139
+ * ⚠ A CROSS-REPO CONTRACT, not a local string. The server emits it as
140
+ * `403 { error: "capture_not_approved", owners_notified }` and pins the body
141
+ * shape in its own suite. **`repository_forbidden` is ALSO a 403** and carries
142
+ * no `owners_notified` at all — deliberately, because it is a different
143
+ * condition where no owner was or could have been notified. That is why every
144
+ * consumer here branches on THIS CODE and never on the status.
145
+ */
146
+ export declare const CAPTURE_NOT_APPROVED = "capture_not_approved";
147
+ /**
148
+ * The non-secret detail parsed out of an error response body.
149
+ *
150
+ * Deliberately narrow: a code and one number. The body carries no org id, no
151
+ * repo id, no user id and no owner list — the server discloses nothing — so
152
+ * there is nothing else here to lift, and widening this type later is a
153
+ * decision rather than a drift.
154
+ */
155
+ export interface ErrorDetail {
156
+ /** The `error` field. The ONLY thing any caller may branch on. */
157
+ readonly code: string;
158
+ /**
159
+ * `owners_notified` — **`null` = NOT DETERMINABLE, `0` = determinable and
160
+ * nobody was told, `> 0` = N notified** (D81). `undefined` when the key was
161
+ * absent from the body, which is what `repository_forbidden` looks like.
162
+ *
163
+ * ⚠ `null` and `undefined` are NOT interchangeable for the caller's copy, but
164
+ * they resolve to the same branch: never assert delivery on either.
165
+ */
166
+ readonly ownersNotified?: number | null;
167
+ }
168
+ export type SendOutcome = {
169
+ readonly kind: "response";
170
+ readonly status: number;
171
+ /**
172
+ * The parsed error body, or `null` when there was not one to parse.
173
+ *
174
+ * ⚠ **The hook path ignores this for free.** It is only ever populated on
175
+ * a non-2xx (see `readErrorDetail`), so a healthy hook never pays for the
176
+ * body read at all, and `deliver()`'s classification does not consult it.
177
+ */
178
+ readonly detail: ErrorDetail | null;
179
+ }
180
+ /** No response at all — DNS, refused, TLS, timeout. Not an HTTP status. */
181
+ | {
182
+ readonly kind: "unreachable";
183
+ };
184
+ /**
185
+ * One attempt. No retry, no backoff, no classification — `CR-018` owns all three.
186
+ *
187
+ * The timeout is mandatory rather than optional: a hook has a wall-clock budget
188
+ * it must not exceed (DESIGN.md §13.7), and `fetch` without a signal waits on the
189
+ * platform default, which is far longer than any hook timeout.
190
+ *
191
+ * Never throws. A hook that takes an exception from the network has already
192
+ * broken the contract; returning `unreachable` keeps the decision with the
193
+ * caller.
194
+ */
195
+ export declare function send(url: string, headers: Record<string, string>, body: Uint8Array, timeoutMs: number): Promise<SendOutcome>;
196
+ /**
197
+ * A one-way digest of the credential, for the `fatal` stop's key.
198
+ *
199
+ * The second and last call to `expose()` in the package, kept in this module for
200
+ * the same reason as the first: the plaintext's exits are greppable in one file.
201
+ * What is stored is 16 hex characters of a SHA-256 over a high-entropy opaque
202
+ * token — enough to tell one credential from another, and not a preimage of
203
+ * anything. No copy path and no header ever carries it.
204
+ */
205
+ export declare function credentialFingerprint(credential: IngestCredential): string;
206
+ export type Delivery =
207
+ /** The offset is already at EOF. No request was made and none was needed. */
208
+ {
209
+ readonly kind: "nothing-to-send";
210
+ }
211
+ /** A `fatal` class fired earlier for this credential. No request was made. */
212
+ | {
213
+ readonly kind: "stopped";
214
+ }
215
+ /** No repo identity, so no binding key. `CR-017d` — refuse, never guess. */
216
+ | {
217
+ readonly kind: "no-repo";
218
+ } | {
219
+ readonly kind: "attempted";
220
+ readonly disposition: Disposition;
221
+ /**
222
+ * The parsed error body, when there was one — `CR-110`.
223
+ *
224
+ * ⚠ **This does NOT reintroduce branching on `later` / `never`.** The
225
+ * disposition still decides what happens to the offset, and nothing here
226
+ * changes it. What this carries is the ONE outcome `connect` renders a
227
+ * beat for, identified by its ERROR CODE, and a caller that ignores it
228
+ * behaves exactly as before — which is what the hook path does.
229
+ */
230
+ readonly detail: ErrorDetail | null;
231
+ };
232
+ export interface DeliveryContext {
233
+ readonly home: string;
234
+ readonly env: NodeJS.ProcessEnv;
235
+ readonly url: string;
236
+ readonly credential: IngestCredential;
237
+ /**
238
+ * `resolveProjectKey()`'s git toplevel — half the binding key (`CR-017d`),
239
+ * ahead of `sessionId` as `repository_id` sits ahead of `session_id` in
240
+ * `CR-017a`'s PK. Required: the caller resolves it or does not call.
241
+ *
242
+ * LOCAL ONLY. Pairs with `repoSlug` below, which is the wire half; the two
243
+ * describe the same repository in two different vocabularies and are not
244
+ * interchangeable.
245
+ */
246
+ readonly repoKey: string;
247
+ /** The wire identity — `X-Repo-Slug` (`CR-019d`). See `IngestDelta`. */
248
+ readonly repoSlug: string;
249
+ readonly sessionId: string;
250
+ readonly fileKey: FileKey;
251
+ /**
252
+ * The file these bytes come from — ⛔ **the input `X-Agent` is derived from,
253
+ * and the ONLY reason this module is told a path** (`CR-204`, D177 §7).
254
+ *
255
+ * ⚠ **A path, not an agent id, and that is the point.** Taking a resolved
256
+ * `agent` from callers would let one hand over `ctx.agentId` — the `--agent=`
257
+ * flag — which is the false-provenance defect this task exists to close.
258
+ * Taking the PATH means the derivation happens here, once, through
259
+ * `agentForTranscript`, and no caller can express the wrong thing.
260
+ *
261
+ * The same path the caller's `readBody` reads, and it is already confined:
262
+ * `readBody` is supplied precisely because the CALLER is the one that ran
263
+ * `isInsideAny(transcriptRoots(...))` over it (`/cso` finding 1). This module
264
+ * still never opens a file.
265
+ */
266
+ readonly transcriptPath: string;
267
+ readonly timeoutMs: number;
268
+ /** Epoch ms. Passed in so the backlog's age cap is reachable from a test. */
269
+ readonly nowMs: number;
270
+ /**
271
+ * ⛔ ATTACHED TO EXACTLY ONE DELIVERY PER HOOK — the MAIN transcript's.
272
+ *
273
+ * One hook fire delivers the main file plus up to `MAX_SUBAGENT_FILES` (8)
274
+ * sub-agent files, **each its own `capture_id`**. `capture_commits`' PK is
275
+ * `(org_id, capture_id, commit_sha)`, so attaching these to every delivery
276
+ * would admit NINE LEGAL, PERMANENT ROWS for one commit — the exact objection
277
+ * that killed the polling design (D154). Sub-agent deliveries leave both unset.
278
+ */
279
+ readonly head?: {
280
+ readonly sha: string;
281
+ readonly branch: string | null;
282
+ } | null;
283
+ readonly commits?: Observed;
284
+ /** Rewrite pairs, wire-spelled. Main delivery only, for the same reason. */
285
+ readonly rewrites?: readonly string[];
286
+ }
287
+ /**
288
+ * Send the undelivered span of one transcript file and apply the failure policy.
289
+ *
290
+ * **Exactly one attempt, ever.** `later` does not retry here; it leaves the
291
+ * offset where it is so the hook that fires on the NEXT turn picks the same
292
+ * bytes up. A loop in this function would reopen D56 plan §D8, the founder
293
+ * override that says a hook must never block or slow a developer's turn.
294
+ *
295
+ * Never throws: the caller is a hook, and an exception on this path is the
296
+ * contract breaking. Every failure resolves to a `Delivery`.
297
+ *
298
+ * `readBody` is supplied by the caller rather than read here, because the caller
299
+ * is the one that has confined `transcript_path` to the Claude Code root
300
+ * (`/cso` finding 1). This module never opens a file.
301
+ */
302
+ export declare function deliver(ctx: DeliveryContext, eof: number, readBody: (from: number, to: number) => Uint8Array | null): Promise<Delivery>;