litmus-cli 1.4.92 → 1.4.94

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 (95) hide show
  1. package/README.md +48 -0
  2. package/dist/commands/connect.d.ts +6 -2
  3. package/dist/commands/connect.d.ts.map +1 -1
  4. package/dist/commands/connect.js +222 -35
  5. package/dist/commands/connect.js.map +1 -1
  6. package/dist/commands/forward.d.ts +7 -1
  7. package/dist/commands/forward.d.ts.map +1 -1
  8. package/dist/commands/forward.js +8 -7
  9. package/dist/commands/forward.js.map +1 -1
  10. package/dist/commands/init.d.ts.map +1 -1
  11. package/dist/commands/init.js +12 -2
  12. package/dist/commands/init.js.map +1 -1
  13. package/dist/lib/ai-tracking.d.ts +8 -0
  14. package/dist/lib/ai-tracking.d.ts.map +1 -1
  15. package/dist/lib/ai-tracking.js +96 -1
  16. package/dist/lib/ai-tracking.js.map +1 -1
  17. package/dist/lib/connect-skills.d.ts +1 -0
  18. package/dist/lib/connect-skills.d.ts.map +1 -1
  19. package/dist/lib/connect-skills.js +1 -1
  20. package/dist/lib/connect-skills.js.map +1 -1
  21. package/dist/lib/desktop-readiness.d.ts +9 -0
  22. package/dist/lib/desktop-readiness.d.ts.map +1 -0
  23. package/dist/lib/desktop-readiness.js +131 -0
  24. package/dist/lib/desktop-readiness.js.map +1 -0
  25. package/dist/lib/editor-foreground.d.ts +6 -0
  26. package/dist/lib/editor-foreground.d.ts.map +1 -0
  27. package/dist/lib/editor-foreground.js +38 -0
  28. package/dist/lib/editor-foreground.js.map +1 -0
  29. package/dist/lib/environment-checks.d.ts +2 -0
  30. package/dist/lib/environment-checks.d.ts.map +1 -1
  31. package/dist/lib/environment-checks.js +4 -2
  32. package/dist/lib/environment-checks.js.map +1 -1
  33. package/dist/lib/file-monitor.d.ts +18 -0
  34. package/dist/lib/file-monitor.d.ts.map +1 -0
  35. package/dist/lib/file-monitor.js +87 -0
  36. package/dist/lib/file-monitor.js.map +1 -0
  37. package/dist/lib/hosted-capture.d.ts +1 -1
  38. package/dist/lib/hosted-capture.d.ts.map +1 -1
  39. package/dist/lib/hosted-capture.js +22 -12
  40. package/dist/lib/hosted-capture.js.map +1 -1
  41. package/dist/lib/init-location.d.ts +4 -0
  42. package/dist/lib/init-location.d.ts.map +1 -0
  43. package/dist/lib/init-location.js +36 -0
  44. package/dist/lib/init-location.js.map +1 -0
  45. package/dist/lib/installed-tools.d.ts +29 -0
  46. package/dist/lib/installed-tools.d.ts.map +1 -0
  47. package/dist/lib/installed-tools.js +159 -0
  48. package/dist/lib/installed-tools.js.map +1 -0
  49. package/dist/lib/portal-launcher-background.d.ts +13 -0
  50. package/dist/lib/portal-launcher-background.d.ts.map +1 -0
  51. package/dist/lib/portal-launcher-background.js +51 -0
  52. package/dist/lib/portal-launcher-background.js.map +1 -0
  53. package/dist/lib/portal-launcher-worker.d.ts +2 -0
  54. package/dist/lib/portal-launcher-worker.d.ts.map +1 -0
  55. package/dist/lib/portal-launcher-worker.js +43 -0
  56. package/dist/lib/portal-launcher-worker.js.map +1 -0
  57. package/dist/lib/portal-launcher.d.ts +43 -0
  58. package/dist/lib/portal-launcher.d.ts.map +1 -0
  59. package/dist/lib/portal-launcher.js +408 -0
  60. package/dist/lib/portal-launcher.js.map +1 -0
  61. package/dist/lib/prepared-desktop.d.ts +7 -0
  62. package/dist/lib/prepared-desktop.d.ts.map +1 -0
  63. package/dist/lib/prepared-desktop.js +35 -0
  64. package/dist/lib/prepared-desktop.js.map +1 -0
  65. package/dist/lib/prepared-editor.d.ts +26 -0
  66. package/dist/lib/prepared-editor.d.ts.map +1 -0
  67. package/dist/lib/prepared-editor.js +150 -0
  68. package/dist/lib/prepared-editor.js.map +1 -0
  69. package/dist/lib/schemas.d.ts +1 -0
  70. package/dist/lib/schemas.d.ts.map +1 -1
  71. package/dist/lib/schemas.js +2 -0
  72. package/dist/lib/schemas.js.map +1 -1
  73. package/dist/lib/ssh-client.d.ts +12 -0
  74. package/dist/lib/ssh-client.d.ts.map +1 -1
  75. package/dist/lib/ssh-client.js +1 -1
  76. package/dist/lib/ssh-client.js.map +1 -1
  77. package/dist/lib/ssh-config-file.d.ts +3 -0
  78. package/dist/lib/ssh-config-file.d.ts.map +1 -1
  79. package/dist/lib/ssh-config-file.js +42 -0
  80. package/dist/lib/ssh-config-file.js.map +1 -1
  81. package/dist/lib/tracker.d.ts +1 -0
  82. package/dist/lib/tracker.d.ts.map +1 -1
  83. package/dist/lib/tracker.js +4 -2
  84. package/dist/lib/tracker.js.map +1 -1
  85. package/dist/lib/vscode-server-preparation.d.ts +7 -0
  86. package/dist/lib/vscode-server-preparation.d.ts.map +1 -0
  87. package/dist/lib/vscode-server-preparation.js +78 -0
  88. package/dist/lib/vscode-server-preparation.js.map +1 -0
  89. package/dist/lib/watcher.js +98 -77
  90. package/dist/lib/watcher.js.map +1 -1
  91. package/dist/lib/workspace-start-wait.d.ts +23 -0
  92. package/dist/lib/workspace-start-wait.d.ts.map +1 -0
  93. package/dist/lib/workspace-start-wait.js +50 -0
  94. package/dist/lib/workspace-start-wait.js.map +1 -0
  95. package/package.json +1 -1
@@ -0,0 +1,78 @@
1
+ import { spawn } from "child_process";
2
+ import { configSetsRemoteCommand } from "./ssh-client.js";
3
+ /** Prepare the exact client build before advertising the laptop launcher as ready.
4
+ * Uses the workspace's existing proxy; never downloads code to the laptop. Existing
5
+ * editor installations are left alone, including a concurrent Remote-SSH install.
6
+ */
7
+ export function vscodePreparationScript(commit) {
8
+ if (!/^[a-f0-9]{40}$/i.test(commit))
9
+ throw new Error("Invalid editor commit");
10
+ return `import json,pathlib,subprocess,tarfile,tempfile,os,fcntl
11
+ commit=${JSON.stringify(commit)}
12
+ home=pathlib.Path.home()
13
+ root=home/".vscode-server"
14
+ root.mkdir(exist_ok=True)
15
+ def valid(p):
16
+ try:
17
+ return (p/"bin/code-server").is_file() and (p/"node").is_file() and json.loads((p/"product.json").read_text()).get("commit")==commit
18
+ except (OSError,ValueError): return False
19
+ with (root/".litmus-prepare.lock").open("a") as lock:
20
+ fcntl.flock(lock,fcntl.LOCK_EX)
21
+ destination=root/"cli/servers"/("Stable-"+commit)/"server"
22
+ if valid(destination):
23
+ print("ready")
24
+ raise SystemExit(0)
25
+ if destination.exists() or destination.is_symlink(): raise SystemExit(1)
26
+ cache=root/".litmus-cache"
27
+ cache.mkdir(exist_ok=True)
28
+ target=cache/commit
29
+ if not valid(target):
30
+ if target.exists(): raise SystemExit(1)
31
+ with tempfile.TemporaryDirectory(prefix="prepare-",dir=cache) as temporary:
32
+ temporary=pathlib.Path(temporary)
33
+ archive=temporary/"server.tar.gz"
34
+ subprocess.run(["curl","-fsSL","--connect-timeout","10","--max-time","75","-o",str(archive),"https://update.code.visualstudio.com/commit:"+commit+"/server-linux-x64/stable"],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
35
+ extracted=temporary/"unpacked"
36
+ extracted.mkdir()
37
+ with tarfile.open(archive) as tar:
38
+ tar.extractall(extracted,filter="data")
39
+ children=list(extracted.iterdir())
40
+ if len(children)!=1 or not valid(children[0]): raise SystemExit(1)
41
+ subprocess.run([str(children[0]/"node"),"--version"],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=5)
42
+ os.rename(children[0],target)
43
+ destination.parent.mkdir(parents=True,exist_ok=True)
44
+ try: destination.symlink_to(target)
45
+ except FileExistsError: pass
46
+ if not valid(destination): raise SystemExit(1)
47
+ print("ready")
48
+ `;
49
+ }
50
+ export function prepareVscodeServer(alias, commit, signal) {
51
+ if (!/^litmus-[a-zA-Z0-9-]+$/.test(alias) || !/^[a-f0-9]{40}$/i.test(commit) || signal.aborted)
52
+ return Promise.resolve(false);
53
+ return new Promise(resolve => {
54
+ const args = ["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "PermitLocalCommand=no"];
55
+ if (configSetsRemoteCommand(alias))
56
+ args.push("-o", "RemoteCommand=none");
57
+ args.push(alias, "python3 -");
58
+ const child = spawn("ssh", args, { stdio: ["pipe", "pipe", "ignore"] });
59
+ let output = "", settled = false;
60
+ const done = (ok) => {
61
+ if (settled)
62
+ return;
63
+ settled = true;
64
+ clearTimeout(timer);
65
+ signal.removeEventListener("abort", abort);
66
+ resolve(ok);
67
+ };
68
+ const abort = () => { child.kill(); done(false); };
69
+ const timer = setTimeout(abort, 100000);
70
+ signal.addEventListener("abort", abort, { once: true });
71
+ child.stdout.on("data", data => { output = (output + String(data)).slice(-1024); });
72
+ child.stdin.on("error", () => { });
73
+ child.once("error", () => done(false));
74
+ child.once("exit", code => done(code === 0 && output.trim() === "ready"));
75
+ child.stdin.end(vscodePreparationScript(commit));
76
+ });
77
+ }
78
+ //# sourceMappingURL=vscode-server-preparation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vscode-server-preparation.js","sourceRoot":"","sources":["../../src/lib/vscode-server-preparation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AACrC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAA;AAEzD;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAc;IACpD,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAA;IAC7E,OAAO;SACA,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqC9B,CAAA;AACD,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAa,EAAE,MAAc,EAAE,MAAmB;IACpF,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IAC7H,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;QAC3B,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,uBAAuB,CAAC,CAAA;QAC9F,IAAI,uBAAuB,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAA;QACzE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,CAAA;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,EAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAC,CAAC,CAAA;QACrE,IAAI,MAAM,GAAG,EAAE,EAAE,OAAO,GAAG,KAAK,CAAA;QAChC,MAAM,IAAI,GAAG,CAAC,EAAW,EAAE,EAAE;YAC3B,IAAI,OAAO;gBAAE,OAAM;YACnB,OAAO,GAAG,IAAI,CAAA;YACd,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;YAC1C,OAAO,CAAC,EAAE,CAAC,CAAA;QACb,CAAC,CAAA;QACD,MAAM,KAAK,GAAG,GAAG,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA,CAAC,CAAC,CAAA;QACjD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,EAAE,MAAO,CAAC,CAAA;QACxC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,EAAC,IAAI,EAAE,IAAI,EAAC,CAAC,CAAA;QACrD,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;QAClF,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QACjC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;QACtC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,OAAO,CAAC,CAAC,CAAA;QACzE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC,CAAA;IAClD,CAAC,CAAC,CAAA;AACJ,CAAC"}
@@ -25,7 +25,7 @@ import { execSync, execFile } from "child_process";
25
25
  import dns from "dns";
26
26
  import https from "https";
27
27
  import http from "http";
28
- import { watch as chokidarWatch } from "chokidar";
28
+ import { startFileMonitor, fileMonitorPolicy } from "./file-monitor.js";
29
29
  import { getEffectiveDeadline, writeConfig, validateRebuiltTimes, computeTimeSync } from "./config.js";
30
30
  import { CLI_VERSION } from "./version.js";
31
31
  import { armDeadlinePoll, createAutoSubmitController } from "./deadline.js";
@@ -40,7 +40,7 @@ import { readActivityLogTail } from "./activity-tail.js";
40
40
  import { setupAiTracking } from "../commands/init.js";
41
41
  import { writeShadowChainCursor, readShadowChainCursor, writeShadowTrackerFile, writeShadowReviveInfo, readShadowConfigById, findShadowConfigForDir, deleteShadowForAssessment, writeRepairFile, removeRepairFile, REPAIR_FILE_NAME, } from "./shadow.js";
42
42
  import { createKeepAliveGuard } from "./keepalive.js";
43
- import { WATCHER_READY_GRACE_MS } from "./tracker.js";
43
+ import { WATCHER_READY_GRACE_MS, WATCHER_BOOT_TIMEOUT_MS } from "./tracker.js";
44
44
  import { getProcessListAsync, probeUnfilteredTasklist, getConnections, extractConnection, resolvePidNames } from "./platform.js";
45
45
  import { detectTools, refineClaudeTool } from "./detection.js";
46
46
  import { decideNudge, loadNudgeState, saveNudgeState, scanAssessmentCapture, writeAiNoticeFile, removeAiNoticeFile, nudgeNotificationCopy, isCliNoticeArtifact, AI_NOTICE_FILE_NAME, } from "./capture-nudge.js";
@@ -79,7 +79,7 @@ process.on("unhandledRejection", (reason) => {
79
79
  });
80
80
  let [, , projectDir, activityLogPath, cliBinPath, noncePath] = process.argv;
81
81
  process.stderr.write(`[watcher] starting: projectDir=${projectDir} log=${activityLogPath}\n`);
82
- process.stderr.write(`[watcher] node ${process.version}, platform=${process.platform}, arch=${process.arch}\n`);
82
+ process.stderr.write(`[watcher] node ${process.version}, platform=${process.platform}, arch=${process.arch}, kernel=${os.release()}\n`);
83
83
  // Touch the nonce file on startup so tracker.ts can distinguish "our watcher
84
84
  // is alive" from "a recycled PID happens to match an old tracker.pid".
85
85
  // Heartbeat updates the mtime; tracker.ts compares against NONCE_MAX_AGE_MS.
@@ -166,15 +166,28 @@ function claimantIsAlive(owner) {
166
166
  // reclaim from it just like a corpse. The env override exists for tests only.
167
167
  // The default is tracker.ts's, so it cannot fall below init's boot wait (ENG-3194).
168
168
  let readyFilePath = path.join(path.dirname(activityLogPath), "tracker.ready");
169
+ let fileCaptureVerified = false;
170
+ let chainStartTimer;
169
171
  const READY_GRACE_MS = Number(process.env.LITMUS_WATCHER_READY_GRACE_MS) || WATCHER_READY_GRACE_MS;
170
172
  function markReady() {
173
+ if (!fileCaptureVerified)
174
+ return false;
171
175
  try {
172
176
  fs.writeFileSync(readyFilePath, String(process.pid), "utf8");
177
+ // A successor may take time to verify files. Its incumbent keeps writing
178
+ // until this stamp appears, so the handoff delay starts NOW, not at spawn.
179
+ if (!chainStartTimer && preChainBuffer !== null && !chainAbandoned) {
180
+ chainStartTimer = setTimeout(startChain, CHAIN_START_DELAY_MS);
181
+ }
182
+ return true;
173
183
  }
174
184
  catch (err) {
175
- // Non-fatal: we keep watching either way. Worst case another watcher
176
- // eventually reclaims the pid file from us; the project stays tracked.
177
185
  process.stderr.write(`[watcher] could not write ready marker: ${err}\n`);
186
+ // Without the stamp an incumbent cannot yield and chain writes cannot
187
+ // start. Flush the unchained startup buffer and retire, rather than hold
188
+ // the rest of the session in memory forever.
189
+ shutdown({ preserveSentinel: true });
190
+ return false;
178
191
  }
179
192
  }
180
193
  function claimantIsReady(owner) {
@@ -519,6 +532,18 @@ const IGNORED = [
519
532
  const noticeOwnership = new Map();
520
533
  isCliNoticeArtifact(projectDir, AI_NOTICE_FILE_NAME, noticeOwnership);
521
534
  isCliNoticeArtifact(projectDir, REPAIR_FILE_NAME, noticeOwnership);
535
+ const AI_ARTIFACT_PATTERNS = [
536
+ { tool: "cursor", paths: [".cursorrules", ".cursorignore"] },
537
+ { tool: "windsurf", paths: [".windsurfrules"] },
538
+ { tool: "aider", paths: [".aider.chat.history.md", ".aider.input.history", ".aider.tags.cache"] },
539
+ { tool: "continue", paths: [".continue"] },
540
+ { tool: "codeium", paths: [".codeium"] },
541
+ { tool: "tabnine", paths: [".tabnine"] },
542
+ { tool: "codex", paths: ["codex.md", ".codex"] },
543
+ ];
544
+ function isAiArtifactPath(relative) {
545
+ return AI_ARTIFACT_PATTERNS.some(({ paths }) => paths.some(p => relative === p || relative.startsWith(p + "/") || relative.startsWith(p + "\\")));
546
+ }
522
547
  function shouldIgnore(relPath) {
523
548
  return isCliNoticeArtifact(projectDir, relPath, noticeOwnership) || IGNORED.some((re) => re.test(relPath));
524
549
  }
@@ -607,7 +632,6 @@ function startChain() {
607
632
  for (const event of queued)
608
633
  writeChained(event);
609
634
  }
610
- setTimeout(startChain, CHAIN_START_DELAY_MS);
611
635
  /**
612
636
  * Shutdown inside the chain-start window: the incumbent may still own the
613
637
  * chain tail, so snapshotting it here would recreate the fork the deferral
@@ -712,63 +736,54 @@ function handleFileEvent(eventType, filename) {
712
736
  }, 500));
713
737
  }
714
738
  let fileWatcher;
739
+ let fileWatchingReady;
715
740
  function startFileWatching() {
716
- if (process.platform === "win32") {
717
- // Node's fs.watch(dir, { recursive: true }) on Windows uses
718
- // ReadDirectoryChangesW. On windows-2025-vs2026 runners with Node 24 this
719
- // path calls __fastfail() from native code after watcher init completes,
720
- // aborting the process with exit code 0xC0000409 and bypassing every
721
- // JS-level exit hook (uncaughtException, beforeExit, exit). Confirmed by
722
- // an isolation test that gated fs.watch behind an env var and showed the
723
- // watcher healthy on the same runner. Until either Node or libuv fixes
724
- // the recursive watch on Windows 2025, use chokidar's polling mode here.
725
- try {
726
- const watcher = chokidarWatch(projectDir, {
727
- ignoreInitial: true,
728
- followSymlinks: false,
729
- usePolling: true,
730
- interval: 500,
731
- binaryInterval: 1000,
732
- ignored: (p) => {
733
- const rel = path.relative(projectDir, p);
734
- // Don't ignore the root dir itself, or chokidar reports nothing.
735
- if (!rel)
736
- return false;
737
- return shouldIgnore(rel);
741
+ fileCaptureVerified = false;
742
+ const startedAt = Date.now();
743
+ const { polling } = fileMonitorPolicy();
744
+ const modes = polling ? [true] : [false, true];
745
+ fileWatchingReady = (async () => {
746
+ for (let attempt = 0; attempt < modes.length; attempt++) {
747
+ let failure;
748
+ process.stderr.write(`[watcher] file watcher starting (${modes[attempt] ? "polling" : "native"}, filtered discovery)\n`);
749
+ const monitor = startFileMonitor({
750
+ directory: projectDir,
751
+ ignored: shouldIgnore,
752
+ observed: isAiArtifactPath,
753
+ onEvent: handleFileEvent,
754
+ polling: modes[attempt],
755
+ timeoutMs: Math.floor((WATCHER_BOOT_TIMEOUT_MS - 1000) / modes.length),
756
+ onError: (error) => {
757
+ failure = error;
758
+ fileCaptureVerified = false;
759
+ process.stderr.write(`[watcher] file capture unavailable: ${error.message}\n`);
760
+ try {
761
+ if (fs.readFileSync(readyFilePath, "utf8").trim() === String(process.pid))
762
+ fs.unlinkSync(readyFilePath);
763
+ }
764
+ catch { /* no ready marker */ }
738
765
  },
739
766
  });
740
- fileWatcher = watcher;
741
- watcher.on("add", (p) => handleFileEvent("rename", path.relative(projectDir, p)));
742
- watcher.on("change", (p) => handleFileEvent("change", path.relative(projectDir, p)));
743
- watcher.on("unlink", (p) => handleFileEvent("rename", path.relative(projectDir, p)));
744
- watcher.on("error", (err) => {
745
- process.stderr.write(`[watcher] chokidar error: ${err}\n`);
746
- });
747
- process.stderr.write("[watcher] file watcher started (chokidar polling)\n");
748
- }
749
- catch (err) {
750
- process.stderr.write(`[watcher] chokidar setup failed: ${err.message} (heartbeat + git tracking still active)\n`);
751
- }
752
- }
753
- else {
754
- try {
755
- const watcher = fs.watch(projectDir, { recursive: true }, (eventType, filename) => {
756
- if (!filename)
757
- return;
758
- handleFileEvent(eventType, filename);
759
- });
760
- fileWatcher = watcher;
761
- watcher.on("error", (err) => {
762
- process.stderr.write(`[watcher] fs.watch error: ${err}\n`);
763
- });
764
- process.stderr.write("[watcher] file watcher started\n");
765
- }
766
- catch (err) {
767
- // fs.watch with recursive: true is not supported on Linux < Node 22.
768
- // Continue without file watching — heartbeats and git commit polling still work.
769
- process.stderr.write(`[watcher] file watching unavailable: ${err.message} (heartbeat + git tracking still active)\n`);
767
+ fileWatcher = monitor;
768
+ try {
769
+ await monitor.ready;
770
+ if (failure)
771
+ throw failure;
772
+ if (fileWatcher !== monitor || shuttingDown)
773
+ throw new Error("File watcher replaced or stopped during verification");
774
+ fileCaptureVerified = true;
775
+ process.stderr.write(`[watcher] file capture verified in ${Date.now() - startedAt}ms\n`);
776
+ return;
777
+ }
778
+ catch (error) {
779
+ await monitor.close();
780
+ if (shuttingDown || fileWatcher !== monitor || attempt === modes.length - 1)
781
+ throw error;
782
+ process.stderr.write("[watcher] native file probe failed; retrying with polling\n");
783
+ }
770
784
  }
771
- }
785
+ })();
786
+ void fileWatchingReady.catch(() => { });
772
787
  }
773
788
  startFileWatching();
774
789
  // ── Config presence check ────────────────────────────────────────
@@ -1274,15 +1289,6 @@ async function detectEnvironment() {
1274
1289
  setTimeout(() => { void detectEnvironment(); }, 0);
1275
1290
  setInterval(() => { void detectEnvironment(); }, 10 * 60 * 1000); // Every 10 minutes
1276
1291
  // ── AI artifact file detection ──────────────────────────────────
1277
- const AI_ARTIFACT_PATTERNS = [
1278
- { tool: "cursor", paths: [".cursorrules", ".cursorignore"] },
1279
- { tool: "windsurf", paths: [".windsurfrules"] },
1280
- { tool: "aider", paths: [".aider.chat.history.md", ".aider.input.history", ".aider.tags.cache"] },
1281
- { tool: "continue", paths: [".continue"] },
1282
- { tool: "codeium", paths: [".codeium"] },
1283
- { tool: "tabnine", paths: [".tabnine"] },
1284
- { tool: "codex", paths: ["codex.md", ".codex"] },
1285
- ];
1286
1292
  const seenArtifacts = new Set();
1287
1293
  function checkArtifact(relPath) {
1288
1294
  if (seenArtifacts.has(relPath))
@@ -1538,8 +1544,13 @@ function reportProjectRootProblem() {
1538
1544
  readyFilePath = path.join(path.dirname(activityLogPath), "tracker.ready");
1539
1545
  watchedConfigPath = path.join(projectDir, ".litmus", "config.json");
1540
1546
  shutdownFile = path.join(projectDir, ".litmus", "shutdown");
1541
- void Promise.resolve(fileWatcher?.close()).then(() => { if (!shuttingDown)
1542
- startFileWatching(); }).catch(() => {
1547
+ void Promise.resolve(fileWatcher?.close()).then(() => {
1548
+ if (!shuttingDown) {
1549
+ startFileWatching();
1550
+ void fileWatchingReady.then(() => { if (!shuttingDown)
1551
+ markReady(); }, () => { });
1552
+ }
1553
+ }).catch(() => {
1543
1554
  process.stderr.write("[watcher] file watcher could not restart after folder move\n");
1544
1555
  });
1545
1556
  addActiveAssessment(projectDir);
@@ -1758,7 +1769,7 @@ if (cliBinPath && litmusConfig) {
1758
1769
  // ── Graceful shutdown ────────────────────────────────────────────
1759
1770
  let shuttingDown = false;
1760
1771
  let shutdownFile = path.join(projectDir, ".litmus", "shutdown");
1761
- function shutdown() {
1772
+ function shutdown(options = {}) {
1762
1773
  if (shuttingDown)
1763
1774
  return;
1764
1775
  shuttingDown = true;
@@ -1772,10 +1783,12 @@ function shutdown() {
1772
1783
  // them unchained instead; a fork is structurally impossible that way.
1773
1784
  abandonChainAndDrain();
1774
1785
  // Clean up sentinel file if it exists
1775
- try {
1776
- fs.unlinkSync(shutdownFile);
1786
+ if (!options.preserveSentinel) {
1787
+ try {
1788
+ fs.unlinkSync(shutdownFile);
1789
+ }
1790
+ catch { /* may not exist */ }
1777
1791
  }
1778
- catch { /* may not exist */ }
1779
1792
  // Drop our readiness stamp so it can't outlive us and vouch for whatever
1780
1793
  // process later recycles our pid (see claimantIsReady). Only ours — a
1781
1794
  // successor's stamp is not ours to remove.
@@ -1806,7 +1819,7 @@ process.on("SIGINT", shutdown);
1806
1819
  const sentinelPoll = setInterval(() => {
1807
1820
  if (fs.existsSync(shutdownFile)) {
1808
1821
  process.stderr.write("[watcher] shutdown sentinel detected\n");
1809
- shutdown();
1822
+ shutdown({ preserveSentinel: !fileCaptureVerified });
1810
1823
  return;
1811
1824
  }
1812
1825
  // Recurring half of the singleton claim — see the note beside
@@ -1819,6 +1832,14 @@ const sentinelPoll = setInterval(() => {
1819
1832
  sentinelPoll.unref(); // Don't keep process alive just for this
1820
1833
  // Everything is running — publish readiness so an incumbent knows this
1821
1834
  // process is a working watcher, not a wedged claimant (see markReady).
1822
- markReady();
1823
- process.stderr.write("[watcher] all monitors started\n");
1835
+ void fileWatchingReady.then(() => {
1836
+ if (shuttingDown)
1837
+ return;
1838
+ if (markReady())
1839
+ process.stderr.write("[watcher] all monitors started; file capture verified\n");
1840
+ }, (error) => {
1841
+ process.stderr.write(`[watcher] startup incomplete: ${error instanceof Error ? error.message : error} (retiring unverified watcher)\n`);
1842
+ // A failed replacement must not consume a stop requested for its incumbent.
1843
+ shutdown({ preserveSentinel: true });
1844
+ });
1824
1845
  //# sourceMappingURL=watcher.js.map