@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.
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/agents/claude_code.d.ts +11 -0
- package/dist/agents/claude_code.js +162 -0
- package/dist/agents/claude_code.js.map +1 -0
- package/dist/agents/codex_cli.d.ts +15 -0
- package/dist/agents/codex_cli.js +67 -0
- package/dist/agents/codex_cli.js.map +1 -0
- package/dist/agents/cursor.d.ts +21 -0
- package/dist/agents/cursor.js +89 -0
- package/dist/agents/cursor.js.map +1 -0
- package/dist/agents/registry.d.ts +184 -0
- package/dist/agents/registry.js +212 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/agents/types.d.ts +279 -0
- package/dist/agents/types.js +67 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/commands/connect.d.ts +36 -0
- package/dist/commands/connect.js +499 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/context.d.ts +45 -0
- package/dist/commands/context.js +14 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/report.d.ts +105 -0
- package/dist/commands/report.js +430 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/status.d.ts +29 -0
- package/dist/commands/status.js +236 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/why.d.ts +349 -0
- package/dist/commands/why.js +937 -0
- package/dist/commands/why.js.map +1 -0
- package/dist/consent.d.ts +59 -0
- package/dist/consent.js +111 -0
- package/dist/consent.js.map +1 -0
- package/dist/copy/claims.d.ts +79 -0
- package/dist/copy/claims.js +343 -0
- package/dist/copy/claims.js.map +1 -0
- package/dist/copy/grades.d.ts +117 -0
- package/dist/copy/grades.js +96 -0
- package/dist/copy/grades.js.map +1 -0
- package/dist/copy/index.d.ts +12 -0
- package/dist/copy/index.js +13 -0
- package/dist/copy/index.js.map +1 -0
- package/dist/copy/strings.d.ts +1328 -0
- package/dist/copy/strings.js +1243 -0
- package/dist/copy/strings.js.map +1 -0
- package/dist/copy/time.d.ts +27 -0
- package/dist/copy/time.js +43 -0
- package/dist/copy/time.js.map +1 -0
- package/dist/credential.d.ts +211 -0
- package/dist/credential.js +298 -0
- package/dist/credential.js.map +1 -0
- package/dist/exit.d.ts +59 -0
- package/dist/exit.js +60 -0
- package/dist/exit.js.map +1 -0
- package/dist/git.d.ts +299 -0
- package/dist/git.js +653 -0
- package/dist/git.js.map +1 -0
- package/dist/hooks/entry.d.ts +277 -0
- package/dist/hooks/entry.js +1092 -0
- package/dist/hooks/entry.js.map +1 -0
- package/dist/hooks/post_commit.d.ts +59 -0
- package/dist/hooks/post_commit.js +160 -0
- package/dist/hooks/post_commit.js.map +1 -0
- package/dist/hooks/post_rewrite.d.ts +59 -0
- package/dist/hooks/post_rewrite.js +161 -0
- package/dist/hooks/post_rewrite.js.map +1 -0
- package/dist/hooks/session_end.d.ts +101 -0
- package/dist/hooks/session_end.js +120 -0
- package/dist/hooks/session_end.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +360 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +153 -0
- package/dist/install.js +284 -0
- package/dist/install.js.map +1 -0
- package/dist/json.d.ts +100 -0
- package/dist/json.js +94 -0
- package/dist/json.js.map +1 -0
- package/dist/oauth/discovery.d.ts +96 -0
- package/dist/oauth/discovery.js +163 -0
- package/dist/oauth/discovery.js.map +1 -0
- package/dist/oauth/lock.d.ts +100 -0
- package/dist/oauth/lock.js +287 -0
- package/dist/oauth/lock.js.map +1 -0
- package/dist/oauth/loopback.d.ts +85 -0
- package/dist/oauth/loopback.js +124 -0
- package/dist/oauth/loopback.js.map +1 -0
- package/dist/oauth/pkce.d.ts +58 -0
- package/dist/oauth/pkce.js +77 -0
- package/dist/oauth/pkce.js.map +1 -0
- package/dist/oauth/read.d.ts +120 -0
- package/dist/oauth/read.js +144 -0
- package/dist/oauth/read.js.map +1 -0
- package/dist/oauth/session.d.ts +80 -0
- package/dist/oauth/session.js +112 -0
- package/dist/oauth/session.js.map +1 -0
- package/dist/oauth/signin.d.ts +178 -0
- package/dist/oauth/signin.js +225 -0
- package/dist/oauth/signin.js.map +1 -0
- package/dist/oauth/token.d.ts +139 -0
- package/dist/oauth/token.js +183 -0
- package/dist/oauth/token.js.map +1 -0
- package/dist/paths.d.ts +366 -0
- package/dist/paths.js +460 -0
- package/dist/paths.js.map +1 -0
- package/dist/policy.d.ts +195 -0
- package/dist/policy.js +244 -0
- package/dist/policy.js.map +1 -0
- package/dist/post.d.ts +302 -0
- package/dist/post.js +384 -0
- package/dist/post.js.map +1 -0
- package/dist/project.d.ts +99 -0
- package/dist/project.js +192 -0
- package/dist/project.js.map +1 -0
- package/dist/redact.d.ts +175 -0
- package/dist/redact.js +700 -0
- package/dist/redact.js.map +1 -0
- package/dist/runtime.d.ts +30 -0
- package/dist/runtime.js +37 -0
- package/dist/runtime.js.map +1 -0
- package/dist/spawn_budget.d.ts +101 -0
- package/dist/spawn_budget.js +124 -0
- package/dist/spawn_budget.js.map +1 -0
- package/dist/spool.d.ts +390 -0
- package/dist/spool.js +556 -0
- package/dist/spool.js.map +1 -0
- package/dist/state.d.ts +109 -0
- package/dist/state.js +217 -0
- package/dist/state.js.map +1 -0
- package/dist/system_message.d.ts +101 -0
- package/dist/system_message.js +141 -0
- package/dist/system_message.js.map +1 -0
- package/dist/term.d.ts +112 -0
- package/dist/term.js +134 -0
- package/dist/term.js.map +1 -0
- package/dist/version.d.ts +16 -0
- package/dist/version.js +17 -0
- package/dist/version.js.map +1 -0
- 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
|
package/dist/exit.js.map
ADDED
|
@@ -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;
|