litmus-cli 1.4.32 → 1.4.34

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 (46) hide show
  1. package/README.md +11 -0
  2. package/dist/commands/connect.d.ts.map +1 -1
  3. package/dist/commands/connect.js +59 -6
  4. package/dist/commands/connect.js.map +1 -1
  5. package/dist/commands/doctor.d.ts.map +1 -1
  6. package/dist/commands/doctor.js +89 -0
  7. package/dist/commands/doctor.js.map +1 -1
  8. package/dist/commands/init.d.ts +56 -0
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +198 -37
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/push.d.ts.map +1 -1
  13. package/dist/commands/push.js +72 -5
  14. package/dist/commands/push.js.map +1 -1
  15. package/dist/commands/submit.d.ts.map +1 -1
  16. package/dist/commands/submit.js +12 -17
  17. package/dist/commands/submit.js.map +1 -1
  18. package/dist/lib/ai-tracking.d.ts +21 -0
  19. package/dist/lib/ai-tracking.d.ts.map +1 -1
  20. package/dist/lib/ai-tracking.js +67 -4
  21. package/dist/lib/ai-tracking.js.map +1 -1
  22. package/dist/lib/api.d.ts +24 -1
  23. package/dist/lib/api.d.ts.map +1 -1
  24. package/dist/lib/api.js +52 -1
  25. package/dist/lib/api.js.map +1 -1
  26. package/dist/lib/capture-nudge.d.ts.map +1 -1
  27. package/dist/lib/capture-nudge.js +5 -3
  28. package/dist/lib/capture-nudge.js.map +1 -1
  29. package/dist/lib/capture-selftest.d.ts +202 -0
  30. package/dist/lib/capture-selftest.d.ts.map +1 -0
  31. package/dist/lib/capture-selftest.js +492 -0
  32. package/dist/lib/capture-selftest.js.map +1 -0
  33. package/dist/lib/cursor-restart.d.ts +260 -0
  34. package/dist/lib/cursor-restart.d.ts.map +1 -0
  35. package/dist/lib/cursor-restart.js +308 -0
  36. package/dist/lib/cursor-restart.js.map +1 -0
  37. package/dist/lib/hook-logger.cjs +373 -42
  38. package/dist/lib/schemas.d.ts +5 -0
  39. package/dist/lib/schemas.d.ts.map +1 -1
  40. package/dist/lib/schemas.js +10 -0
  41. package/dist/lib/schemas.js.map +1 -1
  42. package/dist/lib/submit-route.d.ts +32 -41
  43. package/dist/lib/submit-route.d.ts.map +1 -1
  44. package/dist/lib/submit-route.js +33 -42
  45. package/dist/lib/submit-route.js.map +1 -1
  46. package/package.json +1 -1
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Post-init capture self-test (ENG-2477).
3
+ *
4
+ * WHY THIS EXISTS. A candidate ran Cursor for a full hour on the CLI lane,
5
+ * produced zero captured prompts, and the report told the judge the tool was
6
+ * not used. Every safeguard `init` had for that was a config validator or a
7
+ * sentence: the hook entry is present, the logger file exists, "restart your
8
+ * editor". None of them asks the only question that matters, which is whether
9
+ * a prompt typed into the tool would reach Litmus. Checked on a real
10
+ * engineer's machine on 2026-09-22, `~/.cursor/hooks.json` pointed at a
11
+ * leftover debugging probe: valid JSON, executable, exits 0, returns a
12
+ * well-formed verdict, sends nothing. Every shape-check passes on that file
13
+ * and it captures nothing. There will be a third cause with the same symptom.
14
+ *
15
+ * WHAT IT DOES. For each tool `init` configures, read the command string back
16
+ * out of the file the TOOL reads, run it the way the tool would (through the
17
+ * shell, the tool's own payload shape on stdin), and then ask the backend
18
+ * whether the resulting event landed in `candidate_activity_logs`. The payload
19
+ * carries a nonce; the read-back matches on it. Same reasoning as the
20
+ * Codespaces image's build-time hook self-test (`install-cursor-hook.sh` §4:
21
+ * a synthetic payload on stdin, asserted rather than assumed), one step
22
+ * further: that test asserts the hook exits 0 with parseable stdout, this one
23
+ * asserts an event ARRIVED, because exit 0 and a parseable verdict is exactly
24
+ * what the leftover probe produced.
25
+ *
26
+ * WHAT IT PROVES AND WHAT IT DOES NOT. Arrival proves the whole chain from the
27
+ * config file's command string to a row in the database: the entry is ours,
28
+ * the path resolves, node runs, the logger finds the assessment, reads the
29
+ * token, and the upload reaches the backend. It does NOT prove the tool will
30
+ * invoke that command. Cursor and Codex read their hook config once at launch,
31
+ * so a tool that was already open when `init` wrote the file fires nothing
32
+ * until it is restarted; and Codex, Gemini and Copilot skip untrusted hooks in
33
+ * silence. Those cannot be probed from outside the tool, which is why the pass
34
+ * message still carries the restart instruction, sharpened by a process scan
35
+ * to name the tool that is open right now. The probe also runs with THIS
36
+ * terminal's environment, not the editor's: a `node` that resolves here and
37
+ * not inside a Dock-launched Cursor is a gap this check cannot see.
38
+ *
39
+ * WARN, NOT GATE — and why. A failing self-test prints a red block as the last
40
+ * thing `init` outputs, records the verdict in `.litmus/` and uploads it as a
41
+ * `capture_selftest` event so the report and the admin monitor can see it,
42
+ * and then lets the candidate proceed. It does not refuse to finish. Three
43
+ * reasons, in order of weight:
44
+ *
45
+ * 1. The candidate may use none of these tools. A Cursor hook that cannot
46
+ * reach Litmus is no reason to block someone about to work with a web
47
+ * chat or no AI at all, and `init` cannot tell which candidate it has.
48
+ * 2. The check depends on things outside the candidate's control — the
49
+ * backend, their network, a corporate proxy. A gate turns a transient
50
+ * outage of ours into a candidate locked out minutes before a live
51
+ * interview, which is a worse harm than the one being prevented.
52
+ * 3. What a missed capture now costs the candidate is bounded. ENG-2476
53
+ * makes a zero-prompt Cursor session read as unmeasurable rather than as
54
+ * "did not use AI", so proceeding with a known gap yields a report that
55
+ * names the gap, not a wrong score. A gate would also need an escape
56
+ * hatch, and a candidate on a clock reaches for escape hatches blindly.
57
+ *
58
+ * The failure is made impossible to miss by placement and by record rather
59
+ * than by refusal: last block printed, coloured, actionable (`litmus doctor`
60
+ * repairs the hook files and re-runs this same check), and stored on both
61
+ * sides so nobody has to have been watching the terminal.
62
+ *
63
+ * Pure decisions live in exported functions so tests can drive them against a
64
+ * fake backend and a deliberately broken hook without going through `init`.
65
+ */
66
+ import { type HookCapturableTool, type InstalledHookCommands } from "./ai-tracking.js";
67
+ /** The event the logger records for a probe. Not a prompt; no reader counts it. */
68
+ export declare const PROBE_EVENT_TYPE = "capture_selftest_probe";
69
+ /** The CLI's own verdict, uploaded so the report and the monitor can see it. */
70
+ export declare const RESULT_EVENT_TYPE = "capture_selftest";
71
+ /** Where the verdict is kept locally, under `.litmus/`. */
72
+ export declare const SELFTEST_RECORD_FILE = "capture-selftest.json";
73
+ /**
74
+ * Per-hook budget. The logger waits up to 5s for a complete payload on stdin
75
+ * (STDIN_TIMEOUT_MS) and up to 2s for the upload (UPLOAD_TIMEOUT_MS); the
76
+ * hook runners themselves allow 10-30s. Generous on purpose: a probe that is
77
+ * killed early would report "no event" for a hook that was about to deliver.
78
+ */
79
+ export declare const HOOK_TIMEOUT_MS = 15000;
80
+ export declare const TOOL_LABELS: Record<HookCapturableTool, string>;
81
+ export type ToolStatus =
82
+ /** The probe landed in candidate_activity_logs. Capture works for this tool. */
83
+ "arrived"
84
+ /** No Litmus entry in the file the tool reads. Nothing would fire. */
85
+ | "not_installed"
86
+ /** The command ran (or failed to run) and nothing reached Litmus. */
87
+ | "no_event"
88
+ /** The hook ran but the read-back itself failed; nothing is known. */
89
+ | "unverified";
90
+ export interface ToolResult {
91
+ tool: HookCapturableTool;
92
+ label: string;
93
+ status: ToolStatus;
94
+ /** The config file that was read. */
95
+ file: string;
96
+ /** One line a candidate or a reviewer can act on. */
97
+ detail: string;
98
+ exitCode?: number | null;
99
+ stderrTail?: string;
100
+ }
101
+ export type Verdict = "pass" | "fail" | "unverified";
102
+ export interface CaptureSelfTestReport {
103
+ verdict: Verdict;
104
+ at: string;
105
+ durationMs: number;
106
+ tools: ToolResult[];
107
+ }
108
+ /** The prompt text a probe carries. Carries the nonce so a pre-ENG-2477 logger's
109
+ * `ai_prompt` record of it is still recognisable on the read-back. */
110
+ export declare function probePromptText(nonce: string): string;
111
+ /**
112
+ * The payload each tool's prompt hook would deliver, in that tool's shape, so
113
+ * the logger's real per-tool branch runs. Field sets are the ones the logger's
114
+ * extractPrompt documents per tool. `litmus_selftest` is the marker.
115
+ */
116
+ export declare function probePayload(tool: HookCapturableTool, nonce: string, assessmentDir: string): Record<string, unknown>;
117
+ export interface HookRun {
118
+ exitCode: number | null;
119
+ stdout: string;
120
+ stderr: string;
121
+ timedOut: boolean;
122
+ /** The shell itself could not be started. */
123
+ spawnError?: string;
124
+ }
125
+ /**
126
+ * Run one hook command the way its tool would: handed to the platform shell
127
+ * (`/bin/sh -c` on POSIX, `cmd.exe /c` on Windows, which is what the command's
128
+ * quoting was written for — see quoteForShell), payload on stdin, then stdin
129
+ * closed. The command string is not parsed or rebuilt here; whatever the
130
+ * candidate's file says is what runs.
131
+ *
132
+ * THE TIMEOUT IS AUTHORITATIVE. A hook is a candidate-controlled command, and
133
+ * it can start something that outlives it while holding the stdout/stderr
134
+ * pipes it inherited. Waiting for `close` would then wait for that grandchild,
135
+ * and neither `init` nor `doctor` has an outer deadline, so the candidate
136
+ * would sit at a spinner, blocked by the check meant to protect them, at the
137
+ * worst possible moment (Greptile on #2600). So on the deadline this settles
138
+ * with what it has, destroys its ends of the pipes, and kills the whole
139
+ * process group (the shell is spawned as a group leader for exactly that), or
140
+ * the tree on Windows. A hook that has not delivered inside HOOK_TIMEOUT_MS
141
+ * has not delivered; nothing it does afterwards changes the verdict.
142
+ */
143
+ export declare function runHookCommand(command: string, payload: Record<string, unknown>, opts?: {
144
+ cwd: string;
145
+ timeoutMs?: number;
146
+ env?: NodeJS.ProcessEnv;
147
+ }): Promise<HookRun>;
148
+ export interface SelfTestOptions {
149
+ assessmentDir: string;
150
+ backendUrl: string;
151
+ token: string;
152
+ /** Injected for tests; defaults to reading the real config files. */
153
+ hooks?: InstalledHookCommands[];
154
+ /** Injected for tests. */
155
+ confirm?: (nonces: string[]) => Promise<{
156
+ arrived: string[];
157
+ }>;
158
+ timeoutMs?: number;
159
+ env?: NodeJS.ProcessEnv;
160
+ }
161
+ /**
162
+ * Fire one probe per tool through its installed command(s), in parallel, then
163
+ * ask the backend which arrived. Never throws; every failure becomes a status
164
+ * on the tool it belongs to, and a read-back failure becomes "unverified"
165
+ * rather than "broken", because those call for different fixes.
166
+ */
167
+ export declare function runCaptureSelfTest(opts: SelfTestOptions): Promise<CaptureSelfTestReport>;
168
+ export declare function writeSelfTestRecord(assessmentDir: string, report: CaptureSelfTestReport): void;
169
+ /**
170
+ * The event `init` uploads with the verdict. Typed `capture_selftest`, which
171
+ * every reader treats as ours-not-theirs (cli-tracking.ts channel map); it
172
+ * exists so a failed check is visible on the report and the admin monitor,
173
+ * not only in a terminal nobody is watching. Paths are dropped: a home
174
+ * directory is a name.
175
+ */
176
+ export declare function selfTestResultEvent(report: CaptureSelfTestReport, source: "init" | "doctor"): Record<string, unknown>;
177
+ /**
178
+ * Tools that read hook config only at launch and are running at this moment,
179
+ * from a process listing (lowercased `ps aux` / `tasklist`). This is the one
180
+ * cause the probe cannot see, so the pass message names the tool by name
181
+ * instead of saying "any tool". Path-anchored patterns, like detection.ts:
182
+ * the word inside a config-directory path is not the tool running.
183
+ */
184
+ export declare function detectRunningHookTools(ps?: string): HookCapturableTool[];
185
+ /**
186
+ * Lines for the terminal. A failure is a coloured block meant to be the LAST
187
+ * thing `init` prints; a pass is one green line, plus a yellow one naming any
188
+ * tool that is open right now and needs a restart before it will fire.
189
+ */
190
+ export declare function renderCaptureSelfTest(report: CaptureSelfTestReport, opts: {
191
+ runningTools: HookCapturableTool[];
192
+ folderName: string;
193
+ }): string[];
194
+ /**
195
+ * The restart notice for tools that are open right now, shared by init's
196
+ * verdict block and doctor's failure lines so the two never disagree on the
197
+ * grammar or the claim.
198
+ */
199
+ export declare function renderRestartNotice(tools: HookCapturableTool[], opts?: {
200
+ then: string;
201
+ }): string[];
202
+ //# sourceMappingURL=capture-selftest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-selftest.d.ts","sourceRoot":"","sources":["../../src/lib/capture-selftest.ts"],"names":[],"mappings":"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"}