@vibe-commit/capture 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/agents/claude_code.d.ts +11 -0
  4. package/dist/agents/claude_code.js +162 -0
  5. package/dist/agents/claude_code.js.map +1 -0
  6. package/dist/agents/codex_cli.d.ts +15 -0
  7. package/dist/agents/codex_cli.js +67 -0
  8. package/dist/agents/codex_cli.js.map +1 -0
  9. package/dist/agents/cursor.d.ts +21 -0
  10. package/dist/agents/cursor.js +89 -0
  11. package/dist/agents/cursor.js.map +1 -0
  12. package/dist/agents/registry.d.ts +184 -0
  13. package/dist/agents/registry.js +212 -0
  14. package/dist/agents/registry.js.map +1 -0
  15. package/dist/agents/types.d.ts +279 -0
  16. package/dist/agents/types.js +67 -0
  17. package/dist/agents/types.js.map +1 -0
  18. package/dist/commands/connect.d.ts +36 -0
  19. package/dist/commands/connect.js +499 -0
  20. package/dist/commands/connect.js.map +1 -0
  21. package/dist/commands/context.d.ts +45 -0
  22. package/dist/commands/context.js +14 -0
  23. package/dist/commands/context.js.map +1 -0
  24. package/dist/commands/report.d.ts +105 -0
  25. package/dist/commands/report.js +430 -0
  26. package/dist/commands/report.js.map +1 -0
  27. package/dist/commands/status.d.ts +29 -0
  28. package/dist/commands/status.js +236 -0
  29. package/dist/commands/status.js.map +1 -0
  30. package/dist/commands/why.d.ts +349 -0
  31. package/dist/commands/why.js +937 -0
  32. package/dist/commands/why.js.map +1 -0
  33. package/dist/consent.d.ts +59 -0
  34. package/dist/consent.js +111 -0
  35. package/dist/consent.js.map +1 -0
  36. package/dist/copy/claims.d.ts +79 -0
  37. package/dist/copy/claims.js +343 -0
  38. package/dist/copy/claims.js.map +1 -0
  39. package/dist/copy/grades.d.ts +117 -0
  40. package/dist/copy/grades.js +96 -0
  41. package/dist/copy/grades.js.map +1 -0
  42. package/dist/copy/index.d.ts +12 -0
  43. package/dist/copy/index.js +13 -0
  44. package/dist/copy/index.js.map +1 -0
  45. package/dist/copy/strings.d.ts +1328 -0
  46. package/dist/copy/strings.js +1243 -0
  47. package/dist/copy/strings.js.map +1 -0
  48. package/dist/copy/time.d.ts +27 -0
  49. package/dist/copy/time.js +43 -0
  50. package/dist/copy/time.js.map +1 -0
  51. package/dist/credential.d.ts +211 -0
  52. package/dist/credential.js +298 -0
  53. package/dist/credential.js.map +1 -0
  54. package/dist/exit.d.ts +59 -0
  55. package/dist/exit.js +60 -0
  56. package/dist/exit.js.map +1 -0
  57. package/dist/git.d.ts +299 -0
  58. package/dist/git.js +653 -0
  59. package/dist/git.js.map +1 -0
  60. package/dist/hooks/entry.d.ts +277 -0
  61. package/dist/hooks/entry.js +1092 -0
  62. package/dist/hooks/entry.js.map +1 -0
  63. package/dist/hooks/post_commit.d.ts +59 -0
  64. package/dist/hooks/post_commit.js +160 -0
  65. package/dist/hooks/post_commit.js.map +1 -0
  66. package/dist/hooks/post_rewrite.d.ts +59 -0
  67. package/dist/hooks/post_rewrite.js +161 -0
  68. package/dist/hooks/post_rewrite.js.map +1 -0
  69. package/dist/hooks/session_end.d.ts +101 -0
  70. package/dist/hooks/session_end.js +120 -0
  71. package/dist/hooks/session_end.js.map +1 -0
  72. package/dist/index.d.ts +34 -0
  73. package/dist/index.js +360 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/install.d.ts +153 -0
  76. package/dist/install.js +284 -0
  77. package/dist/install.js.map +1 -0
  78. package/dist/json.d.ts +100 -0
  79. package/dist/json.js +94 -0
  80. package/dist/json.js.map +1 -0
  81. package/dist/oauth/discovery.d.ts +96 -0
  82. package/dist/oauth/discovery.js +163 -0
  83. package/dist/oauth/discovery.js.map +1 -0
  84. package/dist/oauth/lock.d.ts +100 -0
  85. package/dist/oauth/lock.js +287 -0
  86. package/dist/oauth/lock.js.map +1 -0
  87. package/dist/oauth/loopback.d.ts +85 -0
  88. package/dist/oauth/loopback.js +124 -0
  89. package/dist/oauth/loopback.js.map +1 -0
  90. package/dist/oauth/pkce.d.ts +58 -0
  91. package/dist/oauth/pkce.js +77 -0
  92. package/dist/oauth/pkce.js.map +1 -0
  93. package/dist/oauth/read.d.ts +120 -0
  94. package/dist/oauth/read.js +144 -0
  95. package/dist/oauth/read.js.map +1 -0
  96. package/dist/oauth/session.d.ts +80 -0
  97. package/dist/oauth/session.js +112 -0
  98. package/dist/oauth/session.js.map +1 -0
  99. package/dist/oauth/signin.d.ts +178 -0
  100. package/dist/oauth/signin.js +225 -0
  101. package/dist/oauth/signin.js.map +1 -0
  102. package/dist/oauth/token.d.ts +139 -0
  103. package/dist/oauth/token.js +183 -0
  104. package/dist/oauth/token.js.map +1 -0
  105. package/dist/paths.d.ts +366 -0
  106. package/dist/paths.js +460 -0
  107. package/dist/paths.js.map +1 -0
  108. package/dist/policy.d.ts +195 -0
  109. package/dist/policy.js +244 -0
  110. package/dist/policy.js.map +1 -0
  111. package/dist/post.d.ts +302 -0
  112. package/dist/post.js +384 -0
  113. package/dist/post.js.map +1 -0
  114. package/dist/project.d.ts +99 -0
  115. package/dist/project.js +192 -0
  116. package/dist/project.js.map +1 -0
  117. package/dist/redact.d.ts +175 -0
  118. package/dist/redact.js +700 -0
  119. package/dist/redact.js.map +1 -0
  120. package/dist/runtime.d.ts +30 -0
  121. package/dist/runtime.js +37 -0
  122. package/dist/runtime.js.map +1 -0
  123. package/dist/spawn_budget.d.ts +101 -0
  124. package/dist/spawn_budget.js +124 -0
  125. package/dist/spawn_budget.js.map +1 -0
  126. package/dist/spool.d.ts +390 -0
  127. package/dist/spool.js +556 -0
  128. package/dist/spool.js.map +1 -0
  129. package/dist/state.d.ts +109 -0
  130. package/dist/state.js +217 -0
  131. package/dist/state.js.map +1 -0
  132. package/dist/system_message.d.ts +101 -0
  133. package/dist/system_message.js +141 -0
  134. package/dist/system_message.js.map +1 -0
  135. package/dist/term.d.ts +112 -0
  136. package/dist/term.js +134 -0
  137. package/dist/term.js.map +1 -0
  138. package/dist/version.d.ts +16 -0
  139. package/dist/version.js +17 -0
  140. package/dist/version.js.map +1 -0
  141. package/package.json +50 -0
@@ -0,0 +1,192 @@
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
+ import { execFileSync } from "node:child_process";
39
+ import { existsSync, realpathSync } from "node:fs";
40
+ import { isAbsolute, join, resolve } from "node:path";
41
+ import { spawnTimeoutMs } from "./spawn_budget.js";
42
+ /** This spawn's own ceiling, used when no hook budget is armed. */
43
+ const TOPLEVEL_TIMEOUT_MS = 5_000;
44
+ /**
45
+ * One `rev-parse` answer, or null.
46
+ *
47
+ * ⛔ **ONE ARGUMENT PER INVOCATION, and `resolveProjectKeys` therefore spawns
48
+ * TWICE rather than once.** Prior learning `git-rev-parse-combined-args-fail-together`,
49
+ * reproduced 10/10: *when the unresolvable arg is 40 hex, `rev-parse` exits 0 and
50
+ * echoes it back verbatim as if it were an answer.* Reproduced again here on
51
+ * 2026-08-21 — `rev-parse --show-toplevel <40×a>` prints the toplevel, then
52
+ * `aaaa…`, and exits 0. A combined invocation therefore cannot be validated by
53
+ * WHICH LINE the answer landed on, because a garbage answer occupies a line too.
54
+ *
55
+ * ⚠ **The cost is +1 spawn at hook entry, outside the 8-file loop**, and it is
56
+ * accepted on the record (`D184 §6`). `hooks/post_commit.ts` arms no spawn
57
+ * budget, deliberately, so this path is unaffected there.
58
+ *
59
+ * `execFileSync` with an argument array, never a shell string: a repository path
60
+ * containing a space or a `;` is ordinary on a developer's machine and would be
61
+ * a command-injection sink through `execSync`.
62
+ */
63
+ function revParse(dir, arg) {
64
+ const timeout = spawnTimeoutMs(TOPLEVEL_TIMEOUT_MS);
65
+ if (timeout === null)
66
+ return null;
67
+ try {
68
+ const out = execFileSync("git", ["-C", dir, "rev-parse", arg], {
69
+ encoding: "utf8",
70
+ stdio: ["ignore", "pipe", "ignore"],
71
+ timeout,
72
+ });
73
+ const value = out.trim();
74
+ return value === "" ? null : value;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ /**
81
+ * The git toplevel for `dir`, or null when it is not inside a work tree.
82
+ *
83
+ * ⚠ **The timeout DERIVES from the hook's remaining budget** (`TODOS[87]`).
84
+ * `execFileSync` blocks the event loop, so the watchdog cannot fire while this
85
+ * call is outstanding — a fixed 5s ceiling here is 5s the watchdog provably
86
+ * cannot cover, and it is one of four such ceilings that summed to 20s against a
87
+ * 5s budget. `null` means the budget is spent, and the answer is the one this
88
+ * function already gives when git cannot tell us: no toplevel, so no consent,
89
+ * so the hook does nothing. Silent by contract and correct by default.
90
+ *
91
+ * ⛔ **The answer is validated by CONTENT: `--show-toplevel` is absolute, always.**
92
+ * Without that gate a `git` on `PATH` that echoed a 40-hex string back would have
93
+ * it `resolve`d against `process.cwd()` and returned as a toplevel — accepted
94
+ * because it arrived where an answer was expected. Fail closed instead.
95
+ */
96
+ export function gitToplevel(dir) {
97
+ const top = revParse(dir, "--show-toplevel");
98
+ if (top === null || !isAbsolute(top))
99
+ return null;
100
+ return resolve(top);
101
+ }
102
+ /**
103
+ * ⛔ **THE CONSENT KEY — the git common dir, canonicalised before it is ever a key.**
104
+ *
105
+ * ## `M3`: keying this naively is a SECURITY REGRESSION, not a formatting bug
106
+ *
107
+ * `--git-common-dir` is **`.git`, RELATIVE**, in a main clone, and an absolute
108
+ * path in a linked worktree. Measured 2026-08-21, four shapes:
109
+ *
110
+ * | shape | `--git-common-dir` |
111
+ * |---|---|
112
+ * | main clone, `-C` the toplevel | `.git` |
113
+ * | main clone, `-C` a subdirectory two deep | `../../.git` |
114
+ * | linked worktree | `<main>/.git`, absolute |
115
+ * | worktree of a BARE repo | `<repos>/project.git`, absolute |
116
+ *
117
+ * ⛔ Keyed on the raw string, **every main clone on the machine gets the key
118
+ * `.git`** — consent to one repo would grant capture on all of them.
119
+ *
120
+ * ⛔ **And the base for that resolution is `dir`, NOT the toplevel.** Row 2 is
121
+ * why, and it is the same hazard one layer over: `resolve(toplevel, "../../.git")`
122
+ * for a repo at `…/scratch/repo` is `…/.git` — a key SHARED by every sibling repo
123
+ * under that parent. Git's answer is relative to git's cwd, which `-C dir` makes
124
+ * `dir`. `D184 §2` says "resolve against the toplevel"; measured, that is wrong
125
+ * for the subdirectory case and this resolves against `dir`.
126
+ *
127
+ * ⚠ **`realpath` before it is a key**, so `/tmp/x` and `/private/tmp/x` are not
128
+ * two grants for one repo, and so an unresolvable answer fails closed.
129
+ *
130
+ * ## The content check, which is what makes this not "trust the line"
131
+ *
132
+ * A 40-hex string is legal syntax for a RELATIVE common dir, so absoluteness
133
+ * cannot reject it here the way it does for the toplevel. The answer must be a
134
+ * directory that actually holds a `HEAD` — present in all four measured shapes
135
+ * above, and absent from any directory that merely happens to exist under `dir`.
136
+ * ⚠ Existence alone would be a check the fixture can satisfy by accident
137
+ * (`project_redaction_fixture_must_exist_on_disk` is the same shape of trap).
138
+ */
139
+ function gitConsentKey(dir) {
140
+ const raw = revParse(dir, "--git-common-dir");
141
+ if (raw === null)
142
+ return null;
143
+ let canonical;
144
+ try {
145
+ canonical = realpathSync(resolve(dir, raw));
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ return existsSync(join(canonical, "HEAD")) ? canonical : null;
151
+ }
152
+ /**
153
+ * Both keys for a directory, or **null** — ⛔ **fail closed, never a fallback.**
154
+ *
155
+ * If either spawn fails or either answer fails its content check, there is no
156
+ * partial answer: a guessed consent key grants capture on a repository the user
157
+ * never approved, and a guessed worktree key files a session under the wrong
158
+ * repository permanently.
159
+ *
160
+ * ⚠ **NO MIGRATION PATH FOR EXISTING `projects.json` ENTRIES, deliberately.**
161
+ * The allow list is keyed by string and the key changes shape, so a pre-existing
162
+ * grant at a toplevel stops matching. Pre-launch there are zero users and zero
163
+ * rows (`project_pre_launch_no_data`), so the migration is a no-op with nothing
164
+ * to migrate — writing one would be speculative code guarding a case that cannot
165
+ * occur. The observable cost if that assumption were ever wrong is one re-run of
166
+ * `vibecommit connect`, not a silent grant.
167
+ */
168
+ export function resolveProjectKeys(dir) {
169
+ const worktree = gitToplevel(dir);
170
+ if (worktree === null)
171
+ return null;
172
+ const consent = gitConsentKey(dir);
173
+ if (consent === null)
174
+ return null;
175
+ return { worktree, consent };
176
+ }
177
+ /**
178
+ * The worktree toplevel for a directory: the session bucket and slug input.
179
+ *
180
+ * ⛔ **NO LONGER THE CONSENT KEY — `D184` moved that to
181
+ * `resolveProjectKeys(dir).consent`.** The name is kept because the three roles
182
+ * it still serves are unchanged and every caller of it wants the toplevel; a
183
+ * rename would be churn across files this change has no other reason to touch.
184
+ *
185
+ * Falls back to NOTHING. A directory outside a work tree has no project identity,
186
+ * and inventing one from `cwd` would file a session under a key the user cannot
187
+ * see in `vibecommit status`.
188
+ */
189
+ export function resolveProjectKey(dir) {
190
+ return gitToplevel(dir);
191
+ }
192
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEtD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,mEAAmE;AACnE,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAElC;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAS,QAAQ,CAAC,GAAW,EAAE,GAAW;IACxC,MAAM,OAAO,GAAG,cAAc,CAAC,mBAAmB,CAAC,CAAC;IACpD,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE;YAC7D,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;YACnC,OAAO;SACR,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QACzB,OAAO,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IAC7C,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAClD,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;AACtB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,SAAS,aAAa,CAAC,GAAW;IAChC,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;IAC9C,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,SAAiB,CAAC;IACtB,IAAI,CAAC;QACH,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;AAChE,CAAC;AAkBD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAC/B,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Out-of-tree redaction — `CR-024d`, D57 §OV8d.
3
+ *
4
+ * A Claude Code transcript records structured file reads and writes VERBATIM:
5
+ * a `Read`, `Edit` or `Write` record carries a path and the file's content. When
6
+ * that path is outside the repository the user consented to, the content of a
7
+ * THIRD PARTY'S file is about to be uploaded under the user's credential and
8
+ * attached to a repo that has nothing to do with it. That is a confidentiality
9
+ * boundary, not a tidiness issue, and it is the whole reason this module exists.
10
+ *
11
+ * ## Why the client, permanently
12
+ *
13
+ * D57 §OV8d: *"Out-of-tree redaction is client-side. Server-side redaction would
14
+ * break `turn_hash == sha256(content)`, so it cannot live on the server at
15
+ * all."* A server that rewrote content after hashing would hold a hash chain
16
+ * over bytes it no longer has. There is no server-side variant to fall back on.
17
+ *
18
+ * ## The deviation this is allowed to make, and its limit
19
+ *
20
+ * D60 §D6 says the client holds no ANALYSIS, and OV8d is the ONE recorded
21
+ * deviation from it, because redacting requires parsing each line. The rule that
22
+ * survives is *"the client may render; it may not analyze, hash, or
23
+ * canonicalize."* So this module parses a line, finds a path, compares it to the
24
+ * project root, and replaces a span. It extracts no turns, computes no digest,
25
+ * canonicalises no record, and imports nothing from `vibecommit-mcp` — that last
26
+ * one is a LICENSING violation, not a style one (D60 §D1a, D64).
27
+ *
28
+ * ## ⚠ LENGTH PRESERVATION IS THE LOAD-BEARING PROPERTY
29
+ *
30
+ * The client sends a raw byte span with `X-Byte-Offset: from`, and the server
31
+ * advances its watermark by the DECOMPRESSED body's length
32
+ * (`vibecommit-mcp/src/conversation/ingest_session.ts`). The client's own
33
+ * `sentOffset` advances to `to`. If a redaction makes the body SHORTER, the two
34
+ * watermarks diverge and every subsequent delta arrives flagged `gap: true`,
35
+ * forever; longer, and every delta is `overlap: true`. A permanently
36
+ * gap-flagged stream is a capture-health surface reporting data loss that is
37
+ * not happening.
38
+ *
39
+ * So every replacement is padded to the exact byte length it replaced, and a
40
+ * span too short to hold even the shortest honest marker is LEFT ALONE. That
41
+ * trade — leak a sub-13-byte span rather than break the accounting — is the
42
+ * brief's, stated rather than smuggled, and `redactSpan` counts what it skipped
43
+ * so the number is reportable.
44
+ *
45
+ * ## Why re-serialising is safe here, measured rather than assumed
46
+ *
47
+ * `JSON.parse` → `JSON.stringify` was byte-identical on **39,910 of 39,910**
48
+ * real transcript lines across 25 sessions, because the transcript is written by
49
+ * `JSON.stringify` in the first place. That is what makes the padding
50
+ * arithmetic exact instead of approximate. It is not taken on faith: every line
51
+ * is round-tripped and compared BEFORE anything is changed, and a line that does
52
+ * not round-trip is returned untouched.
53
+ *
54
+ * @provenance vibecommit-mcp src/conversation/ingest_session.ts — offset accounting, read
55
+ */
56
+ import { Buffer } from "node:buffer";
57
+ import type { RedactionProfile } from "./agents/types.js";
58
+ /**
59
+ * The DATA one profile supplies. **The algorithm above does not move.**
60
+ *
61
+ * `CR-185`, D164 §5. Everything below `walk` is the rule this module already
62
+ * had — *within one object, a path key naming a file outside the project root
63
+ * makes that object's content-bearing siblings third-party content, and the
64
+ * verdict inherits down the subtree* — and each agent supplies which keys play
65
+ * which part. A second agent is a second row in the table below, not a branch.
66
+ */
67
+ export interface RedactionRules {
68
+ /** Keys whose STRING value is a filesystem path. The SELECTOR, never redacted. */
69
+ readonly pathKeys: ReadonlySet<string>;
70
+ /** Keys whose value is content, withheld when a path in scope is out of tree. */
71
+ readonly contentKeys: ReadonlySet<string>;
72
+ /**
73
+ * Keys whose STRING value is itself a JSON document to descend into.
74
+ *
75
+ * ⚠ **Empty for Claude Code, and that is not an oversight** — a Claude record
76
+ * carries its tool input as a real nested OBJECT, so `walk` already reaches
77
+ * it. Codex hands the whole argument list over as one escaped string.
78
+ */
79
+ readonly nestedJsonKeys: ReadonlySet<string>;
80
+ /** Keys holding `[{ lines: [...] }]` hunk arrays whose lines are content. */
81
+ readonly patchKeys: ReadonlySet<string>;
82
+ }
83
+ /**
84
+ * The rules a profile LABEL names.
85
+ *
86
+ * For a caller that already knows the dialect — `test/` does, and nothing in
87
+ * `src/` should, because ⛔ **the profile is selected by the containing root and
88
+ * not by anything the caller believes about the agent** (see
89
+ * `rulesForTranscript`).
90
+ */
91
+ export declare function rulesFor(profile: RedactionProfile): RedactionRules;
92
+ /**
93
+ * ⛔ **THE PROFILE IS SELECTED BY WHICH REGISTRY ROOT CONTAINS THE RESOLVED
94
+ * TRANSCRIPT PATH — NEVER BY THE `--agent=` FLAG (D164 §5).**
95
+ *
96
+ * The flag stays the BEHAVIOUR selector (roots to search, events, budgets,
97
+ * delegated discovery). The scrubber stops depending on anyone remembering to
98
+ * set it — which is the whole defect: both no-flag install paths (the
99
+ * `.claude-plugin` bundle whose command carries no `--agent=`, and a
100
+ * hand-written `.codex/` config) resolved to `claude-code`, and a Codex record
101
+ * went through Claude's key set and was redacted NOT AT ALL.
102
+ *
103
+ * ## ⛔ This does NOT reopen `CR-183`'s `transcript_path`-as-selector rejection
104
+ *
105
+ * That rejection is about the **confinement boundary**, which is computed from
106
+ * the registry and is never derived from input — and it still is. The
107
+ * difference is the direction of the failure:
108
+ *
109
+ * - choosing a **stricter scrubber** because a path sits outside every known
110
+ * root is **fail-closed**;
111
+ * - choosing a **wider root** because a path *claims* to be there is not.
112
+ *
113
+ * A wrong answer here costs a suboptimal profile and can never make a file
114
+ * readable that was not readable before. `isInside` resolves both sides with
115
+ * `realpathSync`, so a symlink cannot claim a root it is not under.
116
+ *
117
+ * ⛔ **AMBIGUITY FAILS CLOSED TOO.** Two dialects can be pointed at one
118
+ * directory by environment (`CLAUDE_CONFIG_DIR` and `CODEX_HOME` are both
119
+ * operator-set, and both sides resolve through `realpath`), and a path inside
120
+ * two roots is a path whose producer is not known. Exactly one match selects a
121
+ * profile; zero or two select the strictest.
122
+ *
123
+ * ⚠ **Today no path reaching this function is unattributed**, because
124
+ * `isInsideAny(transcriptRoots(...))` refuses everything outside the union
125
+ * BEFORE `readSpan` opens it. So the fallback is a second, independent line
126
+ * rather than a live branch — stated plainly rather than left for a reader to
127
+ * assume it has a caller (`feedback_unexercisable_branch_not_verified`).
128
+ */
129
+ export declare function rulesForTranscript(home: string, env: NodeJS.ProcessEnv, transcriptPath: string): RedactionRules;
130
+ export interface RedactionCounts {
131
+ /** String values replaced with a marker. */
132
+ readonly redacted: number;
133
+ /** Out-of-tree values left alone because no honest marker fits. */
134
+ readonly skipped: number;
135
+ }
136
+ export interface RedactedLine extends RedactionCounts {
137
+ readonly line: string;
138
+ }
139
+ /**
140
+ * Redact one NDJSON line. **Pure, total, and length-preserving.**
141
+ *
142
+ * Returns the line UNCHANGED on anything it cannot handle — a blank line, a
143
+ * parse failure, a line that does not round-trip. A hook cannot report an error
144
+ * (DESIGN.md §13.7), and a redactor that dropped a line it could not parse would
145
+ * be silent data loss on a path where D56 §D8 makes loss permanent.
146
+ */
147
+ export declare function redactLine(line: string, projectRoots: readonly string[], rules: RedactionRules, resolver?: Containment): RedactedLine;
148
+ /**
149
+ * Redact every complete line in a raw transcript span.
150
+ *
151
+ * Operates on BYTES because a span is a byte range and its edges need not fall
152
+ * on a line — or even on a character. Segments are split on `\n` and a segment
153
+ * that does not decode to exactly its own bytes is passed through untouched,
154
+ * which is what keeps a span that bisects a multi-byte character from changing
155
+ * length when it is re-encoded.
156
+ */
157
+ export declare function redactSpan(span: Buffer, projectRoots: readonly string[], rules: RedactionRules): {
158
+ bytes: Buffer;
159
+ } & RedactionCounts;
160
+ /**
161
+ * A marker whose JSON-ENCODED byte length is exactly the original's.
162
+ *
163
+ * Encoded, not raw: what has to match is the bytes on the wire, and
164
+ * `JSON.stringify` escapes newlines, quotes and control characters, so a
165
+ * 400-byte source file is rarely 400 bytes of JSON. The marker is pure ASCII
166
+ * with nothing `JSON.stringify` escapes, so its encoded length is simply its
167
+ * own length plus the two quotes — which is what makes the padding exact.
168
+ *
169
+ * Null when even `[out-of-tree]` does not fit. The caller counts that and leaves
170
+ * the value alone: a short span left intact is a small leak, and a mis-sized one
171
+ * is a permanently gap-flagged stream.
172
+ */
173
+ export declare function markerFor(original: string): string | null;
174
+ type Containment = (roots: readonly string[], candidate: string) => boolean;
175
+ export {};