litmus-cli 1.4.35 → 1.4.37

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 (45) hide show
  1. package/README.md +28 -2
  2. package/dist/commands/connect.d.ts +59 -4
  3. package/dist/commands/connect.d.ts.map +1 -1
  4. package/dist/commands/connect.js +137 -12
  5. package/dist/commands/connect.js.map +1 -1
  6. package/dist/commands/init.d.ts.map +1 -1
  7. package/dist/commands/init.js +3 -2
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/lib/ai-tracking.d.ts +5 -0
  10. package/dist/lib/ai-tracking.d.ts.map +1 -1
  11. package/dist/lib/ai-tracking.js +24 -9
  12. package/dist/lib/ai-tracking.js.map +1 -1
  13. package/dist/lib/api.d.ts.map +1 -1
  14. package/dist/lib/api.js +6 -5
  15. package/dist/lib/api.js.map +1 -1
  16. package/dist/lib/backfill-tools.d.ts +37 -1
  17. package/dist/lib/backfill-tools.d.ts.map +1 -1
  18. package/dist/lib/backfill-tools.js +63 -12
  19. package/dist/lib/backfill-tools.js.map +1 -1
  20. package/dist/lib/capture-selftest.d.ts +7 -2
  21. package/dist/lib/capture-selftest.d.ts.map +1 -1
  22. package/dist/lib/capture-selftest.js +4 -4
  23. package/dist/lib/capture-selftest.js.map +1 -1
  24. package/dist/lib/cursor-restart.js +1 -1
  25. package/dist/lib/cursor-restart.js.map +1 -1
  26. package/dist/lib/detection.d.ts +75 -11
  27. package/dist/lib/detection.d.ts.map +1 -1
  28. package/dist/lib/detection.js +259 -33
  29. package/dist/lib/detection.js.map +1 -1
  30. package/dist/lib/hook-logger.cjs +57 -60
  31. package/dist/lib/platform.d.ts +19 -6
  32. package/dist/lib/platform.d.ts.map +1 -1
  33. package/dist/lib/platform.js +40 -9
  34. package/dist/lib/platform.js.map +1 -1
  35. package/dist/lib/ssh-client.d.ts +64 -0
  36. package/dist/lib/ssh-client.d.ts.map +1 -0
  37. package/dist/lib/ssh-client.js +128 -0
  38. package/dist/lib/ssh-client.js.map +1 -0
  39. package/dist/lib/watcher.js +4 -41
  40. package/dist/lib/watcher.js.map +1 -1
  41. package/dist/lib/wsl.d.ts +49 -0
  42. package/dist/lib/wsl.d.ts.map +1 -0
  43. package/dist/lib/wsl.js +99 -0
  44. package/dist/lib/wsl.js.map +1 -0
  45. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Claude surface classification — pure functions, unit-testable (watcher.ts
3
- * is a spawned script, so logic living there can't be imported by tests).
2
+ * Editor, AI tool and Claude surface detection — pure functions,
3
+ * unit-testable (watcher.ts is a spawned script, so logic living there can't
4
+ * be imported by tests).
4
5
  *
5
6
  * "claude" on a candidate machine is at least three different products, and
6
7
  * the old bare-word pattern labeled them all `claude` — which report readers
@@ -10,37 +11,6 @@
10
11
  * claude_web — claude.ai in a browser tab; no hook can exist
11
12
  * claude — legacy/unattributable, kept so old readers keep working
12
13
  */
13
- /**
14
- * Classifies every process-list line containing "claude" into surfaces.
15
- * `ps` is the lowercased `ps aux` / `tasklist` output from getProcessList().
16
- * A line matching no surface pattern stays legacy `claude` (unattributable —
17
- * notably Windows tasklist, where Desktop and Claude Code are both bare
18
- * `claude.exe` image names, and bystander strings like the VS Code
19
- * extension's install path in some process's args).
20
- */
21
- export function classifyClaudeProcesses(ps) {
22
- const found = new Set();
23
- for (const line of ps.split("\n")) {
24
- if (!/\bclaude\b/.test(line))
25
- continue;
26
- if (/claude\.app|claude helper/.test(line)) {
27
- // macOS Desktop app bundle and its renderer/GPU helpers
28
- found.add("claude_desktop");
29
- }
30
- else if (/claude-code[/\\]cli\.js|[/\\]claude(?:\s|$)/.test(line)) {
31
- // The npm entrypoint, or the `claude` binary itself as the executed
32
- // path. Path-anchored so the word inside a directory name (the VS Code
33
- // extension's install path, a .claude config dir) doesn't count as the
34
- // CLI running. Desktop's binary also ends in /claude but its lines
35
- // match the app-bundle branch above first.
36
- found.add("claude_code");
37
- }
38
- else {
39
- found.add("claude");
40
- }
41
- }
42
- return [...found];
43
- }
44
14
  // Owners that mean "a browser tab". First-token matching: lsof reports
45
15
  // multi-word app names ("Google Chrome Helper", "Brave Browser") but the
46
16
  // leading token identifies the browser; Safari's network traffic comes from
@@ -77,4 +47,260 @@ export function refineClaudeTool(tool, owner) {
77
47
  // drops the surface claim; api traffic keeps its API label.
78
48
  return tool === "claude_web" ? "claude" : tool;
79
49
  }
50
+ // USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND — the same eleven
51
+ // columns on macOS and procps, none of the first ten containing a space.
52
+ const PS_AUX_ROW = /^(\S+)\s+(\d+)\s+(?:\S+\s+){8}(.*)$/;
53
+ // `tasklist /fo csv /nh`: "Image Name","PID","Session Name","Session#","Mem
54
+ // Usage". CSV, because the default table cuts image names at 25 characters.
55
+ const TASKLIST_ROW = /^"([^"]+)","(\d+)","([^"]*)","(\d+)",/;
56
+ /**
57
+ * Parses getProcessList() output into rows. Header, separator and
58
+ * unrecognised lines are dropped — a line that cannot be attributed to a
59
+ * program is not evidence of one.
60
+ */
61
+ export function parseProcessRows(ps, platform) {
62
+ const rows = [];
63
+ for (const line of ps.split("\n")) {
64
+ if (platform === "win32") {
65
+ const m = line.trim().match(TASKLIST_ROW);
66
+ if (m)
67
+ rows.push({ owner: m[4], pid: Number(m[2]), command: m[1], imageOnly: true });
68
+ }
69
+ else {
70
+ const m = line.trim().match(PS_AUX_ROW);
71
+ if (m)
72
+ rows.push({ owner: m[1], pid: Number(m[2]), command: m[3], imageOnly: false });
73
+ }
74
+ }
75
+ return rows;
76
+ }
77
+ /**
78
+ * Keeps the rows owned by whoever owns `selfPid` — the watcher itself, which
79
+ * runs as the candidate. Its own row carries the owner exactly as this
80
+ * output spells it (procps truncates long user names), so no lookup of the
81
+ * account name is needed. Without that row ownership cannot be established,
82
+ * so nothing counts rather than everyone's processes.
83
+ *
84
+ * On Windows the owner is the Session#, which another account can share
85
+ * (a `runas` process in the candidate's session); getProcessList() therefore
86
+ * asks tasklist for the candidate's own processes only, and this keeps the
87
+ * session filter on top.
88
+ */
89
+ export function ownRows(rows, selfPid) {
90
+ const self = rows.find((r) => r.pid === selfPid);
91
+ return self ? rows.filter((r) => r.owner === self.owner) : [];
92
+ }
93
+ export const EDITOR_SIGNATURES = {
94
+ vscode: {
95
+ names: ["code", "code-insiders", "code - insiders", "code-oss"],
96
+ bundles: ["visual studio code", "visual studio code - insiders"],
97
+ paths: ["/.vscode-server/", "/opt/vscode-server-cache/"],
98
+ },
99
+ cursor: {
100
+ names: ["cursor", "cursor-agent"],
101
+ bundles: ["cursor"],
102
+ paths: ["/.cursor-server/", "/opt/cursor-server-cache/"],
103
+ },
104
+ vim: { names: ["vim", "gvim", "nvim", "mvim"], bundles: ["macvim"] },
105
+ intellij: { names: ["idea", "idea64"], bundles: ["intellij idea", "intellij idea ce", "intellij idea ultimate"] },
106
+ webstorm: { names: ["webstorm", "webstorm64"], bundles: ["webstorm"] },
107
+ sublime: { names: ["subl", "sublime_text"], bundles: ["sublime text"] },
108
+ emacs: { names: ["emacs", "runemacs"], bundles: ["emacs"] },
109
+ zed: { names: ["zed", "zed-editor"], bundles: ["zed", "zed preview"] },
110
+ };
111
+ export const AI_TOOL_SIGNATURES = {
112
+ aider: { names: ["aider"], modules: ["aider"] },
113
+ codeium: { names: ["codeium"], paths: ["/codeium.codeium-", "/.codeium/"] },
114
+ codex: { names: ["codex"], bundles: ["codex"], paths: ["/@openai/codex/"] },
115
+ copilot: { names: ["copilot", "copilot-language-server"], paths: ["/@github/copilot/", "/github.copilot-"] },
116
+ gemini: { names: ["gemini"], paths: ["/@google/gemini-cli/"] },
117
+ windsurf: { names: ["windsurf"], bundles: ["windsurf"] },
118
+ ollama: { names: ["ollama", "ollama app"], bundles: ["ollama"] },
119
+ tabnine: { names: ["tabnine", "tabnine-deep-local", "tabnine-deep-cloud"], paths: ["/.tabnine/"] },
120
+ supermaven: { names: ["supermaven", "sm-agent"], paths: ["/.supermaven/"] },
121
+ cline: { names: ["cline"] },
122
+ cody: { names: ["cody-agent", "sourcegraph-cody"], paths: ["/@sourcegraph/cody/"] },
123
+ "amazon-q": { names: ["amazon-q", "qchat"], bundles: ["amazon q"] },
124
+ "roo-code": { names: ["roo-code", "roo_code"] },
125
+ pearai: { names: ["pearai"], bundles: ["pearai"] },
126
+ ghostwriter: { names: ["ghostwriter"], bundles: ["ghostwriter"] },
127
+ };
128
+ // Programs that run somebody else's program: the tool is the script they
129
+ // were handed (`node /opt/homebrew/bin/codex`, `python3 -m aider`), never
130
+ // the interpreter.
131
+ const INTERPRETER = /^(?:node|nodejs|bun|deno|python[\d.]*|pythonw|sh|bash|dash|zsh)$/;
132
+ const PYTHON = /^python/;
133
+ const SHELL = /^(?:sh|bash|dash|zsh)$/;
134
+ // Interpreter options whose value is the NEXT argument, so it is not the
135
+ // script (`python3 -W ignore -m aider`, `node -r ./hook.js bin/codex`).
136
+ // Lowercase, like the command line they are matched against.
137
+ const VALUE_FLAGS = {
138
+ python: new Set(["-w", "-x", "--check-hash-based-pycs"]),
139
+ node: new Set(["-r", "--require", "--import", "--loader", "--experimental-loader", "--conditions", "--inspect-port", "--title", "--env-file"]),
140
+ shell: new Set(["-o", "+o"]),
141
+ };
142
+ // Options after which the rest is inline code, not a script.
143
+ const CODE_FLAGS = {
144
+ python: new Set(["-c"]),
145
+ node: new Set(["-e", "--eval", "-p", "--print"]),
146
+ shell: new Set(),
147
+ };
148
+ // A Linux editor binary run as node (ELECTRON_RUN_AS_NODE): only its .js
149
+ // script is a program; any other argument is a file or folder it opens.
150
+ const ELECTRON_NODE = /^(?:code|code-insiders|code-oss|cursor)$/;
151
+ // What Electron runs as node is an absolute script path. Anything else among
152
+ // its arguments is a file it opens or a piece of a flag value split at a
153
+ // space (`--user-data-dir=/users/c/library/application support/cursor`).
154
+ const ELECTRON_SCRIPT = /^\/.*\.[cm]?js$/;
155
+ // Only executable suffixes are stripped. A script keeps its source extension,
156
+ // so a candidate's own gemini.py is not the gemini CLI; packaged entrypoints
157
+ // are recognised by their install-path markers instead.
158
+ const EXECUTABLE_EXT = /\.(?:exe|cmd|bat)$/;
159
+ // Outermost bundle of an absolute macOS program path. Only the bundle's own
160
+ // name may hold spaces ("visual studio code - insiders.app"); the directories
161
+ // before it may not, so a `.app/contents/` path in an argument (`codesign
162
+ // --verify /applications/cursor.app/...`, `vim build/cursor.app/...`) is
163
+ // never the program's bundle. The one spaced directory allowed is the macOS
164
+ // per-user install parent "application support" — a deliberate allow-list of
165
+ // known install parents, not a general space allowance, which would let an
166
+ // argument path become the bundle again.
167
+ const BUNDLE = /^\/(?:(?:[^/\s]+|application support)\/)*?([^/]+)\.app\/contents\//;
168
+ // An Electron helper run as node (VS Code / Cursor extension processes):
169
+ // `<...>/x.app/contents/frameworks/x helper (plugin).app/contents/macos/x
170
+ // helper (plugin) <script>`.
171
+ const ELECTRON_HELPER = /^\/(?:(?:[^/\s]+|application support)\/)*?[^/]+\.app\/contents\/frameworks\/([^/]+ helper[^/]*)\.app\/contents\/macos\/\1(?=\s|$)(.*)$/;
172
+ function basename(path) {
173
+ return path.slice(Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1);
174
+ }
175
+ function programName(p) {
176
+ return p.kind === "exe" ? basename(p.path).replace(EXECUTABLE_EXT, "") : basename(p.path);
177
+ }
178
+ /**
179
+ * What an interpreter was handed: the first argument that is neither an
180
+ * option nor an option's value; the module after python's `-m`; the command
181
+ * after a shell's `-c`. Inline code (`python -c`, `node -e`) is no program.
182
+ */
183
+ function scriptArg(interpreter, args) {
184
+ const family = PYTHON.test(interpreter) ? "python" : SHELL.test(interpreter) ? "shell" : "node";
185
+ for (let i = 0; i < args.length; i++) {
186
+ const a = args[i];
187
+ if (family === "python" && a === "-m")
188
+ return args[i + 1] ? { path: args[i + 1], kind: "module" } : undefined;
189
+ if (family === "shell" && a === "-c")
190
+ return args[i + 1] ? { path: args[i + 1], kind: "exe" } : undefined;
191
+ if (CODE_FLAGS[family].has(a))
192
+ return undefined;
193
+ if (VALUE_FLAGS[family].has(a)) {
194
+ i++;
195
+ continue;
196
+ }
197
+ // `bun run x`, `deno run -A x`: the subcommand is not the script.
198
+ if (a === "run" && (interpreter === "bun" || interpreter === "deno"))
199
+ continue;
200
+ if (!a.startsWith("-") && !a.startsWith("+"))
201
+ return { path: a, kind: "script" };
202
+ }
203
+ return undefined;
204
+ }
205
+ /**
206
+ * What a row runs: the macOS bundle name (if any) and the programs — argv0,
207
+ * plus the script when argv0 is an interpreter or an Electron binary.
208
+ */
209
+ export function rowPrograms(row) {
210
+ const command = row.command.trim().toLowerCase();
211
+ if (row.imageOnly)
212
+ return { programs: [{ path: command, kind: "exe" }] };
213
+ const bundle = command.match(BUNDLE)?.[1];
214
+ const helper = command.match(ELECTRON_HELPER);
215
+ if (helper) {
216
+ const script = scriptArg("node", helper[2].split(/\s+/).filter(Boolean));
217
+ return { bundle, programs: script && ELECTRON_SCRIPT.test(script.path) ? [script] : [] };
218
+ }
219
+ const words = command.split(/\s+/);
220
+ // macOS per-user installs live under "Library/Application Support".
221
+ if (words[0].endsWith("/library/application") && words[1]?.startsWith("support/")) {
222
+ words.splice(0, 2, `${words[0]} ${words[1]}`);
223
+ }
224
+ const [argv0 = "", ...args] = words;
225
+ const programs = [{ path: argv0, kind: "exe" }];
226
+ const name = basename(argv0).replace(EXECUTABLE_EXT, "");
227
+ if (INTERPRETER.test(name)) {
228
+ const script = scriptArg(name, args);
229
+ if (script)
230
+ programs.push(script);
231
+ }
232
+ else if (ELECTRON_NODE.test(name)) {
233
+ const script = scriptArg("node", args);
234
+ if (script && ELECTRON_SCRIPT.test(script.path))
235
+ programs.push(script);
236
+ }
237
+ return { bundle, programs };
238
+ }
239
+ function matchesSignature(sig, bundle, programs) {
240
+ if (bundle && sig.bundles?.includes(bundle))
241
+ return true;
242
+ return programs.some((p) => {
243
+ // A module name is only a tool where that tool really runs as a module;
244
+ // a candidate's own `python3 -m codex` is their code, not the Codex CLI.
245
+ if (p.kind === "module")
246
+ return (sig.modules ?? []).includes(p.path);
247
+ return sig.names.includes(programName(p)) || (sig.paths ?? []).some((marker) => p.path.includes(marker));
248
+ });
249
+ }
250
+ const CLAUDE_CODE_PATH = "/@anthropic-ai/claude-code/";
251
+ /**
252
+ * Which Claude surface a row runs, from its program only (see the header):
253
+ * claude_desktop — the Claude.app bundle and its helpers. Desktop's own
254
+ * binary is also named `claude`, so the bundle wins.
255
+ * claude_code — an executable named `claude` (native install, npm bin,
256
+ * a shell's bare `claude`), or the npm entrypoint.
257
+ * claude — the Windows image `claude.exe`, which Desktop and
258
+ * Claude Code share, so the surface is unattributable.
259
+ * The word elsewhere in a row (a .claude config dir, an argument, the VS
260
+ * Code extension's install path) is no program.
261
+ */
262
+ function claudeSurface(row) {
263
+ const { bundle, programs } = rowPrograms(row);
264
+ if (bundle === "claude" || /^claude helper\b/.test(row.command.trim().toLowerCase()))
265
+ return "claude_desktop";
266
+ const runsClaude = programs.some((p) => p.kind !== "module" && (programName(p) === "claude" || p.path.includes(CLAUDE_CODE_PATH)));
267
+ if (!runsClaude)
268
+ return undefined;
269
+ return row.imageOnly ? "claude" : "claude_code";
270
+ }
271
+ function claudeSurfaces(rows) {
272
+ const found = new Set();
273
+ for (const row of rows) {
274
+ const surface = claudeSurface(row);
275
+ if (surface)
276
+ found.add(surface);
277
+ }
278
+ return [...found];
279
+ }
280
+ /**
281
+ * Claude surfaces running in getProcessList() output, any user's. `ps` is
282
+ * lowercased `ps aux` / `tasklist /fo csv` text, parsed per row like
283
+ * detectTools; `platform` is the OS that produced it.
284
+ */
285
+ export function classifyClaudeProcesses(ps, platform) {
286
+ return claudeSurfaces(parseProcessRows(ps, platform));
287
+ }
288
+ /**
289
+ * Editors and AI tools running as the watcher's own user. `ps` is
290
+ * getProcessList() output; `selfPid` is the watcher's pid (its own row
291
+ * names the owner to keep). Cursor counts as both an editor and an AI tool.
292
+ */
293
+ export function detectTools(ps, platform, selfPid) {
294
+ const rows = ownRows(parseProcessRows(ps, platform), selfPid);
295
+ const programs = rows.map(rowPrograms);
296
+ const running = (sigs) => Object.entries(sigs)
297
+ .filter(([, sig]) => programs.some((p) => matchesSignature(sig, p.bundle, p.programs)))
298
+ .map(([name]) => name);
299
+ const editors = running(EDITOR_SIGNATURES);
300
+ const aiTools = running(AI_TOOL_SIGNATURES);
301
+ aiTools.push(...claudeSurfaces(rows));
302
+ if (editors.includes("cursor") && !aiTools.includes("cursor"))
303
+ aiTools.push("cursor");
304
+ return { editors, aiTools };
305
+ }
80
306
  //# sourceMappingURL=detection.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"detection.js","sourceRoot":"","sources":["../../src/lib/detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,EAAU;IAChD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;IAC/B,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAQ;QACtC,IAAI,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3C,wDAAwD;YACxD,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;QAC7B,CAAC;aAAM,IAAI,6CAA6C,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACpE,oEAAoE;YACpE,uEAAuE;YACvE,uEAAuE;YACvE,mEAAmE;YACnE,2CAA2C;YAC3C,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;QAC1B,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAA;AACnB,CAAC;AAED,uEAAuE;AACvE,yEAAyE;AACzE,4EAA4E;AAC5E,+BAA+B;AAC/B,MAAM,gBAAgB,GAAG,4GAA4G,CAAA;AAErI;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAa;IAC1D,IAAI,IAAI,KAAK,YAAY,IAAI,IAAI,KAAK,YAAY;QAAE,OAAO,IAAI,CAAA;IAC/D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA,CAAC,+CAA+C;IACvE,4EAA4E;IAC5E,gCAAgC;IAChC,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAA;IACjD,2EAA2E;IAC3E,4EAA4E;IAC5E,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,aAAa,CAAA;IACxD,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,gBAAgB,CAAA;IACpD,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC7C,2EAA2E;IAC3E,0EAA0E;IAC1E,4DAA4D;IAC5D,OAAO,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA;AAChD,CAAC"}
1
+ {"version":3,"file":"detection.js","sourceRoot":"","sources":["../../src/lib/detection.ts"],"names":[],"mappings":"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"}
@@ -123,9 +123,7 @@ function isAtOrUnder(child, parent) {
123
123
 
124
124
  /**
125
125
  * Does an explicitly named assessment agree with where the session reports it
126
- * is? Agreement means either direction of containment: the session sits inside
127
- * that assessment, or the assessment sits under the session's directory (the
128
- * editor-opened-at-a-parent shape, ENG-771).
126
+ * is? Agreement means the session sits inside that assessment.
129
127
  *
130
128
  * `sessionDirs` are locations the TOOL reported (workspace roots, payload cwd)
131
129
  * — deliberately not this process's cwd. A hook runner's cwd is not the
@@ -135,21 +133,13 @@ function isAtOrUnder(child, parent) {
135
133
  * the silent Copilot Chat loss ENG-1292 fixed. When the tool reports nothing —
136
134
  * exactly the Copilot Chat case the argument exists for — the argument stands.
137
135
  *
138
- * ACCEPTED RESIDUAL — the shared-parent case. If the tool reports a workspace
139
- * that CONTAINS the named assessment, reverse containment agrees, so a config
140
- * copied into a sibling project under that same parent still resolves to the
141
- * original assessment. This is not fixable from a path comparison and not
142
- * worth narrowing for:
143
- *
144
- * - It is indistinguishable from the legitimate shape. "Editor opened at a
145
- * parent of the assessment" is ENG-771, the reason resolution looks
146
- * downward at all; when the session reports only the parent, nothing in the
147
- * payload says which child the prompt came from.
148
- * - Rejecting here changes nothing. Verified: with the assessment registered
149
- * (which `litmus init` always does), dropping the explicit path just falls
150
- * through to the registry, which returns that same assessment. The only
151
- * candidates it would affect are project-scope ones, who have no registry
152
- * entry — so tightening this trades zero privacy gain for real capture loss.
136
+ * A session reporting a PARENT of the named assessment does not agree
137
+ * (ENG-953). It used to, as the "editor opened at a parent" shape (ENG-771),
138
+ * which also let a config copied into a sibling project under that parent file
139
+ * its prompts against the original assessment. Nothing in such a payload says
140
+ * the prompt came from inside the assessment, and the registry fallback no
141
+ * longer claims parent-rooted sessions either, so neither route records work
142
+ * from outside the assessment folder.
153
143
  */
154
144
  function explicitAgreesWithSession(assessmentDir, sessionDirs) {
155
145
  const target = realpathOrNull(assessmentDir)
@@ -161,7 +151,7 @@ function explicitAgreesWithSession(assessmentDir, sessionDirs) {
161
151
  if (real && !roots.includes(real)) roots.push(real)
162
152
  }
163
153
  if (roots.length === 0) return true // tool reported no location — the blind case
164
- return roots.some(root => isAtOrUnder(root, target) || isAtOrUnder(target, root))
154
+ return roots.some(root => isAtOrUnder(root, target))
165
155
  }
166
156
 
167
157
  // A directory only counts as an assessment if it has a config.json — the
@@ -229,21 +219,29 @@ function walkUpForLitmusDir(startDir) {
229
219
  }
230
220
 
231
221
  /**
232
- * True when `child` sits strictly inside `parent`.
222
+ * The registered assessment whose tree holds `location`, as a project dir, or
223
+ * null. When registered assessments nest, the innermost one wins: it is the
224
+ * assessment the session is actually in.
233
225
  *
234
226
  * Reuses ENG-1311's `realpathOrNull` + `isAtOrUnder` rather than carrying a
235
227
  * second path comparison: realpath is what makes this work at all, since the
236
228
  * registry stores the string `litmus init` was given while payloads report the
237
229
  * tool's own cwd, and on macOS the same directory routinely differs in form
238
- * (/tmp vs /private/tmp, symlinked homes). Strict because an assessment is not
239
- * "inside itself" for the purpose of picking a registry entry — an exact match
240
- * is already handled by the walk-up above.
230
+ * (/tmp vs /private/tmp, symlinked homes).
241
231
  */
242
- function isStrictlyInside(parent, child) {
243
- const p = realpathOrNull(parent)
244
- const c = realpathOrNull(child)
245
- if (!p || !c || p === c) return false
246
- return isAtOrUnder(c, p)
232
+ function registeredAssessmentHolding(location, registered) {
233
+ const loc = realpathOrNull(location)
234
+ if (!loc) return null
235
+ let best = null
236
+ let bestReal = ""
237
+ for (const p of registered) {
238
+ const real = realpathOrNull(p)
239
+ if (real && isAtOrUnder(loc, real) && real.length > bestReal.length) {
240
+ best = p
241
+ bestReal = real
242
+ }
243
+ }
244
+ return best
247
245
  }
248
246
 
249
247
  // Find the .litmus directory — walk up from any workspace roots the tool told
@@ -252,24 +250,29 @@ function isStrictlyInside(parent, child) {
252
250
  // every tool runs the hook from the workspace (Cursor invokes hooks with
253
251
  // cwd = its own config dir and passes the workspace in `workspace_roots`).
254
252
  //
255
- // `sessionDirs` is where the payload says the session lives, and it exists to
256
- // pick the RIGHT registry entry. The fallback used to take the first valid
257
- // entry regardless, which silently misfiled capture whenever a session could
258
- // not be placed by walk-up — verified live: an editor opened at the PARENT of
259
- // an assessment, and CLI tools launched from $HOME, all had their prompts and
260
- // responses written into an unrelated assessment that merely happened to be
261
- // registered first. Walk-up only goes upward, so a parent-rooted session can
262
- // never find an assessment nested below it, which makes this the common case
263
- // rather than an edge one.
253
+ // The walk-ups are never gated: a session placed inside an assessment is
254
+ // captured whatever the registry says.
255
+ //
256
+ // The registry fallback is SCOPED (ENG-953). It answers only for a session
257
+ // that is inside a registered assessment's tree, and names that assessment.
258
+ // That is the case walk-up misses while the assessment is live: the in-repo
259
+ // config was deleted mid-session and the shadow mirror carries identity
260
+ // (ENG-1453), or the session sits deeper than the walk-up's ten levels.
261
+ //
262
+ // It used to answer for anything. With no match it returned the first
263
+ // registered assessment, so a prompt typed in an unrelated project on the same
264
+ // machine was written into the assessment and uploaded, and a later version
265
+ // also claimed any session rooted at a PARENT of an assessment ($HOME
266
+ // included). Both captured work outside the assessment folder, which the
267
+ // candidate disclosure says we do not record. A session placed nowhere now
268
+ // captures nothing.
264
269
  //
265
- // So among registered assessments we prefer one that sits INSIDE the declared
266
- // location, and of those the most recently registered — the assessment the
267
- // candidate most recently ran `litmus init` for. With a single registered
268
- // assessment (the normal case) every branch here picks the same directory, so
269
- // this only changes behavior when there is genuine ambiguity to resolve.
270
- function findLitmusDir(hintDirs = [], sessionDirs = []) {
271
- for (const hint of hintDirs) {
272
- if (typeof hint !== "string" || !hint) continue
270
+ // Where the session is: the tool's reported locations (`hintDirs`), or this
271
+ // process's cwd when the tool reported none, since a hook runner's cwd is only
272
+ // a stand-in for the session's.
273
+ function findLitmusDir(hintDirs = []) {
274
+ const reported = hintDirs.filter(d => typeof d === "string" && d)
275
+ for (const hint of reported) {
273
276
  const found = walkUpForLitmusDir(hint)
274
277
  if (found) return found
275
278
  }
@@ -282,7 +285,7 @@ function findLitmusDir(hintDirs = [], sessionDirs = []) {
282
285
  const list = JSON.parse(fs.readFileSync(registry, "utf8"))
283
286
  if (!Array.isArray(list)) return null
284
287
  const valid = list.filter(
285
- p => typeof p === "string" && (
288
+ p => typeof p === "string" && p && (
286
289
  isAssessmentLitmusDir(path.join(p, ".litmus"))
287
290
  // ENG-1453: config wiped but the assessment is alive — the dir still
288
291
  // exists and its shadow mirror supplies identity + credentials.
@@ -291,15 +294,11 @@ function findLitmusDir(hintDirs = [], sessionDirs = []) {
291
294
  )
292
295
  if (!valid.length) return null
293
296
 
294
- const declared = sessionDirs.filter(d => typeof d === "string" && d)
295
- if (declared.length) {
296
- const contained = valid.filter(p => declared.some(d => isStrictlyInside(d, p)))
297
- // Most recently registered wins; `litmus init` appends.
298
- if (contained.length) return path.join(contained[contained.length - 1], ".litmus")
297
+ const locations = reported.length ? reported : [process.cwd()]
298
+ for (const location of locations) {
299
+ const holder = registeredAssessmentHolding(location, valid)
300
+ if (holder) return path.join(holder, ".litmus")
299
301
  }
300
- // Nothing placeable — keep the long-standing behavior rather than dropping
301
- // the event, so a tool that reports no location at all still captures.
302
- return path.join(valid[0], ".litmus")
303
302
  } catch { /* no registry — give up silently */ }
304
303
  return null
305
304
  }
@@ -1676,8 +1675,8 @@ function claimPrompt(generationId) {
1676
1675
  * Where the TOOL says this session is: workspace roots (Cursor) or the
1677
1676
  * payload's own cwd (Claude Code, Codex, Copilot CLI). VS Code Copilot Chat
1678
1677
  * reports neither. Two uses: checking the explicit path (ENG-1311), and
1679
- * picking the RIGHT registry entry when walk-up finds nothing (ENG-1323 — see
1680
- * findLitmusDir).
1678
+ * deciding which registered assessment, if any, holds the session when
1679
+ * walk-up finds nothing (ENG-953, see findLitmusDir).
1681
1680
  *
1682
1681
  * Explicit hint first: it is correct by construction — but only once it
1683
1682
  * agrees with the session's own reported location, since the hook config
@@ -1687,9 +1686,7 @@ function claimPrompt(generationId) {
1687
1686
  * Those merits include every location the tool reported, cwd included. The
1688
1687
  * reported cwd beats this process's cwd (a hook runner's need not be the
1689
1688
  * session's) and is the only pointer left once a copied explicit path has
1690
- * been rejected — without it a rejected Copilot prompt, which carries no
1691
- * workspace roots, would fall through to the registry and be filed against
1692
- * an unrelated assessment.
1689
+ * been rejected.
1693
1690
  */
1694
1691
  function resolveAssessmentDir(parsed, workspaceRoots) {
1695
1692
  const sessionDirs = [
@@ -1701,7 +1698,7 @@ function resolveAssessmentDir(parsed, workspaceRoots) {
1701
1698
  const hintDirs = trustExplicit
1702
1699
  ? [explicitAssessmentDir, ...sessionDirs]
1703
1700
  : sessionDirs
1704
- return findLitmusDir(hintDirs, sessionDirs)
1701
+ return findLitmusDir(hintDirs)
1705
1702
  }
1706
1703
 
1707
1704
  async function main() {
@@ -10,15 +10,28 @@
10
10
  * a no-op on Unix; tests/unit/windows-hide.test.ts enforces it.
11
11
  */
12
12
  /**
13
- * Returns lowercase text listing all running processes.
14
- * Used for regex matching against editor/AI tool name patterns.
13
+ * Returns lowercase text listing running processes, one per line.
14
+ * Tool detection parses it into rows and keeps the watcher's own user's
15
+ * (detection.ts detectTools); never match a word against the whole text.
15
16
  *
16
- * - macOS/Linux: `ps aux` (full command lines)
17
- * - Windows: `tasklist` (image names like Code.exe, Cursor.exe)
18
- *
19
- * Output is lowercased so patterns like /\bcode\b/ match uniformly.
17
+ * - macOS/Linux: `ps aux` (full command lines, every user's)
18
+ * - Windows: `tasklist /fo csv /nh` (full image names like Code.exe,
19
+ * Cursor.exe; the default table truncates them at 25 characters), asked for
20
+ * the current user's processes only. tasklist rows carry a Session#, not
21
+ * an account, and another account can run in the candidate's session
22
+ * (`runas`), so the user filter has to happen here, and it never widens:
23
+ * see ownTasklist.
20
24
  */
21
25
  export declare function getProcessList(): string;
26
+ /**
27
+ * The first user-filtered tasklist that contains this very process (`pid`),
28
+ * trying each spelling of the account in turn (DOMAIN\user, then user).
29
+ * When none does, the filter did not match how this machine names the
30
+ * account, and ownership cannot be established: the result is empty, so
31
+ * nothing counts. It never falls back to an unfiltered or session-scoped
32
+ * list, which would badge another account's processes.
33
+ */
34
+ export declare function ownTasklist(run: (account: string) => string, accounts: string[], pid: number): string;
22
35
  /**
23
36
  * Lists active network connections asynchronously.
24
37
  * Calls cb(err, stdout) with raw connection output.
@@ -1 +1 @@
1
- {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/lib/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAGvC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAarF;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAqBtE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAkBrF"}
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/lib/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAavC;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQrG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAarF;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAqBtE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAkBrF"}
@@ -9,21 +9,52 @@
9
9
  * see terminal windows flash on every detection cycle (ENG-2010). The flag is
10
10
  * a no-op on Unix; tests/unit/windows-hide.test.ts enforces it.
11
11
  */
12
- import { execSync, execFile } from "child_process";
12
+ import { execSync, execFile, execFileSync } from "child_process";
13
+ import { userInfo } from "os";
13
14
  const isWin = process.platform === "win32";
14
15
  const isLinux = process.platform === "linux";
15
16
  /**
16
- * Returns lowercase text listing all running processes.
17
- * Used for regex matching against editor/AI tool name patterns.
17
+ * Returns lowercase text listing running processes, one per line.
18
+ * Tool detection parses it into rows and keeps the watcher's own user's
19
+ * (detection.ts detectTools); never match a word against the whole text.
18
20
  *
19
- * - macOS/Linux: `ps aux` (full command lines)
20
- * - Windows: `tasklist` (image names like Code.exe, Cursor.exe)
21
- *
22
- * Output is lowercased so patterns like /\bcode\b/ match uniformly.
21
+ * - macOS/Linux: `ps aux` (full command lines, every user's)
22
+ * - Windows: `tasklist /fo csv /nh` (full image names like Code.exe,
23
+ * Cursor.exe; the default table truncates them at 25 characters), asked for
24
+ * the current user's processes only. tasklist rows carry a Session#, not
25
+ * an account, and another account can run in the candidate's session
26
+ * (`runas`), so the user filter has to happen here, and it never widens:
27
+ * see ownTasklist.
23
28
  */
24
29
  export function getProcessList() {
25
- const cmd = isWin ? "tasklist" : "ps aux";
26
- return execSync(cmd, { timeout: 5000, encoding: "utf8", windowsHide: true }).toLowerCase();
30
+ if (!isWin)
31
+ return execSync("ps aux", { timeout: 5000, encoding: "utf8", windowsHide: true }).toLowerCase();
32
+ const user = userInfo().username;
33
+ const domain = process.env.USERDOMAIN;
34
+ return ownTasklist((account) => execFileSync("tasklist", ["/fo", "csv", "/nh", "/fi", `USERNAME eq ${account}`], {
35
+ timeout: 5000,
36
+ encoding: "utf8",
37
+ windowsHide: true,
38
+ }), domain ? [`${domain}\\${user}`, user] : [user], process.pid);
39
+ }
40
+ /**
41
+ * The first user-filtered tasklist that contains this very process (`pid`),
42
+ * trying each spelling of the account in turn (DOMAIN\user, then user).
43
+ * When none does, the filter did not match how this machine names the
44
+ * account, and ownership cannot be established: the result is empty, so
45
+ * nothing counts. It never falls back to an unfiltered or session-scoped
46
+ * list, which would badge another account's processes.
47
+ */
48
+ export function ownTasklist(run, accounts, pid) {
49
+ for (const account of accounts) {
50
+ try {
51
+ const out = run(account).toLowerCase();
52
+ if (out.includes(`","${pid}","`))
53
+ return out;
54
+ }
55
+ catch { /* try the next spelling */ }
56
+ }
57
+ return "";
27
58
  }
28
59
  /**
29
60
  * Lists active network connections asynchronously.