litmus-cli 1.4.35 → 1.4.36

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 (37) hide show
  1. package/README.md +28 -2
  2. package/dist/commands/connect.d.ts +43 -4
  3. package/dist/commands/connect.d.ts.map +1 -1
  4. package/dist/commands/connect.js +91 -11
  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/watcher.js +4 -41
  36. package/dist/lib/watcher.js.map +1 -1
  37. package/package.json +1 -1
@@ -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.
@@ -1 +1 @@
1
- {"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/lib/platform.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/lib/platform.ts"],"names":[],"mappings":"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"}
@@ -36,7 +36,7 @@ import { setupAiTracking } from "../commands/init.js";
36
36
  import { writeShadowChainCursor, readShadowChainCursor, writeShadowTrackerFile, writeShadowReviveInfo, readShadowConfigById, findShadowConfigForDir, deleteShadowForAssessment, writeRepairFile, removeRepairFile, REPAIR_FILE_NAME, } from "./shadow.js";
37
37
  import { createKeepAliveGuard } from "./keepalive.js";
38
38
  import { getProcessList, getConnections, extractConnection, resolvePidNames } from "./platform.js";
39
- import { classifyClaudeProcesses, refineClaudeTool } from "./detection.js";
39
+ import { detectTools, refineClaudeTool } from "./detection.js";
40
40
  import { decideNudge, loadNudgeState, saveNudgeState, initialPromptCursor, scanPromptCount, writeAiNoticeFile, removeAiNoticeFile, nudgeNotificationCopy, isCliNoticeArtifact, AI_NOTICE_FILE_NAME, } from "./capture-nudge.js";
41
41
  import { desktopNotify } from "./notify.js";
42
42
  // ENG-1491: an uncaught throw no longer always kills the watcher — on a
@@ -999,34 +999,6 @@ function checkGitCommits() {
999
999
  catch { /* git not available or command failed — skip */ }
1000
1000
  }
1001
1001
  setInterval(checkGitCommits, 30 * 1000); // Every 30 seconds
1002
- // ── Editor & AI tool detection ───────────────────────────────────
1003
- const EDITOR_PATTERNS = {
1004
- vscode: /\bcode\b/,
1005
- cursor: /\bcursor\b/,
1006
- vim: /\b[gn]?vim\b/,
1007
- intellij: /\bidea\b/,
1008
- webstorm: /\bwebstorm\b/,
1009
- sublime: /\bsubl\b/,
1010
- emacs: /\bemacs\b/,
1011
- zed: /\bzed\b/,
1012
- };
1013
- const AI_TOOL_PATTERNS = {
1014
- aider: /\baider\b/,
1015
- codeium: /\bcodeium\b/,
1016
- codex: /\bcodex\b/,
1017
- copilot: /\bcopilot\b/,
1018
- gemini: /\bgemini\b/,
1019
- windsurf: /\bwindsurf\b/,
1020
- ollama: /\bollama\b/,
1021
- tabnine: /\btabnine\b/,
1022
- supermaven: /\bsupermaven\b/,
1023
- cline: /\bcline\b/,
1024
- cody: /\bsourcegraph-cody\b|\bcody-agent\b/,
1025
- "amazon-q": /\bamazon-q\b/,
1026
- "roo-code": /\broo-code\b|\broo_code\b/,
1027
- pearai: /\bpearai\b/,
1028
- ghostwriter: /\bghostwriter\b/,
1029
- };
1030
1002
  // ── Capture nudge (ENG-1857) ─────────────────────────────────────
1031
1003
  // A hook-capturable AI tool running with ZERO captured prompts is the silent
1032
1004
  // Codex/Copilot trust-gate signature: the candidate's AI work is invisibly
@@ -1091,18 +1063,9 @@ function runCaptureNudge(aiTools) {
1091
1063
  }
1092
1064
  function detectEnvironment() {
1093
1065
  try {
1094
- const ps = getProcessList();
1095
- const editors = Object.entries(EDITOR_PATTERNS)
1096
- .filter(([, re]) => re.test(ps))
1097
- .map(([name]) => name);
1098
- const aiTools = Object.entries(AI_TOOL_PATTERNS)
1099
- .filter(([, re]) => re.test(ps))
1100
- .map(([name]) => name);
1101
- aiTools.push(...classifyClaudeProcesses(ps));
1102
- // Cursor is both an editor and an AI tool
1103
- if (editors.includes("cursor") && !aiTools.includes("cursor")) {
1104
- aiTools.push("cursor");
1105
- }
1066
+ // Per process row, own user only, matched on the program — see
1067
+ // detectTools (ENG-1273).
1068
+ const { editors, aiTools } = detectTools(getProcessList(), process.platform, process.pid);
1106
1069
  if (editors.length > 0 || aiTools.length > 0) {
1107
1070
  const event = {
1108
1071
  ts: new Date().toISOString(),