@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,236 @@
1
+ /**
2
+ * `vibecommit status` and `vibecommit off` — the screen, `CR-021`.
3
+ *
4
+ * Style guide §10.3 draws it and D65 makes that drawing the ruling, so this
5
+ * module renders it rather than designing it: four questions in fixed order
6
+ * (on for *this* repo? last successful send? where do I look? what do I type to
7
+ * fix it?), then the revoke link, on §13.5's `LABEL_GUTTER`.
8
+ *
9
+ * ## What is deliberately NOT on the screen
10
+ *
11
+ * §10.3's `last sent` row reads `4 minutes ago · 3 turns · seq 41`. **The turn
12
+ * count is not rendered and must not be.** Counting turns means parsing the
13
+ * transcript's NDJSON and deciding what a turn is, and that is the analysis the
14
+ * client is barred from holding — D60 §D6: *the client may render; it may not
15
+ * analyze, hash, or canonicalize*. The one recorded deviation is `CR-024d`'s
16
+ * redaction parse, authorised by name; there is no such authorisation here, and
17
+ * the server is the only thing holding sealed turns to count. The age and the
18
+ * `seq` are both facts this client already wrote down, so both render.
19
+ *
20
+ * ## Absence is not a blank
21
+ *
22
+ * When nothing has been delivered for this repo the row is OMITTED and
23
+ * `STATUS.neverSent` says so in a sentence. No sixth absence phrasing is
24
+ * invented here: `CR-112` (W8) owns the five-absence grammar as one set, and
25
+ * adding a variant now is what that task exists to prevent.
26
+ */
27
+ import { COMMANDS, CREDENTIAL, ERRORS, OFF, STATUS, URLS, relativeAge } from "../copy/index.js";
28
+ import { isProjectAllowed, revokeProject } from "../consent.js";
29
+ import { loadCredential } from "../credential.js";
30
+ import { EXIT } from "../exit.js";
31
+ import { resolveRepoSlug } from "../git.js";
32
+ import { emitJson } from "../json.js";
33
+ import { resolveProjectKeys } from "../project.js";
34
+ import { lastSendForRepo } from "../state.js";
35
+ import { LABEL_GUTTER, WRAP_COLUMNS, glyph, labelled, paint, renderErrorBlock, truncatePath, wrap, } from "../term.js";
36
+ import { writeLines } from "./context.js";
37
+ /** §10.3 indents the gutter rows four columns under the state line. */
38
+ const ROW_INDENT = 4;
39
+ export function status(ctx, argv = []) {
40
+ // ⛔ THIS SCREEN USES BOTH ROLES AT ONCE (`D184 §1`), which is why the whole
41
+ // function is not flipped: `on` below is CONSENT STATE and keys on the git
42
+ // common dir, while the repo row, the send scan and the `--json` document are
43
+ // REPO IDENTITY and stay on the worktree toplevel. The `--json` field is
44
+ // literally named `toplevel`, so moving it would make the document lie.
45
+ const keys = resolveProjectKeys(ctx.cwd);
46
+ if (keys === null) {
47
+ writeLines(ctx.stderr, renderErrorBlock({ kind: "bad", what: ERRORS.notAGitRepo, why: [] }, ctx.colour));
48
+ return EXIT.failure;
49
+ }
50
+ const projectKey = keys.worktree;
51
+ // ⛔ CONSENT STATE, on the common dir. A worktree of a consented clone must
52
+ // report ON here, because the hook that runs there is now admitted — a
53
+ // `status` still keyed on the toplevel would tell that user they are
54
+ // disconnected while their session was being captured.
55
+ const on = isProjectAllowed(ctx.home, keys.consent);
56
+ const kind = on ? "ok" : "warn";
57
+ const lines = [
58
+ ` ${paint(ctx.colour, kind, glyph(ctx.colour, kind))} ${paint(ctx.colour, "strong", on ? STATUS.onForRepo : STATUS.offForRepo)}`,
59
+ labelled(ROW_INDENT, STATUS.repoLabel, repoValue(ctx, projectKey)),
60
+ ];
61
+ // Question 2: the last successful send FOR THIS REPO. `status` has no
62
+ // session_id — it is interactive — so this is a scan of the repo's session
63
+ // files rather than a lookup (`lastSendForRepo`).
64
+ const lastSend = lastSendForRepo(ctx.home, projectKey);
65
+ if (lastSend !== null) {
66
+ lines.push(labelled(ROW_INDENT, STATUS.lastSuccessLabel, STATUS.lastSuccessValue(relativeAge(lastSend.at, ctx.now().getTime()), lastSend.seq)));
67
+ }
68
+ lines.push(labelled(ROW_INDENT, STATUS.dashboardLabel, paint(ctx.colour, "accent", URLS.dashboard)));
69
+ const load = loadCredential({ env: ctx.env, home: ctx.home });
70
+ // §13.3 — `--json` emits NOTHING BUT the JSON document on stdout, so the
71
+ // screen assembled above is DISCARDED rather than printed beside it.
72
+ //
73
+ // The credential fault below still refuses, and it refuses the same way it
74
+ // does in prose: §13.6's block to stderr, nothing on stdout, exit 1. A failure
75
+ // is not an answer, so it gets no document — `src/json.ts`'s docblock states
76
+ // that split and why it is a builder's reading of §13.3 rather than a ruling.
77
+ //
78
+ // ⚠ `last_send` is `null` when nothing has been delivered, never a zeroed
79
+ // object: absence here is *nothing has been sent*, and a `{ at_ms: 0 }` would
80
+ // be a timestamp claim. §13.2's glyph rule has no glyph on this surface, so
81
+ // `outcome` carries the on/off distinction the glyph carries on screen.
82
+ if (argv.includes("--json")) {
83
+ if (load.kind !== "ok" && load.kind !== "absent") {
84
+ writeLines(ctx.stderr, credentialProblem(load, ctx.colour));
85
+ return EXIT.failure;
86
+ }
87
+ emitJson(ctx.stdout, "status", on ? "on" : "off", {
88
+ repo: resolveRepoSlug(projectKey),
89
+ toplevel: projectKey,
90
+ last_send: lastSend === null ? null : { at_ms: lastSend.at, seq: lastSend.seq },
91
+ dashboard: URLS.dashboard,
92
+ });
93
+ return on ? EXIT.ok : EXIT.notConnected;
94
+ }
95
+ if (load.kind !== "ok" && load.kind !== "absent") {
96
+ writeLines(ctx.stdout, lines);
97
+ writeLines(ctx.stderr, credentialProblem(load, ctx.colour));
98
+ return EXIT.failure;
99
+ }
100
+ // Absence is a SENTENCE, not a blank row. Triggered on the send record rather
101
+ // than on the credential: a repo that has delivered and then lost its
102
+ // credential has still recorded a session, and saying otherwise would be false.
103
+ if (on && lastSend === null)
104
+ lines.push("", ...wrap(STATUS.neverSent, 2));
105
+ lines.push("", ...actionLines(ctx, on));
106
+ writeLines(ctx.stdout, lines);
107
+ return on ? EXIT.ok : EXIT.notConnected;
108
+ }
109
+ /**
110
+ * §10.3's repo row: `owner/name (git toplevel ~/path)`.
111
+ *
112
+ * The slug is `CR-019d`'s — the same value that goes on the wire as
113
+ * `X-Repo-Slug` — so what `status` shows is what the server will resolve, which
114
+ * is the point of `status` being the debugging tool (D57 §DX1). A repo with no
115
+ * GitHub remote shows its `local:` identity rather than hiding it: that IS the
116
+ * answer to "why does the dashboard not show my repo under its name?".
117
+ *
118
+ * The path is truncated from the LEFT (§13.4) against what the line actually has
119
+ * left, not a fixed 60, so the row cannot exceed 80 columns for a long slug.
120
+ */
121
+ function repoValue(ctx, toplevel) {
122
+ const slug = resolveRepoSlug(toplevel);
123
+ const shown = tildify(toplevel, ctx.home);
124
+ const overhead = ROW_INDENT + LABEL_GUTTER + STATUS.repoToplevel(slug, "").length;
125
+ let budget = Math.max(12, WRAP_COLUMNS - overhead);
126
+ let value = STATUS.repoToplevel(slug, truncatePath(shown, budget));
127
+ // Fit the row by BYTES as well as by columns, and pay the difference only on
128
+ // rows that actually truncate. `truncatePath`'s `…` is one column but three
129
+ // UTF-8 bytes, so a row filled to exactly 80 columns is 82 bytes — which
130
+ // passes §13.4 (a terminal renders one column) and fails the byte-counting
131
+ // `awk` check the verify recipe uses. BSD awk counts bytes whatever the
132
+ // locale, so satisfying the stricter measure is cheaper than arguing about
133
+ // which one is right: it costs at most two columns, and only here.
134
+ while (ROW_INDENT + LABEL_GUTTER + Buffer.byteLength(value) > WRAP_COLUMNS && budget > 12) {
135
+ budget -= 1;
136
+ value = STATUS.repoToplevel(slug, truncatePath(shown, budget));
137
+ }
138
+ return value;
139
+ }
140
+ /** `/Users/dev/code/x` -> `~/code/x`, as §10.3 draws it. */
141
+ function tildify(path, home) {
142
+ if (home !== "" && (path === home || path.startsWith(`${home}/`))) {
143
+ return `~${path.slice(home.length)}`;
144
+ }
145
+ return path;
146
+ }
147
+ /**
148
+ * The two trailing actions, aligned as one pair — §10.3 draws their values in a
149
+ * shared column, which only holds if both labels pad to the longer of the two.
150
+ *
151
+ * When capture is OFF the first line is the reconnect command instead: there is
152
+ * nothing to turn off, and §10.3 only draws the on-state.
153
+ */
154
+ function actionLines(ctx, on) {
155
+ const pairs = on
156
+ ? [
157
+ [STATUS.turnOffLabel, COMMANDS.off],
158
+ [STATUS.revokeLabel, URLS.settings],
159
+ ]
160
+ : [
161
+ [STATUS.fixCommandLabel, COMMANDS.connect],
162
+ [STATUS.revokeLabel, URLS.settings],
163
+ ];
164
+ const width = Math.max(...pairs.map(([label]) => label.length));
165
+ return pairs.map(([label, value]) => {
166
+ const painted = value.startsWith("https://") ? paint(ctx.colour, "accent", value) : value;
167
+ return ` ${`${label}:`.padEnd(width + 2)}${painted}`;
168
+ });
169
+ }
170
+ export function off(ctx) {
171
+ const keys = resolveProjectKeys(ctx.cwd);
172
+ if (keys === null) {
173
+ writeLines(ctx.stderr, renderErrorBlock({ kind: "bad", what: ERRORS.notAGitRepo, why: [] }, ctx.colour));
174
+ return EXIT.failure;
175
+ }
176
+ // ⛔ THE REVOKE SIDE, AND IT MUST DELETE THE KEY `connect` WROTE (`D184 §9`).
177
+ // `revokeProject` deletes by exact string and reports `false` when the key is
178
+ // absent — so a revoke still keyed on the toplevel would find nothing, print
179
+ // "already off", and LEAVE THE GRANT IN PLACE. The user believes they
180
+ // withdrew consent; capture continues. That is the worse direction of this
181
+ // same bug, and it is the second mutation the cells below pin.
182
+ const changed = revokeProject(ctx.home, keys.consent);
183
+ writeLines(ctx.stdout, [
184
+ ...wrap(changed ? OFF.done : OFF.alreadyOff, 2),
185
+ ...wrap(OFF.note, 2),
186
+ ]);
187
+ return EXIT.ok;
188
+ }
189
+ /**
190
+ * The three credential problems that are worth four different fixes on an
191
+ * interactive surface. The hook collapses them to one `systemMessage`; here the
192
+ * user can act, so §13.6 gets its full what / why / fix.
193
+ *
194
+ * None of these lines can carry credential bytes: the plaintext leaves
195
+ * `IngestCredential` only through `expose()`, and no copy path calls it.
196
+ */
197
+ function credentialProblem(load, colour) {
198
+ switch (load.kind) {
199
+ case "wrong-class":
200
+ return renderErrorBlock({
201
+ kind: "bad",
202
+ what: CREDENTIAL.wrongClassWhat,
203
+ why: [
204
+ load.source === "env"
205
+ ? CREDENTIAL.wrongClassEnvWhy
206
+ : CREDENTIAL.wrongClassFileWhy,
207
+ ],
208
+ fixLabel: CREDENTIAL.wrongClassFix,
209
+ fixes: [COMMANDS.connect],
210
+ }, colour);
211
+ case "insecure-file":
212
+ return renderErrorBlock({
213
+ kind: "bad",
214
+ what: CREDENTIAL.insecureFileWhat,
215
+ why: [
216
+ `${CREDENTIAL.insecureFileWhyLabel} ${truncatePath(load.path, 60)}`,
217
+ `${CREDENTIAL.insecureFileModeLabel} ${load.mode}`,
218
+ CREDENTIAL.insecureFileWhy,
219
+ ],
220
+ fixLabel: CREDENTIAL.insecureFileFix,
221
+ fixes: [COMMANDS.chmodCredentials],
222
+ }, colour);
223
+ case "unreadable":
224
+ return renderErrorBlock({
225
+ kind: "bad",
226
+ what: CREDENTIAL.unreadableWhat,
227
+ why: [
228
+ `${CREDENTIAL.insecureFileWhyLabel} ${truncatePath(load.path, 60)}`,
229
+ CREDENTIAL.unreadableWhy,
230
+ ],
231
+ fixLabel: CREDENTIAL.unreadableFix,
232
+ fixes: [COMMANDS.connect],
233
+ }, colour);
234
+ }
235
+ }
236
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,349 @@
1
+ /**
2
+ * `vibecommit why` — the screenshotted surface. `CR-086`, W9.
3
+ *
4
+ * *"Show the conversation turns recorded against the commit that last changed
5
+ * this line."* That sentence is VERBATIM-APPROVED (D61 §PS6), already in
6
+ * `strings.ts` and already pinned by `test/help.golden.txt`. This module is what
7
+ * makes it true.
8
+ *
9
+ * The screen is style guide §10.4; the copy is §R3's decision table in
10
+ * `docs/designs/claims-audit-why.md`, which §10.4 says must not be paraphrased.
11
+ * Layout is `DESIGN.md` §13 — six SGR roles, base-16 only, 80-column HARD WRAP,
12
+ * §13.6's error shape, §13.7's exit contract.
13
+ *
14
+ * ─────────────────────────────────────────────────────────────────────────────
15
+ * ## ⚠ WHAT THIS VERB SHIPS WITHOUT, AND WHY IT SHIPS ANYWAY
16
+ *
17
+ * `why` is *blame → commit → attribution*. The third arrow has no producer, and
18
+ * two more of this screen's inputs do not exist either. All four were
19
+ * re-measured for this task rather than taken on trust (every tracked blob at
20
+ * `vibecommit-capture` `4dcf47d`, `vibecommit-mcp` `eeda878`,
21
+ * and `vibecommit-schema` `2c77400`, read as utf8 in
22
+ * Python — never `grep`, because `src/redact.ts` carries literal NUL bytes and
23
+ * `grep -I` skips it silently):
24
+ *
25
+ * 1. **`observed` has no producer in any repo** — 0 client git probes on the
26
+ * write path, and `vibecommit-mcp/src/conversation/edge_derivation.ts:36-47`
27
+ * states it outright. This module does not build one and must not be read
28
+ * as building one: `git blame` here is an INTERACTIVE READ-PATH probe whose
29
+ * output never crosses the ingest wire. `src/hooks/`, `src/post.ts` and
30
+ * `src/state.ts` are untouched.
31
+ * 2. **Nothing writes `commit_file_attribution`** — the table `agent_id` lives
32
+ * on. `CR-070` created it; `CR-072` shipped the edge only and said so.
33
+ * 3. **`capture_turns.subagent_count` EXISTS AND STILL HAS NO PRODUCER.**
34
+ * `CR-129a` added it (`vibecommit-schema` `f3022c5`), `integer`, nullable,
35
+ * no default. Nothing can carry it: mcp's `insertCaptureTurns` is an
36
+ * explicit seven-key literal and `blame_commit` selects
37
+ * `position, turn_hash`, so the column never reaches this client. The
38
+ * measurement, with the file and line for each answer, lives in
39
+ * `test/subagent-count.test.ts`.
40
+ *
41
+ * ⚠ **A COLUMN LIST STOOD HERE AND IT WAS A TALLY — D101, WRITE THE
42
+ * PREDICATE, NEVER THE COUNT.** It read `capture_id, position,
43
+ * workspace_id, conversation_id, turn_hash, is_human_prompt`, and it was
44
+ * an exact transcription of the original `create table` when it was
45
+ * written. **Three later migrations emptied it, none of them wrong:**
46
+ * `20260610140000` added `human_prompt_hash`; `20260620180000` renamed
47
+ * `workspace_id` to `org_id` across fourteen tables; `20260820000000`
48
+ * added `subagent_count`. So the list decayed to naming a column that no
49
+ * longer exists — D97's species, a statement that was genuine when
50
+ * authored and was emptied later by someone else's correct change. **The
51
+ * live set is not restated here**, because restating it would start the
52
+ * same decay again; ask the catalog.
53
+ * 4. **`blame_commit` does not exist** — it is `CR-085`, being built in
54
+ * another repo in this same wave, and D106 §1 measured production mcp still
55
+ * serving release v4, which has no ingest route at all. **There is no
56
+ * deployed server to test this against.**
57
+ *
58
+ * (1) and (2) are recorded in D104 §2 as owed their own `CR-###` and are
59
+ * explicitly not this task's. (3) was `CR-129`: `CR-129a` shipped the column in
60
+ * W10 and `CR-129d` verified the consumer against it — **the gap is narrower
61
+ * than it was and it is not closed**, because a producer needs an mcp write
62
+ * path AND an mcp read path, and no row owns either.
63
+ *
64
+ * D81 governs the shape of the answer: *"A task can be un-shippable because of
65
+ * what it CLAIMS, not because of what it lacks… making the claim honest is
66
+ * usually cheaper than building the thing, and it is always the correct
67
+ * order."* So every one of those holes renders as an absence in this project's
68
+ * grammar — **state the absence, state the cause, state what would change it** —
69
+ * and none of them renders as a zero, a placeholder, or a silence.
70
+ *
71
+ * ─────────────────────────────────────────────────────────────────────────────
72
+ * ## THE THREE RULES THIS FILE IS BUILT AROUND
73
+ *
74
+ * **The payload is UNTRUSTED JSON.** `blame_commit` has no implementation to
75
+ * agree with, so `BlameCommitPayload` below is a contract this repo DECLARES and
76
+ * `CR-085` owns the other side of. Every field is validated; anything
77
+ * unrecognised is `malformed` and renders as such rather than being partly
78
+ * guessed at.
79
+ *
80
+ * **The client may render; it may not analyze** (D60 §D1a). The one authorised
81
+ * deviation in this package is `CR-024d`'s redaction parse, by name; there is no
82
+ * authorisation here. So the truncation totals come from the payload and this
83
+ * module never counts the array it was handed, never ranks it, and never
84
+ * reorders it.
85
+ *
86
+ * **`null` and ABSENT are different facts.** The schema comment on `agent_id`
87
+ * says *"NULL = the main thread"* — so a null we were SENT is knowledge, and a
88
+ * field that never arrived is not. An unmarked turn rendered from an absent
89
+ * field is the overclaim, so a turn is rendered unmarked only when the field was
90
+ * actually present.
91
+ *
92
+ * @provenance vibecommit-mcp src/read/envelope.ts — the one-text-part wire shape, retyped
93
+ * @provenance vibecommit-schema capture_turns + commit_file_attribution — MEASURED ABSENCE, cited
94
+ */
95
+ import { type BlamedCommit, type EdgeGrade } from "../copy/index.js";
96
+ import { readWithSession } from "../oauth/signin.js";
97
+ import { type CommandContext } from "./context.js";
98
+ /**
99
+ * THE SHA-LENGTH RULE — read this before "fixing" the asymmetry.
100
+ *
101
+ * ⚠⚠ **THE TWO SHAS ON THE SQUASH SCREEN ARE ABBREVIATED DIFFERENTLY, ON
102
+ * PURPOSE, AND THE SCREEN SAYS SO** (D120 §1 as revised).
103
+ *
104
+ * The BLAMED sha renders at `ABBREV`, like every other sha this product prints:
105
+ * it is on the reader's ref, so seven hex resolves under `git show` — which is
106
+ * §10.6 E's stated purpose — and seven is also git's own dynamic `--short`
107
+ * length for repositories this size.
108
+ *
109
+ * **The RECORDED sha renders IN FULL, and the first ruling to abbreviate it was
110
+ * reversed.** This state exists for the squash case, and after a squash-merge
111
+ * WITH BRANCH DELETE the original commit is **not in the reader's clone at
112
+ * all** — measured, not assumed: cloned over the git transport, the original
113
+ * object is ABSENT and `git show` fails while the squashed commit resolves.
114
+ *
115
+ * ⚠ **AND THE MECHANISM IS NOT THE ONE THE RULING ASSUMED.** The full sha does
116
+ * NOT resolve locally either — if the object is gone, both forms fail. What the
117
+ * full value buys is that it stays a COMPLETE IDENTIFIER, resolvable off this
118
+ * machine: it can be looked up on the host, quoted in a record, or matched
119
+ * against another system. **A seven-character prefix is not addressable that
120
+ * way** — prefix resolution needs a store that already holds the object, and
121
+ * nothing here does. Truncating it would make the value unrecoverable
122
+ * anywhere, which is a claims problem rather than a formatting one.
123
+ */
124
+ /**
125
+ * What `why` requires of `blame_commit`.
126
+ *
127
+ * ⚠ **UNRECONCILED AS OF `vibecommit-mcp` `eeda878`.** No tool of this name
128
+ * exists there: `src/read/envelope.ts:18` says *"`blame_commit` DOES NOT EXIST
129
+ * until W9"*, and `CR-085` — the last link of W9's serial mcp chain — is being
130
+ * written in parallel with this file, by a different builder, in a repo this
131
+ * task may not touch. This declaration was reported to the orchestrator so the
132
+ * two sides could be AGREED rather than discovered at merge.
133
+ *
134
+ * The transport contract it rides on IS settled and golden-tested on both sides
135
+ * (`vibecommit-mcp/src/read/envelope.ts`, `src/oauth/read.ts` here): exactly one
136
+ * `text` part whose text is `JSON.stringify(payload)`, consumed by a single
137
+ * `JSON.parse`, with an optional `isError: true` on the same shape.
138
+ *
139
+ * ### Request
140
+ *
141
+ * ```jsonc
142
+ * { "repo": "acohan/blog", // resolveRepoSlug — the X-Repo-Slug value
143
+ * "commit_sha": "<40 hex>", // what `git blame` returned, in full
144
+ * "file_path": "src/auth/session.ts" // repo-relative, as blame reported it
145
+ * }
146
+ * ```
147
+ *
148
+ * ### Response — a discriminated union on `state`, and every arm must render
149
+ *
150
+ * ```jsonc
151
+ * { "state": "cold_start" } // no capture for this repo, ever
152
+ * { "state": "no_edge" } // repo captured; this commit has none
153
+ * { "state": "squash_resolved", "recorded_sha": "<40 hex>" }
154
+ * { "state": "turns",
155
+ * "grades": ["derived"], // EVERY edge grade held; the floor governs
156
+ * "turns": [ { "position": 12, "tool": "Edit", // ⚠ string | null | ABSENT — see below
157
+ * "files": ["src/auth/session.ts"],
158
+ * "excerpt": ["…verbatim scrubbed…"],
159
+ * "agent_id": null, // ⚠ REQUIRED — see below
160
+ * "subagent_count": 2 } ], // ⚠ optional; no producer (CR-129)
161
+ * "window": { "first": 12, "last": 16, "total": 23 }, // server-computed; never counted here
162
+ * "files_without_turn": 2 }
163
+ * ```
164
+ *
165
+ * ### ⚠ `tool` IS `string | null | ABSENT`, AND THE SERVER MUST NOT NAME THE HOLE
166
+ *
167
+ * **The wire carries the FACT; the client owns the WORDS** (D112 §3). A turn that
168
+ * ran no tool sends `null` or omits the key, and this module renders **no
169
+ * bracket and no space where the bracket was**. All three inputs are accepted
170
+ * and the two absence forms render identical bytes.
171
+ *
172
+ * ⚠ **A placeholder here is refused even when it is TRUE.** `"(none)"` is
173
+ * derivable from a turn actually fetched, so it is a claim about content rather
174
+ * than an invention — and it is still rejected, because a user-facing string
175
+ * minted in `vibecommit-mcp` bypasses **every copy gate this repo has**:
176
+ * `test/copy-claims.test.ts` maps exports of `strings.ts` and a wire string is
177
+ * in no export; `test/copy-width.test.ts` never measures it at 80 columns;
178
+ * `test/copy-inline-literals.test.ts` cannot see a literal that is not in this
179
+ * tree. `CR-090d` centralised this package's copy for exactly one reason — **the
180
+ * claims gate can only check copy it can find.**
181
+ *
182
+ * ⚠ **AND PRESENCE IS DELIBERATELY NOT REQUIRED.** Requiring a key the server
183
+ * could not satisfy is what broke this screen once already: `typeof value.tool
184
+ * !== "string"` rejected one turn, one rejected turn made the WHOLE document
185
+ * malformed, and `why` exited 1 on every payload (D110 §1). A required-but-
186
+ * nullable `tool` reproduces that defect one field over, so `parseTurn` accepts
187
+ * a turn object with no `tool` key at all.
188
+ *
189
+ * ### ⚠ THE ONE REQUIREMENT THIS FILE MAKES OF `CR-085`, AND WHY
190
+ *
191
+ * **`agent_id` must be PRESENT on every turn object — `null` for the main
192
+ * thread, a string for a delegated agent — and its absence must never be used to
193
+ * mean "main thread".** The schema comment on the column is explicit that *"NULL
194
+ * = the main thread"* and that the empty string is refused because it *"would
195
+ * render as an unnamed sub-agent — a plausible wrong answer rather than an
196
+ * error"*. The same reasoning one level up makes an ABSENT field and a null
197
+ * field different facts, so this module marks every turn `actor not recorded`
198
+ * unless the key was actually there.
199
+ *
200
+ * **Today `CR-085` cannot satisfy that**, because `commit_file_attribution` has
201
+ * no writer — which is why the absence branch is the one that ships, named on
202
+ * screen rather than hidden. `subagent_count` is worse: the column does not
203
+ * exist in any schema, so the second attribution row is implemented, exercised
204
+ * against a synthetic payload, and unreachable on the wire. Both are `CR-129`.
205
+ */
206
+ export interface BlameCommitPayload {
207
+ readonly state: "cold_start" | "no_edge" | "edge_unreadable" | "squash_resolved" | "turns";
208
+ /**
209
+ * The sha the session was actually recorded against, when a rewrite was
210
+ * resolved. ⚠ **Legal on the `turns` branch too** (D112 §4) — that is the
211
+ * whole of `CR-138`: `squash_resolved` was TERMINAL, and `parseTarget`
212
+ * accepts only `<file>:<line>`, so the state handed the reader a sha and no
213
+ * verb that takes it. A dead end by construction. Carried on `turns`, both
214
+ * shas render AND the session is readable underneath them.
215
+ */
216
+ readonly recordedSha: string | null;
217
+ /**
218
+ * HOW the pair was established, and it RENDERS rather than being carried.
219
+ * `null` when no rewrite was resolved — the ordinary case.
220
+ */
221
+ readonly matchKind: MatchKind | null;
222
+ readonly grades: readonly EdgeGrade[];
223
+ readonly turns: readonly WhyTurn[];
224
+ readonly window: TurnWindow | null;
225
+ readonly filesWithoutTurn: number | null;
226
+ }
227
+ /** One turn, exactly as §10.4 draws its row. */
228
+ export interface WhyTurn {
229
+ readonly position: number;
230
+ /**
231
+ * The tool this turn ran, or `null` when none is recorded — and on this field
232
+ * ALONE, `null` and ABSENT are the same fact (D112 §3). Both render no
233
+ * bracket, which is the point: **no bracket makes no claim**, where an
234
+ * `[(none)]` would be words this repo's copy gates cannot see. The opposite
235
+ * of the `agent_id` rule below, deliberately, because `tool` has no reading
236
+ * under which absence could mean "the main thread".
237
+ */
238
+ readonly tool: string | null;
239
+ readonly files: readonly string[];
240
+ readonly excerpt: readonly string[];
241
+ /**
242
+ * `null` = the main thread, a string = that sub-agent. ⚠ Meaningful ONLY when
243
+ * `agentIdPresent` — otherwise it is this module's default, not the server's
244
+ * answer, and rendering an unmarked turn from it would be the overclaim.
245
+ */
246
+ readonly agentId: string | null;
247
+ readonly agentIdPresent: boolean;
248
+ /** §R3 row 2. NO PRODUCER EXISTS — `capture_turns.subagent_count` is not a column. */
249
+ readonly subagentCount: number | null;
250
+ }
251
+ /**
252
+ * §R3's truncation notice. Every number is the server's (D60 §D1a).
253
+ *
254
+ * ⚠ `first` / `last`, NOT `from` / `to`, and the reason is a known scanner shape
255
+ * rather than taste. `test/provenance.test.ts`'s linkage wall is a REGEX over
256
+ * raw text — `/(?:from|import)\s*\(?\s*"([^"]+)"/` — so the token `from`
257
+ * immediately followed by a double quote reads as an import specifier escaping
258
+ * the package, ANYWHERE in the file including a JSON example in a docblock.
259
+ * `src/oauth/signin.ts` already carries the same warning for prose. A false RED
260
+ * is the more dangerous direction (it justifies "fixing" correct content and
261
+ * nothing later flags it), so the field is named around it and the reason is
262
+ * written down. `first`/`last` also says INCLUSIVE, which `from`/`to` does not.
263
+ */
264
+ export interface TurnWindow {
265
+ readonly first: number;
266
+ readonly last: number;
267
+ readonly total: number;
268
+ }
269
+ /**
270
+ * How a squash rewrite was established — `commit_sha_successors.match_kind`.
271
+ *
272
+ * ⚠ **THE SET IS EXACTLY TWO AND AN UNRECOGNISED VALUE IS MALFORMED, NEVER
273
+ * DEFAULTED.** Defaulting would silently downgrade a claim: `exact` came from a
274
+ * recorded successor row, `probable` from a patch-id match, and D67 forbids
275
+ * folding them. The far side freezes the same pair
276
+ * (`vibecommit-mcp/src/db/commit_sha_successors.ts`, `SUCCESSOR_MATCH_KINDS`),
277
+ * and its own comment says a third value is *"a `DECISIONS.md` question, not a
278
+ * widening"* — so a value we do not recognise means the two repos disagree,
279
+ * which is precisely the thing to refuse rather than paper over.
280
+ */
281
+ export type MatchKind = "exact" | "probable";
282
+ /**
283
+ * ⛔ THE SKEW SENTINEL. Returned when the document is WELL-FORMED but names a
284
+ * `state` this build does not know — which means the server is newer than this
285
+ * client, not that the answer is broken.
286
+ *
287
+ * ⚠ A UNIQUE OBJECT, compared by identity. A string sentinel could collide with
288
+ * a future legitimate return, and `null` is already taken by "malformed" — the
289
+ * two must stay distinguishable, because they send the user to opposite
290
+ * remedies: "report this" versus "update the plugin".
291
+ */
292
+ export declare const PAYLOAD_VERSION_SKEW: Readonly<{
293
+ skew: true;
294
+ }>;
295
+ export type PayloadVersionSkew = typeof PAYLOAD_VERSION_SKEW;
296
+ /**
297
+ * ⚠ A TYPE GUARD RATHER THAN A BARE `===`. TypeScript does not narrow a union
298
+ * on object identity — the two members share no discriminant literal — so
299
+ * `payload === PAYLOAD_VERSION_SKEW` compiles but leaves `payload` wide, and
300
+ * the next line passing it to a renderer fails to typecheck. The guard makes
301
+ * the narrowing explicit and keeps the identity comparison as the actual test.
302
+ */
303
+ export declare function isVersionSkew(p: BlameCommitPayload | PayloadVersionSkew): p is PayloadVersionSkew;
304
+ /**
305
+ * The whole document.
306
+ *
307
+ * - `null` — MALFORMED. The server sent something that is not a valid document
308
+ * in any version, and the remedy is to report it.
309
+ * - `PAYLOAD_VERSION_SKEW` — WELL-FORMED, UNKNOWN STATE. The server is newer
310
+ * than this client and the remedy is to upgrade. ⛔ Added by `U1`
311
+ * (2026-08-30): before it, this case returned `null` and told the user a
312
+ * perfectly good answer was broken. See `WHY.skewWhat` for why that mattered
313
+ * more here than in most protocols.
314
+ * - a payload — parsed.
315
+ */
316
+ export declare function parseBlameCommitPayload(document: unknown): BlameCommitPayload | PayloadVersionSkew | null;
317
+ export interface BlameResult {
318
+ readonly sha: string;
319
+ readonly commit: BlamedCommit;
320
+ }
321
+ /**
322
+ * `git blame` one line, via `--porcelain`.
323
+ *
324
+ * Porcelain rather than the human format: the human format embeds the author's
325
+ * NAME, which can contain spaces, parentheses and digits, so every field after
326
+ * it is positional guesswork. Porcelain puts the SHA first on its own line and
327
+ * the timestamp on a labelled one.
328
+ */
329
+ export declare function blameLine(dir: string, file: string, line: number): BlameResult | null;
330
+ /** Exported so the parser can be driven against captured git output in the suite. */
331
+ export declare function parseBlamePorcelain(out: string): BlameResult | null;
332
+ export interface WhyTarget {
333
+ readonly file: string;
334
+ readonly line: number;
335
+ }
336
+ /**
337
+ * `<file>:<line>`.
338
+ *
339
+ * `lastIndexOf` rather than a split, because a path may legitimately contain a
340
+ * colon and only the last one can be the line number. The tail must be all
341
+ * digits and at least 1 — `file:0` and `file:-3` are not lines, and `file:1e3`
342
+ * is the kind of input `parseInt` would quietly accept as 1.
343
+ */
344
+ export declare function parseTarget(argv: readonly string[]): WhyTarget | null;
345
+ export interface WhyDeps {
346
+ /** Injected so the suite can drive every arm without a server — there is none. */
347
+ readonly read?: typeof readWithSession;
348
+ }
349
+ export declare function why(ctx: CommandContext, argv: readonly string[], deps?: WhyDeps): Promise<number>;