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.
- package/README.md +11 -0
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +59 -6
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +89 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +56 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +198 -37
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +72 -5
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/submit.d.ts.map +1 -1
- package/dist/commands/submit.js +12 -17
- package/dist/commands/submit.js.map +1 -1
- package/dist/lib/ai-tracking.d.ts +21 -0
- package/dist/lib/ai-tracking.d.ts.map +1 -1
- package/dist/lib/ai-tracking.js +67 -4
- package/dist/lib/ai-tracking.js.map +1 -1
- package/dist/lib/api.d.ts +24 -1
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +52 -1
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/capture-nudge.d.ts.map +1 -1
- package/dist/lib/capture-nudge.js +5 -3
- package/dist/lib/capture-nudge.js.map +1 -1
- package/dist/lib/capture-selftest.d.ts +202 -0
- package/dist/lib/capture-selftest.d.ts.map +1 -0
- package/dist/lib/capture-selftest.js +492 -0
- package/dist/lib/capture-selftest.js.map +1 -0
- package/dist/lib/cursor-restart.d.ts +260 -0
- package/dist/lib/cursor-restart.d.ts.map +1 -0
- package/dist/lib/cursor-restart.js +308 -0
- package/dist/lib/cursor-restart.js.map +1 -0
- package/dist/lib/hook-logger.cjs +373 -42
- package/dist/lib/schemas.d.ts +5 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +10 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/submit-route.d.ts +32 -41
- package/dist/lib/submit-route.d.ts.map +1 -1
- package/dist/lib/submit-route.js +33 -42
- package/dist/lib/submit-route.js.map +1 -1
- 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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"}
|