@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/post.js ADDED
@@ -0,0 +1,384 @@
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 { createHash } from "node:crypto";
21
+ import { agentForTranscript } from "./agents/registry.js";
22
+ import { UNKNOWN_AGENT_ID } from "./agents/types.js";
23
+ import { classify, markDelivered, markHeld, markSkipped, nextSpan, resolveCaps, } from "./policy.js";
24
+ import { fileState, isStopped, loadSessionState, saveSessionState, withFileState, } from "./state.js";
25
+ import { dropRewrites, dropSpooled } from "./spool.js";
26
+ import { CLIENT_VERSION, CLIENT_VERSION_HEADER } from "./version.js";
27
+ /** Production data plane. Overridable for tests and for a self-hosted server. */
28
+ export const DEFAULT_INGEST_URL = "https://api.vibecommit.ai/ingest/v1/session";
29
+ /**
30
+ * Loopback hosts, the only place a plaintext ingest URL is tolerated.
31
+ * The contract test needs `http://127.0.0.1:<port>`; nothing else does.
32
+ */
33
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
34
+ /**
35
+ * `/cso` finding 2 (2026-08-09), MEDIUM. The override used to be passed to
36
+ * `fetch` unexamined, so `http://` put `Authorization: Bearer vcik_…` on the
37
+ * wire in cleartext — and the ingest credential is long-lived with no rotation
38
+ * by design, so one interception is a durable compromise rather than a
39
+ * 15-minute one.
40
+ */
41
+ export function isAllowedIngestUrl(raw) {
42
+ let url;
43
+ try {
44
+ url = new URL(raw);
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ if (url.protocol === "https:")
50
+ return true;
51
+ return url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
52
+ }
53
+ /** The ingest URL, or null when the override is one we refuse to send to. */
54
+ export function ingestUrl(env) {
55
+ const override = env.VIBECOMMIT_INGEST_URL;
56
+ if (override === undefined || override.trim() === "")
57
+ return DEFAULT_INGEST_URL;
58
+ const candidate = override.trim();
59
+ return isAllowedIngestUrl(candidate) ? candidate : null;
60
+ }
61
+ /**
62
+ * Build the request headers.
63
+ *
64
+ * `expose()` is called here and in no other module. That is the point of the
65
+ * opaque wrapper: the single call site is greppable, reviewable, and sits at the
66
+ * boundary where the secret has to be plaintext anyway.
67
+ *
68
+ * ## ⛔ SHAs ON THE WIRE, NEVER COMMIT METADATA (`CR-170` §4)
69
+ *
70
+ * No message, no author, no dates. MEASURED over the last 20 commits per repo:
71
+ * **9/20 schema, 8/20 web and 5/20 capture exceed 4096 bytes, largest 12,112** —
72
+ * so a quarter of real commits would land PERMANENTLY TRUNCATED in a header, and
73
+ * `capture_commits` rows cannot be retracted. Metadata is backfillable from a
74
+ * sha by anyone holding the repository; the observation that a commit happened
75
+ * at all is not. So the wire carries the irreplaceable half.
76
+ */
77
+ export function buildIngestHeaders(credential, delta) {
78
+ const headers = {
79
+ authorization: `Bearer ${credential.expose()}`,
80
+ "content-type": "application/x-ndjson",
81
+ "content-encoding": "zstd",
82
+ "x-session-id": delta.sessionId,
83
+ "x-seq": String(delta.seq),
84
+ "x-byte-offset": String(delta.byteOffset),
85
+ "x-file": delta.fileKey,
86
+ "x-repo-slug": delta.repoSlug,
87
+ [CLIENT_VERSION_HEADER.toLowerCase()]: CLIENT_VERSION,
88
+ };
89
+ // ⛔ OMITTED ON `unknown`, AND THE OMISSION IS THE POINT — not a tidier way of
90
+ // saying the same thing.
91
+ //
92
+ // The server keeps ABSENT and `unknown` apart deliberately, and its own
93
+ // comment says the header's parsed type is nullable *because* of this guard:
94
+ // an absent header leaves a stream's pinned agent alone, while a PRESENT
95
+ // header resolves and is compared — and `unknown` resolves to `unknown`, which
96
+ // disagrees with any non-`unknown` pin. That disagreement is not a rejection;
97
+ // it drops the stream's buffered `tail_records` and re-pins, which SHEDS THE
98
+ // IN-FLIGHT TURN (D164 §4's mismatch-as-rewind).
99
+ //
100
+ // ⛔ So sending `unknown` would manufacture exactly the disagreement the
101
+ // nullable type exists to prevent. A stream that resolves cleanly on delta 1
102
+ // and becomes ambiguous later — a second root appearing, an env var set in one
103
+ // shell and not the next, a symlink flipped — would shed a turn on every flip.
104
+ // Omitting cannot: it says nothing, and nothing cannot disagree.
105
+ //
106
+ // ⚠ **NOT "silently", and the correction is worth its line**: the server
107
+ // ECHOES `agent_rewind` in the ingest 200 body (D169 §1), so the RE-PIN is
108
+ // reported. What is NOT reported is the shed itself. ⚠ And nothing on this
109
+ // side reads either — `send()` parses a body only on a non-2xx. So the case
110
+ // against sending rests on the turn being LOST, not on the loss being
111
+ // invisible; a designed, reported rewind (D164 §4) is survivable, and this
112
+ // client would simply be causing them for no gain.
113
+ //
114
+ // ⚠ On a genuinely ambiguous FIRST delta the two are indistinguishable — the
115
+ // bind writes `unknown` either way. There is no case where sending wins, and
116
+ // one class where it loses, so the header is conditional.
117
+ //
118
+ // This is also this function's existing idiom, four lines below: OMITTED,
119
+ // never sent empty; an absent header is unambiguously "nothing to say".
120
+ if (delta.agent !== UNKNOWN_AGENT_ID)
121
+ headers["x-agent"] = delta.agent;
122
+ // ⚠ OMITTED, never sent empty. A blank header is a value the server has to
123
+ // have an opinion about; an absent one is unambiguously "nothing to say", and
124
+ // an empty repository genuinely has nothing to say here.
125
+ if (delta.head != null) {
126
+ headers["x-head-sha"] = delta.head.sha;
127
+ if (delta.head.branch !== null)
128
+ headers["x-head-branch"] = delta.head.branch;
129
+ }
130
+ // ⛔⛔ THE WIRE CONTRACT, AND EVERY WAY OF GETTING IT WRONG IS SILENT IN BOTH
131
+ // DIRECTIONS (`D190`). Three header names and two separators, agreed between
132
+ // two repositories with NO COMPILER, NO SCHEMA AND NO SHARED TYPE spanning the
133
+ // seam. Name it `x-commit-attribution` (singular), join with `;`, or emit the
134
+ // pairs as `ancestor,successor`, and you get a green client suite, a green
135
+ // server suite, a 200 response, and ZERO EDGES FOREVER. Nothing goes red
136
+ // anywhere. `test/wire-attribution.test.ts` types these literals a second time
137
+ // rather than importing them, because a cell that round-trips this code's own
138
+ // constant proves only that it equals itself.
139
+ //
140
+ // ⛔ THE TWO LISTS ARE SENT TOGETHER OR NOT AT ALL, AND THEY ARE THE SAME
141
+ // LENGTH. `parseObservedCommits` returns `[]` — the ENTIRE BATCH, not the odd
142
+ // entry — when `rungs.length !== shas.length`. `capSpool` builds them in one
143
+ // loop so they cannot disagree; this line refuses to send them if they somehow
144
+ // do, because a batch dropped server-side is invisible and a batch not sent is
145
+ // retried by the next hook.
146
+ //
147
+ // ⚠ NO RUNG, NO EDGE (`D190 §2`) is why the guard is a REFUSAL rather than a
148
+ // fallback to `x-commits` alone: the server treats a commit with no attribution
149
+ // as a PRE-LADDER client's mtime guess and writes nothing, so sending the shas
150
+ // bare would be a slower road to the same zero, with a spool truncated on the
151
+ // 2xx as though it had worked.
152
+ const observed = delta.commits;
153
+ if (observed !== undefined && observed.shas.length > 0) {
154
+ if (observed.shas.length === observed.attributions.length) {
155
+ headers["x-commits"] = observed.shas.join(",");
156
+ headers["x-commit-attributions"] = observed.attributions.join(",");
157
+ }
158
+ }
159
+ // ⚠ Same idiom as every other conditional header here: OMITTED, never sent
160
+ // empty. `parseRewritePairs` drops silently on a wrong separator, a third
161
+ // colon-field, a bad sha or a self-pair, so the spelling is fixed by
162
+ // `capSuccessors` and asserted literally by the wire cell.
163
+ if (delta.rewrites !== undefined && delta.rewrites.length > 0) {
164
+ headers["x-rewrites"] = delta.rewrites.join(",");
165
+ }
166
+ return headers;
167
+ }
168
+ /**
169
+ * The wire's error code for org-approval-pending — `CR-128`, D81.
170
+ *
171
+ * ⚠ A CROSS-REPO CONTRACT, not a local string. The server emits it as
172
+ * `403 { error: "capture_not_approved", owners_notified }` and pins the body
173
+ * shape in its own suite. **`repository_forbidden` is ALSO a 403** and carries
174
+ * no `owners_notified` at all — deliberately, because it is a different
175
+ * condition where no owner was or could have been notified. That is why every
176
+ * consumer here branches on THIS CODE and never on the status.
177
+ */
178
+ export const CAPTURE_NOT_APPROVED = "capture_not_approved";
179
+ /**
180
+ * An error body is a handful of bytes by contract. Anything larger is not one
181
+ * this client parses — a cap so a misconfigured or hostile endpoint cannot make
182
+ * a hook buffer an arbitrary response into memory.
183
+ */
184
+ const MAX_ERROR_BODY_BYTES = 64 * 1024;
185
+ /**
186
+ * Parse an error body, or return null. **NEVER THROWS, on any input.**
187
+ *
188
+ * That is a hard requirement rather than defensive habit: `deliver()` is the
189
+ * HOOK's path, `post.ts` states that an exception there is the contract
190
+ * breaking, and `JSON.parse` on a response body is an exception waiting for a
191
+ * bad day. Every failure — a non-JSON body, an empty body, an array, a body
192
+ * whose `error` is not a string — resolves to `null`, and `null` sends the
193
+ * caller to the branch that asserts nothing. That is the honest default,
194
+ * because `null` already means *not determinable*.
195
+ *
196
+ * ⚠ Read ONLY on a non-2xx. A 2xx body is not read, not awaited, and not
197
+ * parsed, so the hook's hot path is untouched.
198
+ */
199
+ async function readErrorDetail(res) {
200
+ if (res.ok)
201
+ return null;
202
+ // The declared length, when there is one. The abort signal already bounds the
203
+ // read in TIME; this bounds it in BYTES for a fast, huge response.
204
+ const declared = Number(res.headers.get("content-length"));
205
+ if (Number.isFinite(declared) && declared > MAX_ERROR_BODY_BYTES)
206
+ return null;
207
+ let body;
208
+ try {
209
+ const text = await res.text();
210
+ if (text.length > MAX_ERROR_BODY_BYTES)
211
+ return null;
212
+ body = JSON.parse(text);
213
+ }
214
+ catch {
215
+ return null;
216
+ }
217
+ if (body === null || typeof body !== "object" || Array.isArray(body))
218
+ return null;
219
+ const { error, owners_notified: owners } = body;
220
+ if (typeof error !== "string" || error === "")
221
+ return null;
222
+ // The key is absent (`undefined`) on bodies that do not carry it, `null` when
223
+ // it is present and not determinable. Both are preserved; neither is coerced
224
+ // into a number, because a count invented here is a claim invented here.
225
+ const ownersNotified = owners === null ? null : typeof owners === "number" && Number.isFinite(owners) ? owners : undefined;
226
+ return { code: error, ownersNotified };
227
+ }
228
+ /**
229
+ * One attempt. No retry, no backoff, no classification — `CR-018` owns all three.
230
+ *
231
+ * The timeout is mandatory rather than optional: a hook has a wall-clock budget
232
+ * it must not exceed (DESIGN.md §13.7), and `fetch` without a signal waits on the
233
+ * platform default, which is far longer than any hook timeout.
234
+ *
235
+ * Never throws. A hook that takes an exception from the network has already
236
+ * broken the contract; returning `unreachable` keeps the decision with the
237
+ * caller.
238
+ */
239
+ export async function send(url, headers, body, timeoutMs) {
240
+ const controller = new AbortController();
241
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
242
+ try {
243
+ const res = await fetch(url, {
244
+ method: "POST",
245
+ headers,
246
+ body,
247
+ signal: controller.signal,
248
+ // `/cso` finding 2. undici strips Authorization on a cross-origin
249
+ // redirect, so following one is not a live hole — but a POST carrying a
250
+ // bearer token has no business chasing a hop the operator did not
251
+ // configure, and an ingest endpoint has no reason to issue one.
252
+ redirect: "error",
253
+ });
254
+ // The body read happens INSIDE the try and BEFORE the timer is cleared, so
255
+ // the same `AbortController` that bounds the request also bounds the read.
256
+ // A slow body cannot outlive the hook's wall-clock budget.
257
+ return { kind: "response", status: res.status, detail: await readErrorDetail(res) };
258
+ }
259
+ catch {
260
+ return { kind: "unreachable" };
261
+ }
262
+ finally {
263
+ clearTimeout(timer);
264
+ }
265
+ }
266
+ // ---------------------------------------------------------------------------
267
+ // CR-018 — one attempt, classified, with the matching action applied.
268
+ // ---------------------------------------------------------------------------
269
+ /**
270
+ * A one-way digest of the credential, for the `fatal` stop's key.
271
+ *
272
+ * The second and last call to `expose()` in the package, kept in this module for
273
+ * the same reason as the first: the plaintext's exits are greppable in one file.
274
+ * What is stored is 16 hex characters of a SHA-256 over a high-entropy opaque
275
+ * token — enough to tell one credential from another, and not a preimage of
276
+ * anything. No copy path and no header ever carries it.
277
+ */
278
+ export function credentialFingerprint(credential) {
279
+ return createHash("sha256").update(credential.expose()).digest("hex").slice(0, 16);
280
+ }
281
+ /**
282
+ * Send the undelivered span of one transcript file and apply the failure policy.
283
+ *
284
+ * **Exactly one attempt, ever.** `later` does not retry here; it leaves the
285
+ * offset where it is so the hook that fires on the NEXT turn picks the same
286
+ * bytes up. A loop in this function would reopen D56 plan §D8, the founder
287
+ * override that says a hook must never block or slow a developer's turn.
288
+ *
289
+ * Never throws: the caller is a hook, and an exception on this path is the
290
+ * contract breaking. Every failure resolves to a `Delivery`.
291
+ *
292
+ * `readBody` is supplied by the caller rather than read here, because the caller
293
+ * is the one that has confined `transcript_path` to the Claude Code root
294
+ * (`/cso` finding 1). This module never opens a file.
295
+ */
296
+ export async function deliver(ctx, eof, readBody) {
297
+ // `CR-017d`. The consent gate already refuses a `cwd` outside a work tree, so
298
+ // the hook cannot reach this — but the ledger's isolation must not rest on a
299
+ // check in another module, the same discipline `consent.ts` states about its
300
+ // own error paths. No repo, no send: a delta whose progress cannot be recorded
301
+ // against a repo is one whose progress would be recorded against all of them.
302
+ const key = { repoKey: ctx.repoKey, sessionId: ctx.sessionId };
303
+ if (key.repoKey === "")
304
+ return { kind: "no-repo" };
305
+ const session = loadSessionState(ctx.home, key);
306
+ if (isStopped(session, credentialFingerprint(ctx.credential)))
307
+ return { kind: "stopped" };
308
+ const current = fileState(session, ctx.fileKey);
309
+ const span = nextSpan(current, eof);
310
+ if (span === null)
311
+ return { kind: "nothing-to-send" };
312
+ const body = readBody(span.from, span.to);
313
+ if (body === null)
314
+ return { kind: "nothing-to-send" };
315
+ // `seq` is claimed BEFORE the attempt and persisted regardless of outcome, so
316
+ // it is monotonic per session across retries. A seq reused after a failure
317
+ // would read server-side as a replay of a delta that is not the same bytes.
318
+ const seq = session.seq + 1;
319
+ const outcome = await send(ctx.url, buildIngestHeaders(ctx.credential, {
320
+ sessionId: ctx.sessionId,
321
+ seq,
322
+ byteOffset: span.from,
323
+ fileKey: ctx.fileKey,
324
+ repoSlug: ctx.repoSlug,
325
+ // ⛔ THE ONE PLACE THE WIRE AGENT IS DECIDED — from the containing
326
+ // registry root, never from the flag (D177 §7). Per delivery rather than
327
+ // per hook: a delegated stream is a file in its own right, and the root
328
+ // that contains it is what names its producer.
329
+ agent: agentForTranscript(ctx.home, ctx.env, ctx.transcriptPath),
330
+ head: ctx.head,
331
+ commits: ctx.commits,
332
+ rewrites: ctx.rewrites,
333
+ }), body, ctx.timeoutMs);
334
+ const disposition = classify(outcome);
335
+ const caps = resolveCaps(ctx.env);
336
+ let next = { ...session, seq };
337
+ switch (disposition) {
338
+ case "ok":
339
+ next = withFileState(next, ctx.fileKey, markDelivered(current, span.to, ctx.nowMs));
340
+ // ⛔ TRUNCATE HERE AND NOWHERE ELSE. The spool is append-then-drop-on-2xx,
341
+ // so a commit survives a 500, a timeout and a `later` and is retried by
342
+ // the next hook. Dropping on READ would lose it permanently to one bad
343
+ // response, and `capture_commits` rows can never be retracted (D105/D108).
344
+ //
345
+ // ⛔ `count`, NEVER `shas.length`. `dropSpooled` drops the FIRST N LINES,
346
+ // and a HELD line is consumed without being sent — so truncating by the
347
+ // number of shas on the wire would delete the held line at the head and
348
+ // leave the delivered commit to be re-sent on every hook, forever.
349
+ if (ctx.commits !== undefined && ctx.commits.count > 0) {
350
+ dropSpooled(ctx.home, key, ctx.commits.count);
351
+ }
352
+ // The rewrite spool is its own file with its own cap, so its own drop.
353
+ if (ctx.rewrites !== undefined && ctx.rewrites.length > 0) {
354
+ dropRewrites(ctx.home, key, ctx.rewrites.length);
355
+ }
356
+ break;
357
+ case "later":
358
+ next = withFileState(next, ctx.fileKey, markHeld(current, span, ctx.nowMs, caps));
359
+ break;
360
+ case "never":
361
+ next = withFileState(next, ctx.fileKey, markSkipped(current, span.from, span.to));
362
+ break;
363
+ case "fatal":
364
+ // Credential-level, not payload-level: the offset does NOT advance, because
365
+ // these bytes are fine and a reconnected user should still get them.
366
+ next = {
367
+ ...withFileState(next, ctx.fileKey, markHeld(current, span, ctx.nowMs, caps)),
368
+ stop: {
369
+ at: new Date(ctx.nowMs).toISOString(),
370
+ fingerprint: credentialFingerprint(ctx.credential),
371
+ },
372
+ };
373
+ break;
374
+ }
375
+ saveSessionState(ctx.home, key, next);
376
+ // `outcome.detail` is null on every path except a non-2xx with a parseable
377
+ // error body, so this is a pass-through and not a second decision.
378
+ return {
379
+ kind: "attempted",
380
+ disposition,
381
+ detail: outcome.kind === "response" ? outcome.detail : null,
382
+ };
383
+ }
384
+ //# sourceMappingURL=post.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post.js","sourceRoot":"","sources":["../src/post.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * What "this repository" means — ⛔ **TWO answers, and D184 splits them.**
3
+ *
4
+ * One value served four roles until `D184`. It still serves three of them; the
5
+ * fourth — **the consent key** — moves, and `resolveProjectKeys` is where the two
6
+ * surviving answers are produced together so a caller cannot take the wrong one
7
+ * by accident:
8
+ *
9
+ * ```
10
+ * resolveProjectKeys(cwd) ──┬──> consent gate = canonicalised git COMMON DIR [MOVES]
11
+ * ├──> session bucket = worktree TOPLEVEL (D58/CR-017d)
12
+ * ├──> repoSlug input = worktree TOPLEVEL
13
+ * └──> redaction root = NOT FROM HERE. A SET, and the
14
+ * common dir is never in it (D184 §3).
15
+ * ```
16
+ *
17
+ * ⛔ **The redaction confinement root is NOT one of the values this module
18
+ * returns**, and that omission is the decision. `D184 §3`: if the confinement
19
+ * root follows the consent key to the common dir, every file of an external
20
+ * worktree is judged outside the main clone and `isInside` fail-closes each one
21
+ * into a redaction marker. Capture succeeds, turn counts look right, the content
22
+ * is gone. Widening it is `T2`'s work in `redact.ts`, against a root SET.
23
+ *
24
+ * ## ⛔ Why the consent key had to move at all
25
+ *
26
+ * Consent was keyed on `--show-toplevel`, which **differs per worktree**, with no
27
+ * wildcards and absence-means-no. So an agent session inside a `git worktree`
28
+ * captured **nothing** — no conversation, no commit, no notice — and `D19` makes
29
+ * worktree-per-builder the house convention. `M2`: `rev-parse --git-path hooks`
30
+ * in a linked worktree returns the MAIN clone's `.git/hooks`, so one install
31
+ * already covers every worktree. The common dir is the identity the install
32
+ * already had.
33
+ *
34
+ * ⚠ Deliberately NOT `src/git.ts`. `CR-019` owns that file (deriving the repo slug
35
+ * for `X-Repo-Slug`) and would collide. This module holds only what the consent
36
+ * gate needs; `CR-019` may fold it in when it lands.
37
+ */
38
+ /**
39
+ * The git toplevel for `dir`, or null when it is not inside a work tree.
40
+ *
41
+ * ⚠ **The timeout DERIVES from the hook's remaining budget** (`TODOS[87]`).
42
+ * `execFileSync` blocks the event loop, so the watchdog cannot fire while this
43
+ * call is outstanding — a fixed 5s ceiling here is 5s the watchdog provably
44
+ * cannot cover, and it is one of four such ceilings that summed to 20s against a
45
+ * 5s budget. `null` means the budget is spent, and the answer is the one this
46
+ * function already gives when git cannot tell us: no toplevel, so no consent,
47
+ * so the hook does nothing. Silent by contract and correct by default.
48
+ *
49
+ * ⛔ **The answer is validated by CONTENT: `--show-toplevel` is absolute, always.**
50
+ * Without that gate a `git` on `PATH` that echoed a 40-hex string back would have
51
+ * it `resolve`d against `process.cwd()` and returned as a toplevel — accepted
52
+ * because it arrived where an answer was expected. Fail closed instead.
53
+ */
54
+ export declare function gitToplevel(dir: string): string | null;
55
+ /** The two keys `D184` splits apart. Never one value doing both jobs. */
56
+ export interface ProjectKeys {
57
+ /**
58
+ * The worktree TOPLEVEL. ⛔ **UNCHANGED role** — the session-state bucket
59
+ * (`D58`/`CR-017d` keeps per-worktree isolation deliberately) and the
60
+ * `resolveRepoSlug` input.
61
+ */
62
+ readonly worktree: string;
63
+ /**
64
+ * The canonicalised git COMMON DIR. ⛔ **The consent key, and nothing else.**
65
+ * Shared by a main clone and every worktree linked to it, which is the whole
66
+ * point; never a redaction confinement root (`D184 §3`).
67
+ */
68
+ readonly consent: string;
69
+ }
70
+ /**
71
+ * Both keys for a directory, or **null** — ⛔ **fail closed, never a fallback.**
72
+ *
73
+ * If either spawn fails or either answer fails its content check, there is no
74
+ * partial answer: a guessed consent key grants capture on a repository the user
75
+ * never approved, and a guessed worktree key files a session under the wrong
76
+ * repository permanently.
77
+ *
78
+ * ⚠ **NO MIGRATION PATH FOR EXISTING `projects.json` ENTRIES, deliberately.**
79
+ * The allow list is keyed by string and the key changes shape, so a pre-existing
80
+ * grant at a toplevel stops matching. Pre-launch there are zero users and zero
81
+ * rows (`project_pre_launch_no_data`), so the migration is a no-op with nothing
82
+ * to migrate — writing one would be speculative code guarding a case that cannot
83
+ * occur. The observable cost if that assumption were ever wrong is one re-run of
84
+ * `vibecommit connect`, not a silent grant.
85
+ */
86
+ export declare function resolveProjectKeys(dir: string): ProjectKeys | null;
87
+ /**
88
+ * The worktree toplevel for a directory: the session bucket and slug input.
89
+ *
90
+ * ⛔ **NO LONGER THE CONSENT KEY — `D184` moved that to
91
+ * `resolveProjectKeys(dir).consent`.** The name is kept because the three roles
92
+ * it still serves are unchanged and every caller of it wants the toplevel; a
93
+ * rename would be churn across files this change has no other reason to touch.
94
+ *
95
+ * Falls back to NOTHING. A directory outside a work tree has no project identity,
96
+ * and inventing one from `cwd` would file a session under a key the user cannot
97
+ * see in `vibecommit status`.
98
+ */
99
+ export declare function resolveProjectKey(dir: string): string | null;