@vibe-commit/capture 0.4.2 → 0.4.4

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 (42) hide show
  1. package/README.md +26 -0
  2. package/dist/commands/finalize.js +102 -2
  3. package/dist/commands/finalize.js.map +1 -1
  4. package/dist/commands/status.js +41 -2
  5. package/dist/commands/status.js.map +1 -1
  6. package/dist/copy/strings.d.ts +39 -1
  7. package/dist/copy/strings.js +65 -1
  8. package/dist/copy/strings.js.map +1 -1
  9. package/dist/credential.js +14 -2
  10. package/dist/credential.js.map +1 -1
  11. package/dist/git.d.ts +10 -0
  12. package/dist/git.js +16 -0
  13. package/dist/git.js.map +1 -1
  14. package/dist/header_safety.d.ts +41 -0
  15. package/dist/header_safety.js +48 -0
  16. package/dist/header_safety.js.map +1 -0
  17. package/dist/hooks/entry.d.ts +2 -0
  18. package/dist/hooks/entry.js +18 -3
  19. package/dist/hooks/entry.js.map +1 -1
  20. package/dist/hooks/post_commit.js +23 -4
  21. package/dist/hooks/post_commit.js.map +1 -1
  22. package/dist/inferred_squash.d.ts +67 -0
  23. package/dist/inferred_squash.js +151 -0
  24. package/dist/inferred_squash.js.map +1 -0
  25. package/dist/inferred_tally.d.ts +50 -0
  26. package/dist/inferred_tally.js +99 -0
  27. package/dist/inferred_tally.js.map +1 -0
  28. package/dist/policy.d.ts +17 -0
  29. package/dist/policy.js +7 -1
  30. package/dist/policy.js.map +1 -1
  31. package/dist/post.d.ts +60 -5
  32. package/dist/post.js +373 -50
  33. package/dist/post.js.map +1 -1
  34. package/dist/spool.d.ts +113 -0
  35. package/dist/spool.js +466 -1
  36. package/dist/spool.js.map +1 -1
  37. package/dist/state.d.ts +46 -0
  38. package/dist/state.js +114 -0
  39. package/dist/state.js.map +1 -1
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +2 -2
@@ -0,0 +1,151 @@
1
+ /**
2
+ * `TODOS[177]` — INFER a squash that git never reported.
3
+ *
4
+ * ## ⛔ WHY IT EXISTS
5
+ *
6
+ * `git reset --soft B && git commit -m …` collapses `B..T` into one commit `F`, and **git fires no `post-rewrite` for it**: a
7
+ * reset is not a rewrite as far as the hook is concerned, and the commit that follows is an ordinary `post-commit`. So the pairing
8
+ * `T-and-friends → F` — which the amend and the interactive rebase get from git itself — is never recorded, and `blame_commit`
9
+ * on any of the folded commits calls them untouched (measured by Gate C probe 3, in-session squash agent).
10
+ *
11
+ * ## ⛔ AN INFERENCE IS NOT A FACT, AND THE WIRE SAYS SO
12
+ *
13
+ * Nothing here is reported by git. The pairs go out on their own header (`x-rewrites-inferred`, with the patch-id as the stored
14
+ * evidence) and NEVER on `x-rewrites`, which the server stores as exact. See `IngestDelta.inferredRewrites`.
15
+ *
16
+ * ## THE DETECTOR — all five, or nothing
17
+ *
18
+ * For the commit `F` just made on a BRANCH (not a detached HEAD), an inferred squash is emitted iff ALL of:
19
+ *
20
+ * 1. the branch reflog entry immediately before F's own `commit: …` entry is `reset: moving to <ref>`, whose result is `B` and
21
+ * whose predecessor tip was `T`;
22
+ * 2. `B` is a STRICT ancestor of `T` (`merge-base --is-ancestor`, `B != T`);
23
+ * 3. `parent(F) == B`, and `F` has exactly ONE parent;
24
+ * 4. `tree(F) == tree(T)`;
25
+ * 5. the patch-ids agree: the diff `B → T` and the diff of `F` hash to the same `git patch-id --stable` (redundant with 4 — equal
26
+ * trees on an equal base are the same diff — and kept because it is the EVIDENCE the header stores).
27
+ *
28
+ * The pairs are `(each c in rev-list B..T) → F`, each carrying that patch-id.
29
+ *
30
+ * Everything else emits NOTHING, and each is a different reason with the same silence: the trees differ (reset, edit, commit);
31
+ * `B` is not an ancestor; the reflog is missing, expired, or too short; two resets stand in between; `F` is a merge or an amend or
32
+ * an initial commit (`commit (merge)`, `commit (amend)`, `commit (initial)` are not `commit:`); HEAD is detached; the diff is
33
+ * empty (a squash whose net effect is nothing has no patch-id to store).
34
+ *
35
+ * ## DECISIONS (said here so a reader does not have to guess)
36
+ *
37
+ * - **A ONE-commit "squash" counts.** `reset --soft HEAD~1 && git commit -m "new message"` with the SAME tree is a reword done by
38
+ * reset: `B..T` is `{T}`, `F` has the same tree and parent. The pair `T → F` is as true as any other, and it is exactly what a
39
+ * `git commit --amend` would have reported. The same reset WITH an edit changes the tree and emits nothing (condition 4).
40
+ * - **At most `MAX_INFERRED_COMMITS` (64) commits per squash, and a larger fold emits NOTHING** (server contract: the reader counts
41
+ * anything past 64 per request as dropped, and a partial edge set is worse than none). It is TALLIED (`inferred_tally.ts`), not
42
+ * truncated: the squash was detected, the contract could not carry it, and `status` says so.
43
+ * - **Only 40-hex repositories.** The wire contract is `ancestor40:successor40:patchid40` and the server refuses 64-hex, so a sha256
44
+ * repository emits nothing — also TALLIED, but only once the five conditions hold (a sha256 repo's ordinary commits are not noise).
45
+ * - **Needs a reflog.** `core.logAllRefUpdates=false` and bare repositories keep none, so nothing is inferred there.
46
+ *
47
+ * ## ⛔ THIS RUNS INSIDE THE USER'S `git commit`
48
+ *
49
+ * Never throws; no network; writes nothing itself. +1 spawn on every ordinary commit (the reflog read) and a handful more ONLY when
50
+ * the reflog says a reset came first. `post_commit.ts` arms no spawn budget (see its header), so each probe takes its own ceiling.
51
+ */
52
+ import { gitPatchId, gitProbe, gitRangePatchId } from "./git.js";
53
+ /** Most folded commits one squash may name. More than this and the squash emits NOTHING (and is tallied): the server caps a request at 64. */
54
+ export const MAX_INFERRED_COMMITS = 64;
55
+ /** Object names as git prints them: 40-hex (sha1) or 64-hex (sha256). Only 40-hex may be SENT. */
56
+ const OID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
57
+ const SHA1 = /^[0-9a-f]{40}$/;
58
+ const NONE = { pairs: [], skipped: null };
59
+ /**
60
+ * The newest `n` entries of `refs/heads/<branch>`'s reflog, newest first, or null if git cannot answer. A line that is not
61
+ * `<40-hex> TAB <subject>` makes the whole answer null: half a reflog is how a wrong predecessor gets picked.
62
+ */
63
+ function branchReflog(dir, branch, n) {
64
+ // ⛔ `--no-show-signature` (VG pre-review, 2026-10-08): with the user's `log.showSignature=true` and an
65
+ // SSH-signed commit, `git log` prepends a signature-verification banner to the formatted line, which
66
+ // breaks the `%H\t%gs` parse — measured as the fold reading 0 pairs, not an error. The flag is unconditional:
67
+ // this output is parsed by byte position, never shown to anyone, so there is nothing for the banner to serve.
68
+ const out = gitProbe(dir, ["log", "--no-show-signature", "-g", "--no-abbrev", "--format=%H%x09%gs", "-n", String(n), `refs/heads/${branch}`, "--"]);
69
+ if (out === null)
70
+ return null;
71
+ const entries = [];
72
+ for (const line of out.split("\n")) {
73
+ if (line === "")
74
+ continue;
75
+ const tab = line.indexOf("\t");
76
+ const sha = tab < 0 ? "" : line.slice(0, tab);
77
+ if (!OID.test(sha))
78
+ return null;
79
+ entries.push({ sha, subject: line.slice(tab + 1) });
80
+ }
81
+ return entries;
82
+ }
83
+ /**
84
+ * The inferred pairs for the commit `head` just made — or, for a squash that WAS detected but cannot be sent, why not (so the caller can
85
+ * tally it). `head.branch` is null on a detached HEAD, which is nothing to infer. Never throws.
86
+ */
87
+ export function detectSquash(dir, head) {
88
+ try {
89
+ return infer(dir, head);
90
+ }
91
+ catch {
92
+ return NONE;
93
+ }
94
+ }
95
+ function infer(dir, head) {
96
+ const F = head.sha;
97
+ if (head.branch === null || !OID.test(F))
98
+ return NONE;
99
+ // (1) the reflog: F's own plain `commit:` entry, directly preceded by a reset, itself preceded by the tip it moved away from.
100
+ const log = branchReflog(dir, head.branch, 3);
101
+ if (log === null || log.length < 3)
102
+ return NONE;
103
+ const [commitEntry, resetEntry, before] = log;
104
+ // `commit: ` exactly. `commit (amend): `, `commit (merge): `, `commit (initial): ` and `commit (cherry-pick): ` are other events.
105
+ if (commitEntry.sha !== F || !commitEntry.subject.startsWith("commit: "))
106
+ return NONE;
107
+ if (!/^reset: moving to \S/.test(resetEntry.subject))
108
+ return NONE;
109
+ const B = resetEntry.sha;
110
+ const T = before.sha;
111
+ // (2) B is a STRICT ancestor of T. `merge-base --is-ancestor` exits 0 for yes: gitProbe answers "" for yes and null for no/unanswerable.
112
+ if (B === T)
113
+ return NONE;
114
+ if (gitProbe(dir, ["merge-base", "--is-ancestor", B, T]) === null)
115
+ return NONE;
116
+ // (3) parent(F) == B, exactly one parent. `rev-list --parents -n 1 F` prints `F P1 [P2 …]`.
117
+ const parents = gitProbe(dir, ["rev-list", "--parents", "-n", "1", F]);
118
+ if (parents === null)
119
+ return NONE;
120
+ const fields = parents.trim().split(/\s+/);
121
+ if (fields.length !== 2 || fields[0] !== F || fields[1] !== B)
122
+ return NONE;
123
+ // (4) tree(F) == tree(T).
124
+ const trees = gitProbe(dir, ["rev-parse", `${F}^{tree}`, `${T}^{tree}`]);
125
+ if (trees === null)
126
+ return NONE;
127
+ const [treeF, treeT] = trees.trim().split("\n");
128
+ if (!OID.test(treeF ?? "") || treeF !== treeT)
129
+ return NONE;
130
+ // (5) the patch-ids, the evidence we store. A missing one (an empty diff, a git that would not answer) is "no evidence": nothing.
131
+ const patchB = gitRangePatchId(dir, B, T);
132
+ const patchF = gitPatchId(dir, F);
133
+ if (patchB === null || patchF === null || patchB !== patchF || !OID.test(patchB))
134
+ return NONE;
135
+ // The squash is DETECTED. From here on, "nothing" is a tallied skip, not a non-event.
136
+ // One more than the cap, so a fold of exactly 64 is sendable and 65 is known to be over.
137
+ const listed = gitProbe(dir, ["rev-list", `--max-count=${MAX_INFERRED_COMMITS + 1}`, `${B}..${T}`]);
138
+ if (listed === null)
139
+ return NONE;
140
+ const folded = listed.split("\n").filter((l) => l !== "");
141
+ if (folded.length === 0 || !folded.every((c) => OID.test(c) && c !== F))
142
+ return NONE;
143
+ // sha256 repository (any object name 64-hex): the server refuses 64-hex, so nothing is sent.
144
+ if (![F, B, patchB, ...folded].every((x) => SHA1.test(x)))
145
+ return { pairs: [], skipped: "sha256" };
146
+ // A fold of more than 64: the server counts the excess as dropped and a partial edge set is worse than none, so none.
147
+ if (folded.length > MAX_INFERRED_COMMITS)
148
+ return { pairs: [], skipped: "oversize" };
149
+ return { pairs: folded.map((ancestor) => ({ ancestor, successor: F, patchId: patchB })), skipped: null };
150
+ }
151
+ //# sourceMappingURL=inferred_squash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inferred_squash.js","sourceRoot":"","sources":["../src/inferred_squash.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `TODOS[177]` — the counts of squash pairs that were DETECTED but never ended up recorded by the server, so the
3
+ * silence is not a non-event.
4
+ *
5
+ * `oversize`/`sha256` are a contract limit of the server's `x-rewrites-inferred` reader, caught before the squash is
6
+ * ever spooled: a fold of more than 64 commits (the server counts anything past 64 per request as dropped, and a
7
+ * partial edge set is worse than none), and a sha256 repository (the server refuses 64-hex shas). Either way the
8
+ * client emits NOTHING for that squash and counts it here instead — one per squash, since neither ever had pairs on
9
+ * disk to count.
10
+ *
11
+ * ⛔⛔ `set_aside`/`pruned` (VL, 2026-10-08, VG's silent-loss finding over a real `finalize` short-ack run) exist
12
+ * because set-aside and the 7-day prune both end by deleting or quietly carrying forward the group line a reader
13
+ * would otherwise count from the live spool file. Without a durable record here, a group that was set aside and
14
+ * then `finalize` deleted the (now fully-resolved) spool for — or one that aged out under the prune — left no
15
+ * trace anywhere: MEASURED, `status` read 0 before the event and 0 after, as if 70 real pairs had never existed.
16
+ * These two count PAIRS, recorded at the moment of the event, before any deletion can happen — which is what
17
+ * makes deleting the spool afterward fine.
18
+ *
19
+ * ⚠ THIS TALLY IS THE ONE SOURCE OF TRUTH FOR `set_aside` (VG's follow-up finding, item (b)): an earlier version
20
+ * of this fix also added a LIVE gauge (`inferredSetAsideForRepo` in `spool.ts`, reading the still-on-disk file
21
+ * directly), which double-counted the SAME pairs once a group aged into the prune — `status` announced 80 pairs
22
+ * for one 40-pair group, 40 from each source. That gauge is gone; `inferredUnacknowledgedForRepo` ("still
23
+ * waiting on an ack," a genuinely different fact) is the only live gauge left in this area.
24
+ *
25
+ * Counts only — never a sha, a path or a branch. One small file per repo beside the session files, named so no
26
+ * scanner matches it (`pruned-pending.tally`'s rule); a tally older than 7 days restarts instead of accumulating.
27
+ * Never throws: this can run inside the user's `git commit`.
28
+ */
29
+ export type SkippedReason = "oversize" | "sha256" | "set_aside" | "pruned";
30
+ export interface SkippedTally {
31
+ readonly oversize: number;
32
+ readonly sha256: number;
33
+ readonly set_aside: number;
34
+ readonly pruned: number;
35
+ }
36
+ /**
37
+ * Count `count` (default 1) pairs/squashes skipped for one reason. Never throws.
38
+ *
39
+ * ⛔⛔ `set_aside` and `pruned` (VL, 2026-10-08, VG's silent-loss finding) are PAIR counts — unlike `oversize`/
40
+ * `sha256`, which count SQUASHES (an oversize/sha256 squash is never spooled, so there is nothing to count by
41
+ * pair). Both record the ONLY durable trace of pairs that a live gauge (`inferredUnacknowledgedForRepo`) stops
42
+ * counting the moment the file backing it is deleted or pruned. Called BEFORE that deletion — `set_aside` from
43
+ * `post.ts` at the moment it calls `setAsideInferredGroup`, `pruned` from the prune sweep at the moment it
44
+ * discards a group with NEITHER mark (never a delivered or already-tallied-set-aside one, or this would
45
+ * double-count — `spool.ts`'s `settleInferred`) — so "deleting the spool is fine" (VL) because this tally is
46
+ * what makes it fine.
47
+ */
48
+ export declare function recordInferredSkipped(home: string, repoKey: string, reason: SkippedReason, nowMs?: number, count?: number): void;
49
+ /** The counts to announce, or null when nothing was skipped in the last 7 days. */
50
+ export declare function readInferredSkipped(home: string, repoKey: string, nowMs: number): SkippedTally | null;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `TODOS[177]` — the counts of squash pairs that were DETECTED but never ended up recorded by the server, so the
3
+ * silence is not a non-event.
4
+ *
5
+ * `oversize`/`sha256` are a contract limit of the server's `x-rewrites-inferred` reader, caught before the squash is
6
+ * ever spooled: a fold of more than 64 commits (the server counts anything past 64 per request as dropped, and a
7
+ * partial edge set is worse than none), and a sha256 repository (the server refuses 64-hex shas). Either way the
8
+ * client emits NOTHING for that squash and counts it here instead — one per squash, since neither ever had pairs on
9
+ * disk to count.
10
+ *
11
+ * ⛔⛔ `set_aside`/`pruned` (VL, 2026-10-08, VG's silent-loss finding over a real `finalize` short-ack run) exist
12
+ * because set-aside and the 7-day prune both end by deleting or quietly carrying forward the group line a reader
13
+ * would otherwise count from the live spool file. Without a durable record here, a group that was set aside and
14
+ * then `finalize` deleted the (now fully-resolved) spool for — or one that aged out under the prune — left no
15
+ * trace anywhere: MEASURED, `status` read 0 before the event and 0 after, as if 70 real pairs had never existed.
16
+ * These two count PAIRS, recorded at the moment of the event, before any deletion can happen — which is what
17
+ * makes deleting the spool afterward fine.
18
+ *
19
+ * ⚠ THIS TALLY IS THE ONE SOURCE OF TRUTH FOR `set_aside` (VG's follow-up finding, item (b)): an earlier version
20
+ * of this fix also added a LIVE gauge (`inferredSetAsideForRepo` in `spool.ts`, reading the still-on-disk file
21
+ * directly), which double-counted the SAME pairs once a group aged into the prune — `status` announced 80 pairs
22
+ * for one 40-pair group, 40 from each source. That gauge is gone; `inferredUnacknowledgedForRepo` ("still
23
+ * waiting on an ack," a genuinely different fact) is the only live gauge left in this area.
24
+ *
25
+ * Counts only — never a sha, a path or a branch. One small file per repo beside the session files, named so no
26
+ * scanner matches it (`pruned-pending.tally`'s rule); a tally older than 7 days restarts instead of accumulating.
27
+ * Never throws: this can run inside the user's `git commit`.
28
+ */
29
+ import { readFileSync, writeFileSync } from "node:fs";
30
+ import { join } from "node:path";
31
+ import { repoSessionsDir } from "./paths.js";
32
+ const TALLY_NAME = "inferred-skipped.tally";
33
+ const TTL_MS = 7 * 24 * 3600_000;
34
+ /**
35
+ * ⚠ ADDITIVE: `set_aside`/`pruned` default to 0 when absent, rather than invalidating a tally written by 0.4.2 or
36
+ * earlier (which has `oversize`/`sha256` and no concept of the other two) — a missing field means "before this
37
+ * existed," never "corrupt," same instinct as `SpoolEntry.observedAt?`.
38
+ */
39
+ function read(dir) {
40
+ try {
41
+ const t = JSON.parse(readFileSync(join(dir, TALLY_NAME), "utf8"));
42
+ if (!(Number.isSafeInteger(t.oversize) && Number.isSafeInteger(t.sha256) && typeof t.at === "number" && Number.isFinite(t.at)))
43
+ return null;
44
+ return {
45
+ oversize: t.oversize,
46
+ sha256: t.sha256,
47
+ set_aside: Number.isSafeInteger(t.set_aside) ? t.set_aside : 0,
48
+ pruned: Number.isSafeInteger(t.pruned) ? t.pruned : 0,
49
+ at: t.at,
50
+ };
51
+ }
52
+ catch {
53
+ /* absent or damaged: no tally */
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * Count `count` (default 1) pairs/squashes skipped for one reason. Never throws.
59
+ *
60
+ * ⛔⛔ `set_aside` and `pruned` (VL, 2026-10-08, VG's silent-loss finding) are PAIR counts — unlike `oversize`/
61
+ * `sha256`, which count SQUASHES (an oversize/sha256 squash is never spooled, so there is nothing to count by
62
+ * pair). Both record the ONLY durable trace of pairs that a live gauge (`inferredUnacknowledgedForRepo`) stops
63
+ * counting the moment the file backing it is deleted or pruned. Called BEFORE that deletion — `set_aside` from
64
+ * `post.ts` at the moment it calls `setAsideInferredGroup`, `pruned` from the prune sweep at the moment it
65
+ * discards a group with NEITHER mark (never a delivered or already-tallied-set-aside one, or this would
66
+ * double-count — `spool.ts`'s `settleInferred`) — so "deleting the spool is fine" (VL) because this tally is
67
+ * what makes it fine.
68
+ */
69
+ export function recordInferredSkipped(home, repoKey, reason, nowMs = Date.now(), count = 1) {
70
+ try {
71
+ const dir = repoSessionsDir(home, repoKey);
72
+ if (dir === null || count <= 0)
73
+ return;
74
+ const prev = read(dir);
75
+ const carry = prev !== null && nowMs - prev.at <= TTL_MS ? prev : { oversize: 0, sha256: 0, set_aside: 0, pruned: 0, at: nowMs };
76
+ const next = {
77
+ oversize: carry.oversize + (reason === "oversize" ? count : 0),
78
+ sha256: carry.sha256 + (reason === "sha256" ? count : 0),
79
+ set_aside: carry.set_aside + (reason === "set_aside" ? count : 0),
80
+ pruned: carry.pruned + (reason === "pruned" ? count : 0),
81
+ at: nowMs,
82
+ };
83
+ writeFileSync(join(dir, TALLY_NAME), `${JSON.stringify(next)}\n`, { mode: 0o600 });
84
+ }
85
+ catch {
86
+ /* the event already happened; failing to count it must not throw */
87
+ }
88
+ }
89
+ /** The counts to announce, or null when nothing was skipped in the last 7 days. */
90
+ export function readInferredSkipped(home, repoKey, nowMs) {
91
+ const dir = repoSessionsDir(home, repoKey);
92
+ if (dir === null)
93
+ return null;
94
+ const t = read(dir);
95
+ if (t === null || nowMs - t.at > TTL_MS || (t.oversize === 0 && t.sha256 === 0 && t.set_aside === 0 && t.pruned === 0))
96
+ return null;
97
+ return { oversize: t.oversize, sha256: t.sha256, set_aside: t.set_aside, pruned: t.pruned };
98
+ }
99
+ //# sourceMappingURL=inferred_tally.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inferred_tally.js","sourceRoot":"","sources":["../src/inferred_tally.ts"],"names":[],"mappings":"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"}
package/dist/policy.d.ts CHANGED
@@ -109,6 +109,23 @@ export interface FileState {
109
109
  * would be a confident lie about a value we simply do not have.
110
110
  */
111
111
  readonly lastSentAt: number;
112
+ /**
113
+ * ⛔⛔ `TODOS[182]` — STICKY, and that word is load-bearing. Set exactly once,
114
+ * on this file's first 431 (a header the server's limit refused), and never
115
+ * cleared for the rest of this session: every attempt after that — hook or
116
+ * `finalize`, this span or the next one — builds the deny-list (lean) header
117
+ * set from the start, never re-evaluated size or charset attempt by attempt.
118
+ *
119
+ * ⚠ **Not re-evaluated is the point.** A flag that cleared itself once a
120
+ * smaller delta happened to fit would alternate between full and lean
121
+ * requests depending on what this session's NEXT turn happens to contain,
122
+ * which is exactly the "sometimes it works" shape a user cannot debug.
123
+ * `false` in `EMPTY_FILE_STATE`, so a state file written before this field
124
+ * existed reads as "never gone lean" — the correct default: nothing about an
125
+ * old file's history recorded a 431 for `markSkipped`/`enforceCaps` to carry
126
+ * forward if it did not ask.
127
+ */
128
+ readonly lean: boolean;
112
129
  }
113
130
  export declare const EMPTY_FILE_STATE: FileState;
114
131
  /**
package/dist/policy.js CHANGED
@@ -108,6 +108,7 @@ export const EMPTY_FILE_STATE = {
108
108
  gapBytes: 0,
109
109
  gapCount: 0,
110
110
  lastSentAt: 0,
111
+ lean: false,
111
112
  };
112
113
  /**
113
114
  * The caps.
@@ -193,6 +194,11 @@ export function markSkipped(state, from, to) {
193
194
  backlog: [],
194
195
  gapBytes: state.gapBytes + skipped,
195
196
  gapCount: state.gapCount + (skipped > 0 ? 1 : 0),
197
+ // `TODOS[182]`. Carried, never cleared here: `lean` is STICKY, and a `never`
198
+ // disposition (including the "already lean, still 431" terminal case) says
199
+ // nothing about whether the NEXT span on this file should go back to
200
+ // building full headers.
201
+ lean: state.lean,
196
202
  };
197
203
  }
198
204
  /**
@@ -239,6 +245,6 @@ export function enforceCaps(state, nowMs, caps) {
239
245
  gapCount += 1;
240
246
  backlog = backlog.slice(1);
241
247
  }
242
- return { sentOffset, backlog, gapBytes, gapCount, lastSentAt: state.lastSentAt };
248
+ return { sentOffset, backlog, gapBytes, gapCount, lastSentAt: state.lastSentAt, lean: state.lean };
243
249
  }
244
250
  //# sourceMappingURL=policy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAQzC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsC,IAAI,GAAG,CAAC;IACvE,gFAAgF;IAChF,CAAC,GAAG,EAAE,OAAO,CAAC;IACd,uEAAuE;IACvE,CAAC,GAAG,EAAE,OAAO,CAAC;IACd,2EAA2E;IAC3E,CAAC,GAAG,EAAE,OAAO,CAAC;IACd,0DAA0D;IAC1D,CAAC,GAAG,EAAE,OAAO,CAAC;IACd,6CAA6C;IAC7C,CAAC,GAAG,EAAE,OAAO,CAAC;CACf,CAAC,CAAC;AAEH,uDAAuD;AACvD,MAAM,CAAC,MAAM,kBAAkB,GAAiB,OAAO,CAAC;AAExD,kEAAkE;AAClE,MAAM,CAAC,MAAM,aAAa,GAAiB,OAAO,CAAC;AAEnD;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAiB,OAAO,CAAC;AAEnD,2DAA2D;AAC3D,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;QAAE,OAAO,IAAI,CAAC;IAC/C,MAAM,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACzC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;QAAE,OAAO,kBAAkB,CAAC;IAC7D,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,QAAQ,CAAC,OAAoB;IAC3C,OAAO,OAAO,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AACzF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,GAAiB;IAC3C,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,OAAO;YACV,OAAO,MAAM,CAAC,YAAY,CAAC;QAC7B,KAAK,OAAO;YACV,OAAO,MAAM,CAAC,YAAY,CAAC;QAC7B,KAAK,OAAO;YACV,OAAO,MAAM,CAAC,SAAS,CAAC;IAC5B,CAAC;AACH,CAAC;AA4CD,MAAM,CAAC,MAAM,gBAAgB,GAAc;IACzC,UAAU,EAAE,CAAC;IACb,OAAO,EAAE,EAAE;IACX,QAAQ,EAAE,CAAC;IACX,QAAQ,EAAE,CAAC;IACX,UAAU,EAAE,CAAC;CACd,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,KAAK,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IACtB,KAAK,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;IAC1B,OAAO,EAAE,GAAG;CACJ,CAAC;AAQX;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,GAAsB;IAChD,OAAO;QACL,KAAK,EAAE,UAAU,CAAC,GAAG,CAAC,4BAA4B,EAAE,YAAY,CAAC,KAAK,CAAC;QACvE,KAAK,EAAE,UAAU,CAAC,GAAG,CAAC,6BAA6B,EAAE,YAAY,CAAC,KAAK,CAAC;QACxE,OAAO,EAAE,UAAU,CAAC,GAAG,CAAC,8BAA8B,EAAE,YAAY,CAAC,OAAO,CAAC;KAC9E,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,GAAuB,EAAE,QAAgB;IAC3D,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACvC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC3B,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;AACnE,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,YAAY,CAAC,KAAgB;IAC3C,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACzC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAgB,EAAE,GAAW;IACpD,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC;IAC3D,OAAO,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,KAAgB,EAAE,EAAU,EAAE,KAAa;IACvE,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AACtE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAAgB,EAAE,IAAY,EAAE,EAAU;IACpE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,CAAC;IACvC,OAAO;QACL,6EAA6E;QAC7E,qDAAqD;QACrD,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,UAAU,EAAE,EAAE;QACd,OAAO,EAAE,EAAE;QACX,QAAQ,EAAE,KAAK,CAAC,QAAQ,GAAG,OAAO;QAClC,QAAQ,EAAE,KAAK,CAAC,QAAQ,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KACjD,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,QAAQ,CACtB,KAAgB,EAChB,IAAkC,EAClC,KAAa,EACb,IAAiB;IAEjB,MAAM,IAAI,GACR,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,IAAI;QAC5B,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,0EAA0E;YAC1E,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAEvD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,IAAI,EAAE,EAAE,IAAI,IAAI,CAAC,UAAU,CAAC;IAC3C,MAAM,OAAO,GACX,IAAI,CAAC,EAAE,GAAG,MAAM;QACd,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC;QAC7D,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;IAEnB,OAAO,WAAW,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAAgB,EAAE,KAAa,EAAE,IAAiB;IAC5E,IAAI,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC;IAClC,IAAI,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC5B,IAAI,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;IAC9B,IAAI,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;IAE9B,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QAC1B,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC;QAChF,MAAM,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;QAC/C,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;QAClD,IAAI,CAAC,SAAS,IAAI,CAAC,OAAO,IAAI,CAAC,WAAW;YAAE,MAAM;QAElD,UAAU,GAAG,MAAM,CAAC,EAAE,CAAC;QACvB,QAAQ,IAAI,MAAM,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC;QACpC,QAAQ,IAAI,CAAC,CAAC;QACd,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC;AACnF,CAAC"}
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
package/dist/post.d.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  import { type SelectedAgentId } from "./agents/types.js";
21
21
  import type { IngestCredential } from "./credential.js";
22
22
  import { type Disposition } from "./policy.js";
23
- import { type Observed } from "./spool.js";
23
+ import { type InferredGroupId, type Observed } from "./spool.js";
24
24
  /** Production data plane. Overridable for tests and for a self-hosted server. */
25
25
  export declare const DEFAULT_INGEST_URL = "https://api.vibecommit.ai/ingest/v1/session";
26
26
  /**
@@ -122,6 +122,15 @@ export interface IngestDelta {
122
122
  * be wrong about.
123
123
  */
124
124
  readonly rewrites?: readonly string[];
125
+ /**
126
+ * INFERRED squash pairs — `ancestor:successor:patchid`, already in wire spelling (`capInferred`). `TODOS[177]`.
127
+ *
128
+ * ⛔ **THEIR OWN HEADER, `x-rewrites-inferred`, AND NEVER `x-rewrites`.** `x-rewrites` is what GIT REPORTED (`post-rewrite`) and the
129
+ * server stores it as exact; a pair this client INFERRED from a reflog and a patch-id is a different claim and must be stored as
130
+ * one. Folding the two into one header would launder an inference into a fact, which is the one error this wire cannot take back
131
+ * (`match_kind` is in the primary key and the history is append-only).
132
+ */
133
+ readonly inferredRewrites?: readonly string[];
125
134
  }
126
135
  /**
127
136
  * Build the request headers.
@@ -140,6 +149,19 @@ export interface IngestDelta {
140
149
  * at all is not. So the wire carries the irreplaceable half.
141
150
  */
142
151
  export declare function buildIngestHeaders(credential: IngestCredential, delta: IngestDelta): Record<string, string>;
152
+ /**
153
+ * ⛔⛔ `TODOS[182]` — THE FIVE, AND EXACTLY THE FIVE. `lean` is defined by
154
+ * SUBTRACTION from this set, never by an allow-list: an earlier draft of this
155
+ * fix hand-picked which headers to KEEP and silently dropped `content-type` /
156
+ * `content-encoding` (the server could no longer parse the zstd body) and
157
+ * `x-agent` (a lean FIRST delta would pin `unknown` forever under STICKY
158
+ * `lean` — `UNKNOWN_AGENT_ID`'s own docblock above). A deny-list fails SAFE:
159
+ * anything not named here defaults to "stays", and the only headers worth
160
+ * dropping are the ones whose SIZE is unbounded by a count cap — a branch name
161
+ * and the four list headers. `leanHeaders` is tested by SET EQUALITY against
162
+ * `buildIngestHeaders`'s full output, never by eyeballing an allow-list.
163
+ */
164
+ export declare const LEAN_OMIT_HEADERS: ReadonlySet<string>;
143
165
  /**
144
166
  * The wire's error code for org-approval-pending — `CR-128`, D81.
145
167
  *
@@ -232,6 +254,15 @@ export type SendOutcome = {
232
254
  * verb sends, so `null` after asking for finality means "not done", never "done".
233
255
  */
234
256
  readonly finalAck: "sealed" | "already" | "refused" | null;
257
+ /**
258
+ * ⭐ `X-Rewrites-Inferred-Stored` (VG pre-review, 2026-10-08) — how many of the pairs we SENT on
259
+ * `x-rewrites-inferred` the server actually recorded. `null` when the header is absent (an older
260
+ * server, including the study servers before this deploys) or malformed: "not determinable" is not
261
+ * "zero". A group is only marked delivered when this equals the exact count we sent — anything less,
262
+ * including `null`, keeps every group for the next hook. `ON CONFLICT DO NOTHING` on the server makes
263
+ * a resend idempotent, so keeping on doubt costs a duplicate request, never a duplicate row.
264
+ */
265
+ readonly inferredStored: number | null;
235
266
  }
236
267
  /** No response at all — DNS, refused, TLS, timeout. Not an HTTP status. */
237
268
  | {
@@ -289,6 +320,19 @@ export type Delivery =
289
320
  * behaves exactly as before — which is what the hook path does.
290
321
  */
291
322
  readonly detail: ErrorDetail | null;
323
+ /**
324
+ * `TODOS[177]` follow-up (VL, 2026-10-08) — the server's `x-rewrites-inferred-stored` ack, pass-through from
325
+ * `SendOutcome` (so `null` whenever the response didn't carry it, including a request that sent no inferred
326
+ * pairs at all — the caller already knows whether it asked). `finalize`'s drain loop reads this to decide
327
+ * whether to keep looping: an ABSENT ack (old server) means stop immediately, a SHORT one means move on.
328
+ */
329
+ readonly inferredStored: number | null;
330
+ /**
331
+ * `TODOS[177]` follow-up (VL, 2026-10-08, VG's silent-loss finding) — how many pairs THIS call just set aside
332
+ * (0 when it didn't). `finalize`'s drain loop sums this across the main delivery and every drain request to
333
+ * print ONE stderr line for the whole run, rather than one per request.
334
+ */
335
+ readonly inferredSetAsidePairs: number;
292
336
  };
293
337
  export interface DeliveryContext {
294
338
  readonly home: string;
@@ -344,6 +388,14 @@ export interface DeliveryContext {
344
388
  readonly commits?: Observed;
345
389
  /** Rewrite pairs, wire-spelled. Main delivery only, for the same reason. */
346
390
  readonly rewrites?: readonly string[];
391
+ /** `TODOS[177]` — the inferred pairs this delivery carries (`capInferred`), wire-spelled. */
392
+ readonly inferredRewrites?: readonly string[];
393
+ /**
394
+ * The IDENTITIES of the groups behind `inferredRewrites` (`capInferred`'s `groups`) — what `dropInferredGroups` marks
395
+ * delivered, and ONLY when the server's `x-rewrites-inferred-stored` ack equals `inferredRewrites.length` exactly.
396
+ * `undefined` here means the same as an empty `inferredRewrites`: nothing to drop.
397
+ */
398
+ readonly inferredGroups?: readonly InferredGroupId[];
347
399
  /**
348
400
  * ⭐ `TODOS[146]` — assert the session is over (`X-Session-Final: 1`) and send EVEN WHEN THE
349
401
  * OFFSET IS AT EOF, as an empty delta: the common one-shot case is a session whose bytes all
@@ -361,10 +413,13 @@ export interface DeliveryContext {
361
413
  /**
362
414
  * Send the undelivered span of one transcript file and apply the failure policy.
363
415
  *
364
- * **Exactly one attempt, ever.** `later` does not retry here; it leaves the
365
- * offset where it is so the hook that fires on the NEXT turn picks the same
366
- * bytes up. A loop in this function would reopen D56 plan §D8, the founder
367
- * override that says a hook must never block or slow a developer's turn.
416
+ * **At most one NETWORK attempt per hook, still — exactly D56 §D8's rule.** A
417
+ * `finalize` lean retry (below) is the one exception D56 §D8 itself does not
418
+ * cover: it governs HOOKS, and `finalize` is a synchronous, user-invoked verb
419
+ * that already makes 1 + up to `FINALIZE_DRAIN_MAX_ITERATIONS` requests by
420
+ * design (VL's explicit ruling, `TODOS[182]`). On the hook path, `later` still
421
+ * does not retry here; it leaves the offset where it is so the hook that fires
422
+ * on the NEXT turn picks the same bytes up.
368
423
  *
369
424
  * Never throws: the caller is a hook, and an exception on this path is the
370
425
  * contract breaking. Every failure resolves to a `Delivery`.