@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/exit.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The exit-code contract — CR-022.
3
+ *
4
+ * The contract is SPLIT, and the split is load-bearing (D61 round 3 §D8). As
5
+ * originally written the exit-0/stdout-empty rule governed the whole binary,
6
+ * which would have made `why` on a read-lane 5xx exit 0 and print nothing —
7
+ * a read verb failing silently.
8
+ *
9
+ * - HOOK invocations: always exit 0, always silent on stdout. Capture must never
10
+ * block or slow a developer's turn (D56 plan §D8, a founder override). A hook
11
+ * that exits non-zero or writes to stdout is a defect regardless of what went
12
+ * wrong underneath.
13
+ * - INTERACTIVE verbs (`connect`, `status`, `off`, `why`, `report`): normal CLI
14
+ * semantics. Non-zero on failure, diagnostics on stderr, output on stdout.
15
+ */
16
+ export declare const EXIT: {
17
+ readonly ok: 0;
18
+ /** Interactive-only. A hook must never return this. */
19
+ readonly failure: 1;
20
+ /**
21
+ * Interactive-only. Usage error — unknown verb, bad flag, **or consent
22
+ * requested on a non-TTY** (DESIGN.md §13.7, D65 plan §DR7). A consent prompt
23
+ * that proceeds when nobody can answer it is not a consent prompt.
24
+ */
25
+ readonly usage: 2;
26
+ /**
27
+ * Interactive-only. Not connected — distinct from `failure` so a wrapper can
28
+ * branch on it without parsing copy (DESIGN.md §13.7).
29
+ */
30
+ readonly notConnected: 3;
31
+ /**
32
+ * Interactive-only. Connected, nothing to report — the empty window.
33
+ * **Not an error** (DESIGN.md §13.7, and it is load-bearing): collapsing an
34
+ * empty `report` window or a pre-install `why` into `1` teaches a wrapper that
35
+ * absence of a record means the tool broke — one `||` away from the
36
+ * permanently BLOCKED claim that absence of an edge means no agent was
37
+ * involved.
38
+ */
39
+ readonly empty: 4;
40
+ };
41
+ export type ExitCode = (typeof EXIT)[keyof typeof EXIT];
42
+ export type InvocationMode = "hook" | "interactive";
43
+ /**
44
+ * Resolve the exit code for a completed invocation.
45
+ * Hooks collapse every outcome to 0 by contract; interactive verbs pass through.
46
+ */
47
+ export declare function resolveExitCode(mode: InvocationMode, outcome: number): number;
48
+ /**
49
+ * The hook-side half of the contract, stated as data so the contract test can
50
+ * enumerate it rather than restate it (DESIGN.md §13.7).
51
+ *
52
+ * `stdout` is "empty on the happy path, otherwise exactly one well-formed JSON
53
+ * object carrying `systemMessage`. If JSON construction fails for ANY reason →
54
+ * empty stdout. The fallback is always silence."
55
+ */
56
+ export declare const HOOK_CONTRACT: {
57
+ readonly exit: 0;
58
+ readonly stderrBytes: 0;
59
+ };
package/dist/exit.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The exit-code contract — CR-022.
3
+ *
4
+ * The contract is SPLIT, and the split is load-bearing (D61 round 3 §D8). As
5
+ * originally written the exit-0/stdout-empty rule governed the whole binary,
6
+ * which would have made `why` on a read-lane 5xx exit 0 and print nothing —
7
+ * a read verb failing silently.
8
+ *
9
+ * - HOOK invocations: always exit 0, always silent on stdout. Capture must never
10
+ * block or slow a developer's turn (D56 plan §D8, a founder override). A hook
11
+ * that exits non-zero or writes to stdout is a defect regardless of what went
12
+ * wrong underneath.
13
+ * - INTERACTIVE verbs (`connect`, `status`, `off`, `why`, `report`): normal CLI
14
+ * semantics. Non-zero on failure, diagnostics on stderr, output on stdout.
15
+ */
16
+ export const EXIT = {
17
+ ok: 0,
18
+ /** Interactive-only. A hook must never return this. */
19
+ failure: 1,
20
+ /**
21
+ * Interactive-only. Usage error — unknown verb, bad flag, **or consent
22
+ * requested on a non-TTY** (DESIGN.md §13.7, D65 plan §DR7). A consent prompt
23
+ * that proceeds when nobody can answer it is not a consent prompt.
24
+ */
25
+ usage: 2,
26
+ /**
27
+ * Interactive-only. Not connected — distinct from `failure` so a wrapper can
28
+ * branch on it without parsing copy (DESIGN.md §13.7).
29
+ */
30
+ notConnected: 3,
31
+ /**
32
+ * Interactive-only. Connected, nothing to report — the empty window.
33
+ * **Not an error** (DESIGN.md §13.7, and it is load-bearing): collapsing an
34
+ * empty `report` window or a pre-install `why` into `1` teaches a wrapper that
35
+ * absence of a record means the tool broke — one `||` away from the
36
+ * permanently BLOCKED claim that absence of an edge means no agent was
37
+ * involved.
38
+ */
39
+ empty: 4,
40
+ };
41
+ /**
42
+ * Resolve the exit code for a completed invocation.
43
+ * Hooks collapse every outcome to 0 by contract; interactive verbs pass through.
44
+ */
45
+ export function resolveExitCode(mode, outcome) {
46
+ return mode === "hook" ? EXIT.ok : outcome;
47
+ }
48
+ /**
49
+ * The hook-side half of the contract, stated as data so the contract test can
50
+ * enumerate it rather than restate it (DESIGN.md §13.7).
51
+ *
52
+ * `stdout` is "empty on the happy path, otherwise exactly one well-formed JSON
53
+ * object carrying `systemMessage`. If JSON construction fails for ANY reason →
54
+ * empty stdout. The fallback is always silence."
55
+ */
56
+ export const HOOK_CONTRACT = {
57
+ exit: EXIT.ok,
58
+ stderrBytes: 0,
59
+ };
60
+ //# sourceMappingURL=exit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exit.js","sourceRoot":"","sources":["../src/exit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,EAAE,EAAE,CAAC;IACL,uDAAuD;IACvD,OAAO,EAAE,CAAC;IACV;;;;OAIG;IACH,KAAK,EAAE,CAAC;IACR;;;OAGG;IACH,YAAY,EAAE,CAAC;IACf;;;;;;;OAOG;IACH,KAAK,EAAE,CAAC;CACA,CAAC;AAMX;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAoB,EAAE,OAAe;IACnE,OAAO,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;AAC7C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,IAAI,EAAE,IAAI,CAAC,EAAE;IACb,WAAW,EAAE,CAAC;CACN,CAAC"}
package/dist/git.d.ts ADDED
@@ -0,0 +1,299 @@
1
+ /**
2
+ * What "this repository" means ON THE WIRE — `CR-019d`, the client half of
3
+ * `CR-019`.
4
+ *
5
+ * ⚠ **This is not `project.ts`'s question and the two answers are not
6
+ * interchangeable.** `resolveProjectKey()` answers "which work tree is this?"
7
+ * with a local filesystem path, and that path is (since `CR-017d`) half the
8
+ * local state key — it never leaves the machine. `resolveRepoSlug()` answers
9
+ * "which repository is this, in terms the SERVER can resolve?" and its value
10
+ * goes on the wire as `X-Repo-Slug`. Conflating them would put an absolute path
11
+ * from a developer's laptop into a tenant-scoped lookup, so they stay separate
12
+ * functions with separate return shapes.
13
+ *
14
+ * ⛔ **THIS PARAGRAPH USED TO CALL THAT PATH "THE CONSENT KEY" TOO, AND `D184`
15
+ * MADE THAT FALSE.** The consent gate now keys on the canonicalised git COMMON
16
+ * DIR — `resolveProjectKeys(dir).consent` — and `resolveProjectKey`'s own
17
+ * docblock says so in terms (`project.ts:197`). Corrected here rather than left
18
+ * standing, because this is the paragraph a reader meets first and it was
19
+ * teaching the retired model.
20
+ *
21
+ * ## The contract this must satisfy, read from the merged server, not inferred
22
+ *
23
+ * `CR-019b` shipped and `X-Repo-Slug` is REQUIRED. `parseIngestHeaders`
24
+ * (`vibecommit-mcp/src/conversation/ingest_session.ts`) rejects an absent header
25
+ * — and a present one that is empty after `.trim()` — with a 400 whose reason is
26
+ * `missing X-Repo-Slug`, and rejects anything over 256 characters. So there is
27
+ * no lenient path to fall back to: this module always returns a non-empty slug
28
+ * for a directory that has a git toplevel at all.
29
+ *
30
+ * D57 §OV1 is why the server is strict: *"A NULL `repository_id` is
31
+ * fail-closed-invisible per `org_access_model.sql`, so without this every hook
32
+ * capture would be hidden from its own author."* `readable_repository_ids` is a
33
+ * POSITIVE list, so a capture with no repository renders to nobody — including
34
+ * the person who just made it. A 400 is strictly better than that silence.
35
+ *
36
+ * ## The two keys have different GRAIN, and that is correct in both directions
37
+ *
38
+ * This is where the local and wire identities line up, so it is where the
39
+ * mismatch between them belongs on the record. A linked `git worktree` has its
40
+ * own toplevel — verified, `rev-parse --show-toplevel` returns the worktree
41
+ * directory, not the main clone — so two worktrees of one repository are two
42
+ * `repoKey`s and therefore two local offset ledgers, while sharing one `origin`
43
+ * makes them one `repoSlug` and one server `repository_id`.
44
+ *
45
+ * Both halves of that are the behaviour you want. Locally, separate ledgers are
46
+ * the safe direction: the state key exists to keep two work trees from applying
47
+ * each other's byte offsets (`CR-017d`, D58), and over-separating costs a resync
48
+ * at worst. On the wire, one identity is simply true — two worktrees of one
49
+ * repository ARE one repository, and minting two would fragment the audit record
50
+ * of a single repo.
51
+ *
52
+ * ⛔ **THE SPLIT IS NOW THE STATE BUCKET'S ALONE, AND IT USED TO BE EVERYTHING'S
53
+ * (`D184`).** Read the paragraph above as being about the OFFSET LEDGER, because
54
+ * that is all it still governs. Consent no longer follows the toplevel: it keys
55
+ * on the git common dir, which a main clone and every worktree linked to it
56
+ * SHARE. So the two statements a reader is likely to run together are now
57
+ * opposites — two worktrees are **two** state buckets and **one** consent
58
+ * grant — and the `CR-017d`/D58 reasoning above is kept verbatim because it is
59
+ * still exactly right about the half it was written for.
60
+ *
61
+ * ⚠ **The reason they moved apart is the same fact seen from two sides.** A
62
+ * per-worktree consent key made an agent session inside a `git worktree` capture
63
+ * NOTHING, silently, while `D19` makes worktree-per-builder the house convention
64
+ * — so sharing was the fix. A per-worktree state key is what stops two work
65
+ * trees applying each other's byte offsets — so separating is the fix. One
66
+ * value could not do both, and `project.ts` is where the two are produced
67
+ * together precisely so taking the wrong one has to be deliberate.
68
+ *
69
+ * The one reachable oddity: two worktrees driven by ONE Claude Code session (so,
70
+ * one `session_id`) would keep two independent client ledgers against a single
71
+ * server stream key. The server is idempotent on `(session, offset)` and treats
72
+ * a disagreement as a resync, so the cost is re-sent bytes, never a cross-repo
73
+ * bleed. Not designed around; recorded so the next person meets it as a known
74
+ * property rather than a surprise.
75
+ *
76
+ * ## Case, and why this does not lowercase
77
+ *
78
+ * `resolve_repository` canonicalises with `lower(trim(coalesce(slug, '')))` and
79
+ * the App-installation oracle compares `lower(r.repo_full_name) = v_slug`, so
80
+ * both sides are lowercased and case cannot affect resolution. What case DOES
81
+ * affect is the row the server writes: `display_name` is
82
+ * `trim(p_source_repo_full_name)` — the raw value sent here. Preserving the
83
+ * remote's own case therefore gives a correctly-cased display name for free,
84
+ * and lowercasing would throw that away for no gain.
85
+ *
86
+ * @provenance vibecommit-mcp src/conversation/ingest_session.ts — parseIngestHeaders, read
87
+ * @provenance vibecommit-schema org_access_model.sql — readable_repository_ids, read
88
+ */
89
+ /**
90
+ * `owner/repo` for a work tree whose `origin` is GitHub, `host/owner/repo` for
91
+ * any other host, else `local:<sha256(abspath)[0:12]>`.
92
+ *
93
+ * **Takes the TOPLEVEL, not a `cwd`.** Every caller already has it: the consent
94
+ * gate resolves `resolveProjectKey(input.cwd)` before anything else runs and
95
+ * refuses when it is null, so re-deriving it here would be a second
96
+ * `git rev-parse` spent against the hook's wall-clock budget (DESIGN.md §13.7)
97
+ * to recompute a value the caller is holding — and a second call could disagree
98
+ * with the first, which is the same reason `CR-017d` reuses it for the state key
99
+ * rather than recomputing.
100
+ *
101
+ * **Never returns empty.** A work tree always has an identity: with a remote it
102
+ * is the remote's, without one it is the path's. That is what lets the server
103
+ * require the header (D58 §ER25).
104
+ *
105
+ * ## ⚠ Why `local:` had to stop being the answer for a real remote (D157)
106
+ *
107
+ * `localSlug` hashes the developer's OWN ABSOLUTE PATH. That is correct for a
108
+ * work tree with no remote — there is nothing else to key on, and two such trees
109
+ * genuinely are two repositories. It is WRONG for a tree that has a remote:
110
+ * every colleague, and every second clone on one machine, mints a DIFFERENT
111
+ * repository for what is provably one repo. Captures never join, the graph
112
+ * cannot span them, and **nothing errors** — the failure is silent by
113
+ * construction, which is what made it survive this long.
114
+ *
115
+ * So `local:` now survives only where it is true: **no remote at all**, or a
116
+ * remote whose path is not `owner/repo` and therefore cannot be named without
117
+ * guessing (`parseRemoteSlug` refuses rather than guesses — `CR-019d`).
118
+ */
119
+ export declare function resolveRepoSlug(toplevel: string): string;
120
+ /**
121
+ * `origin`'s URL, or null when there is no `origin` (or git cannot say).
122
+ *
123
+ * `execFileSync` with an argument array, never a shell string — a repository
124
+ * path containing a space or a `;` is ordinary on a developer's machine and
125
+ * would be a command-injection sink through `execSync`. Same discipline as
126
+ * `project.ts`'s `gitToplevel`, for the same reason.
127
+ */
128
+ export declare function gitRemoteUrl(dir: string): string | null;
129
+ /**
130
+ * Run one git command, or return null — the shared probe.
131
+ *
132
+ * ⚠ **AN ARGUMENT ARRAY, NEVER A SHELL STRING.** A repository path containing a
133
+ * space or a `;` is ordinary on a developer's machine and a shell string is a
134
+ * command-injection sink. Identical shape to `gitRemoteUrl` above and
135
+ * `project.ts`'s `gitToplevel`: `-C <dir>`, utf8, stderr discarded, a
136
+ * five-second bound, and a failure that resolves to a VALUE rather than
137
+ * throwing.
138
+ *
139
+ * ⚠ **MOVED HERE FROM `src/commands/why.ts` BY `CR-108`**, because `report`
140
+ * needs the same probe and a second copy is how the two drift. `why`'s screens
141
+ * are byte-identical afterwards — `test/why.test.ts` is the oracle for that,
142
+ * and the move changes no behaviour: this is the same function.
143
+ */
144
+ export declare function gitProbe(dir: string, args: readonly string[]): string | null;
145
+ /**
146
+ * Is this a shallow clone?
147
+ *
148
+ * ⚠ **LOAD-BEARING RATHER THAN DEFENSIVE** (D67's posture), and it is the same
149
+ * hazard for two different verbs. `git blame` on a shallow clone attributes
150
+ * every line older than the graft boundary to the boundary commit; a
151
+ * REACHABILITY question on a shallow clone is wrong the same way, because the
152
+ * commits that would answer it may simply not be in the clone. Both return a
153
+ * PLAUSIBLE WRONG ANSWER — D98's hazard class — and shallow clones live in CI
154
+ * containers, which is where nobody is reading the output.
155
+ *
156
+ * ⚠ **FAILS CLOSED.** If git cannot answer, we do not know the clone is
157
+ * complete, and an answer we cannot stand behind is worse than a refusal.
158
+ */
159
+ export declare function isShallowClone(dir: string): boolean;
160
+ /**
161
+ * Is `sha` an ancestor of `ref` — i.e. still reachable from it?
162
+ *
163
+ * ⚠ **THE ONE THING THE SERVER CANNOT ANSWER.** `commit_coverage` returns the
164
+ * shas we hold an edge for and deliberately no rate, because reachability is a
165
+ * property of the user's clone and that process has no repository (D67). This
166
+ * is the client half of that split.
167
+ *
168
+ * ⚠ **FAILS CLOSED, and the direction matters.** `merge-base --is-ancestor`
169
+ * exits 0 for yes and 1 for no, so `gitProbe` returns `""` for reachable and
170
+ * `null` for BOTH "not reachable" and "git could not answer". Those are
171
+ * different facts, and collapsing them would silently count an unanswerable
172
+ * probe as unreachable — deflating the rate rather than refusing it. So the
173
+ * caller is handed `null` for "unknown" and must decide, and `report` refuses.
174
+ */
175
+ export declare function isReachable(dir: string, sha: string, ref: string): boolean | null;
176
+ /**
177
+ * The `git patch-id` of one commit — the CLIENT-ASSERTED half of a successor
178
+ * pair (`CR-133d`, D111 §1).
179
+ *
180
+ * ⚠ **THE CLIENT IS THE ONLY SIDE THAT CAN COMPUTE THIS.** The server provably
181
+ * cannot: no git dependency, no clone, and its only `child_process` is a
182
+ * best-effort `git show` on no live path (D108). A patch id is a fact about a
183
+ * DIFF, so it survives the rewrite that changes the sha — which is what lets a
184
+ * successor pair resolve through a rebase or GitHub's squash-merge button.
185
+ *
186
+ * ⚠ **IT IS EVIDENCE THE CLIENT ASSERTS, NOT SOMETHING WE WITNESSED** (D111 §1
187
+ * condition 3, the same posture as `cli_read` in D73). Whatever consumes this
188
+ * must record it as client-asserted; nothing here establishes that the caller's
189
+ * repository is the one the server thinks it is.
190
+ *
191
+ * ⚠ **NULL RATHER THAN A GUESS, AND THE REASON IS PERMANENCE.** `match_kind` is
192
+ * in the primary key and `reject_history_mutation` raises on every UPDATE
193
+ * including `service_role` (D105, D108), so **a wrong row can never be
194
+ * retracted**. That makes guessing not a degraded-but-acceptable mode. Every
195
+ * unanswerable case below therefore returns `null`, exactly like `isReachable`
196
+ * beside it, and the caller must be able to express "no patch id" as a state
197
+ * rather than as a value.
198
+ *
199
+ * MEASURED cases that return `null` — none of them is an error:
200
+ * - a MERGE commit — `diff-tree` emits no patch for one, and a merge has no
201
+ * single diff to identify;
202
+ * - an EMPTY commit — nothing to hash;
203
+ * - a sha absent from this clone, which is the ordinary SHALLOW-clone case
204
+ * (`isShallowClone` above exists for the same reason);
205
+ * - git missing, a timeout, or output that is not a hex id.
206
+ *
207
+ * ⚠ `--root` is deliberate: without it the INITIAL commit of a repository has
208
+ * no patch id at all, which would silently exclude the one commit every
209
+ * repository has.
210
+ *
211
+ * ⚠ `--no-ext-diff` is load-bearing, not tidiness: a developer's configured
212
+ * external differ would otherwise rewrite the bytes being hashed, and the
213
+ * resulting id would be stable only on that machine.
214
+ *
215
+ * ⚠ **NO SHELL, so no pipe.** `git patch-id` normally reads a pipe; feeding it
216
+ * through `input` keeps the argument-array rule `gitProbe` documents above — a
217
+ * repository path containing a space or a `;` is ordinary.
218
+ */
219
+ export declare function gitPatchId(dir: string, ref: string): string | null;
220
+ /** Where `HEAD` points right now — the ref-move observation (`CR-170`, D154). */
221
+ export interface HeadRef {
222
+ /** Validated 40- or 64-hex. Never abbreviated: the server's join is width-sensitive. */
223
+ readonly sha: string;
224
+ /** Null on a detached HEAD, which is a state and not a failure. */
225
+ readonly branch: string | null;
226
+ }
227
+ /**
228
+ * `HEAD`'s sha and branch — **the ONE new spawn `CR-170` adds to the Claude Code
229
+ * hook path**, and it is for REF MOVES, not for commits.
230
+ *
231
+ * Commits are observed by `post-commit` (D154), which cannot see a `pull`, a
232
+ * `checkout` or a `reset --hard` — those move a ref **without creating a
233
+ * commit**, so no commit hook fires and only a direct look at `HEAD` records it.
234
+ *
235
+ * ⛔ **ONE SPAWN, BUT NOT COMBINED WITH `--show-toplevel`.** MEASURED
236
+ * 2026-08-17: `git rev-parse --show-toplevel nosuchref` prints the toplevel and
237
+ * THEN exits **128**, so `execFileSync` throws and the toplevel is lost with it —
238
+ * and on a repository with no commits `rev-parse HEAD` is exactly that case
239
+ * (measured: exit 128, `HEAD` on stdout, a usage line on stderr). Folding the two
240
+ * together would make an empty repo lose its project key, which is the `C3`
241
+ * regression. The toplevel keeps its own spawn; this is the second.
242
+ *
243
+ * ⛔ **THE OUTPUT IS VALIDATED, NEVER TRUSTED.** MEASURED, same day: when the rev
244
+ * argument is itself 40 hex, `rev-parse` exits **0 and ECHOES IT BACK** as though
245
+ * it had resolved something. Nothing distinguishes that from a real answer except
246
+ * checking the shape — the same reason `gitPatchId` tests its own return before
247
+ * believing it. ⚠ Marked as an inference: the echo-back is not reachable from
248
+ * THIS caller, which passes the literal `HEAD`, so the test below is defence
249
+ * against a future caller and against a git we do not understand, not a live path.
250
+ */
251
+ export declare function headRef(dir: string): HeadRef | null;
252
+ /** The branch `HEAD` is on, or null on a detached HEAD or a failure. */
253
+ export declare function currentBranch(dir: string): string | null;
254
+ /**
255
+ * `owner/repo` from a github.com remote, `host/owner/repo` from any other host,
256
+ * or null when the URL cannot be named without guessing.
257
+ *
258
+ * ⚠ **RENAMED FROM `parseGitHubSlug` BY `CR-167`**, because it stopped being
259
+ * about GitHub. A function that answers `gitlab.com/owner/repo` while calling
260
+ * itself `parseGitHubSlug` is the kind of name the next reader trusts and should
261
+ * not. Nothing outside this module and its test imported the old name.
262
+ *
263
+ * ## ⚠ SEGMENT COUNT IS THE DISCRIMINATOR, AND THE SERVER ALREADY PARSES IT
264
+ *
265
+ * `cr033a` §4 branches on the slash count: exactly one slash means the host is
266
+ * IMPLIED to be github.com and segment 1 is the owner; otherwise segment 1 is
267
+ * the host and segment 2 is the owner. This function emits into that grammar
268
+ * rather than inventing one.
269
+ *
270
+ * ⛔ **github.com therefore stays UNQUALIFIED at two segments.**
271
+ * `resolve_repository` matches the App-installation oracle on
272
+ * `github_installation_repos.repo_full_name`, which is `owner/repo`. Qualifying
273
+ * github.com would silently break every oracle join — the exact failure class
274
+ * this change exists to fix, re-introduced from the other side.
275
+ *
276
+ * ## Null is still the safe answer, and that asymmetry is still the design
277
+ *
278
+ * A wrong slug is worse than no slug. `local:` is designed to be safe: it can
279
+ * never collide with a real repository (neither a two- nor a three-segment slug
280
+ * can contain a `:`), and `resolve_repository` resolves it ungated because a
281
+ * local-only work tree has no external owner to check against. A
282
+ * confidently-wrong slug, by contrast, attributes a capture to a repository the
283
+ * user may not own. So every branch below still fails to null rather than
284
+ * guessing, and callers turn null into `local:`.
285
+ *
286
+ * ⚠ **HOST-QUALIFYING DOES NOT WEAKEN THE SUFFIX-ATTACK GUARD, IT RELOCATES IT.**
287
+ * `github.com.evil.example` no longer returns null — it returns
288
+ * `github.com.evil.example/owner/repo`. The security property was never "returns
289
+ * null"; it is **"is never mistaken for a github.com slug"**, and a three-segment
290
+ * slug provably cannot collide with the two-segment github.com namespace. The
291
+ * server agrees independently: `cr033a` §4 reads segment 1 as the HOST, so that
292
+ * slug is scoped to a namespace claim on `github.com.evil.example`, never to
293
+ * `owner`'s GitHub account.
294
+ *
295
+ * Exported for its own tests: the parsing is where the confidence lives, and it
296
+ * is pure apart from one `ssh -G`, so it is tested directly rather than through
297
+ * a spawned `git`.
298
+ */
299
+ export declare function parseRemoteSlug(remoteUrl: string): string | null;