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
package/README.md CHANGED
@@ -102,7 +102,8 @@ While you're working, the background tracker sends activity data to Litmus
102
102
  in addition to the submission ZIP you upload at the end:
103
103
 
104
104
  - File-change events, git commits, and AI-tool usage detected on your machine
105
- (process list, network endpoints) — every few seconds while you work.
105
+ (the names of editors and AI tools among your own user's running programs,
106
+ network endpoints) — every few seconds while you work.
106
107
  - Captured AI prompts and responses from Claude Code, Codex CLI, Cursor, the
107
108
  Gemini CLI, and Copilot CLI via those tools' hooks. `litmus init` installs
108
109
  the hooks and marks them trusted for the assessment folder (Codex's per-hook
@@ -110,7 +111,11 @@ in addition to the submission ZIP you upload at the end:
110
111
  trusted in Claude Code (`~/.claude.json`) and Codex (`[projects]` in
111
112
  `~/.codex/config.toml`), so no tool asks you to approve the hooks or the
112
113
  folder; `litmus submit` removes the hooks and every trust entry again. Copilot
113
- Chat (VS Code side panel) is not hookable and is not captured.
114
+ Chat in VS Code is captured through the same `.github/hooks/litmus.json` hook
115
+ as the Copilot CLI, when the assessment folder is trusted. This was checked on
116
+ VS Code 1.131 with Copilot Chat 0.59.0 in agent mode. Coverage of every mode
117
+ has not been fully measured: other Copilot Chat modes, and the `copilot` CLI
118
+ beyond one interactive prompt (including `copilot -p`), have not been checked.
114
119
  - A periodic codebase snapshot (every five minutes) so the optional
115
120
  live-interview agent has up-to-date context. Snapshots are capped at 2 MB
116
121
  total and skip binaries, dotfiles (including `.env`), lockfiles,
@@ -127,6 +132,27 @@ in addition to the submission ZIP you upload at the end:
127
132
  You can confirm what's installed and what's being captured at any point with
128
133
  `litmus status`.
129
134
 
135
+ ## Where the AI hooks live and what they record
136
+
137
+ `litmus init` installs the hooks for Claude Code, Codex, Cursor and the Gemini
138
+ CLI in your user-level config for each tool: `~/.claude/settings.json`,
139
+ `~/.codex/hooks.json`, `~/.cursor/hooks.json` and `~/.gemini/settings.json`.
140
+ The Copilot hook is the exception: it lives inside the assessment folder, in
141
+ `.github/hooks/litmus.json`.
142
+
143
+ Because they are user-level, these hooks run for every prompt you send to those
144
+ tools on this machine, not only prompts made inside the assessment. Each run
145
+ checks where the prompt came from. A prompt made inside the assessment folder is
146
+ recorded. A prompt made anywhere else, including a folder above the assessment
147
+ such as your home folder, is not written down or sent to Litmus. So open the
148
+ assessment folder itself in your editor, or `cd` into it before starting an AI
149
+ tool, when you want that work captured.
150
+
151
+ The hooks are removed when you run `litmus submit`. If you leave an assessment
152
+ unfinished, they stay installed until you remove them, and prompts made inside
153
+ that assessment's folder are still recorded there. You can remove them yourself
154
+ at any time by deleting the Litmus entries from the files above.
155
+
130
156
  ## Requirements
131
157
 
132
158
  - Node.js 18+
@@ -40,7 +40,8 @@ export declare function parseSshCommand(cmd: string): ParsedSsh;
40
40
  * This does NOT turn multiplexing on; with no ControlMaster the directive is inert.
41
41
  * It only makes sure that a candidate who HAS enabled it multiplexes over a path that
42
42
  * fits. Every stanza gets it, because a ProxyJump opens its own connection to the
43
- * bastion and that one is the long path.
43
+ * bastion and that one is the long path. "Inert" holds everywhere EXCEPT Windows, where
44
+ * any ControlPath at all breaks the connection — see `controlPathFor` (ENG-2676).
44
45
  *
45
46
  * WRITING THE DIRECTIVE IS ONLY HALF OF IT. ssh takes the FIRST value it sees for a
46
47
  * keyword, so a global `Host *` sitting earlier in the file beats anything we write
@@ -50,7 +51,23 @@ export declare function parseSshCommand(cmd: string): ParsedSsh;
50
51
  * copy here is what applies when there is nothing global to lose to, and keeps this
51
52
  * block correct on its own.
52
53
  */
53
- export declare function buildSshConfigBlock(alias: string, keyPath: string, p: ParsedSsh): string;
54
+ export declare function buildSshConfigBlock(alias: string, keyPath: string, p: ParsedSsh, platform?: NodeJS.Platform): string;
55
+ /**
56
+ * ENG-2681 — an ssh_config argument, quoted so a space cannot split it.
57
+ *
58
+ * `keyPath` is built from the candidate's home directory, and a home with a space in it
59
+ * (`C:\Users\<two-word account name>`, common for Windows local accounts) split the unquoted line into
60
+ * two arguments. ssh does not skip a bad line; it rejects the whole file:
61
+ *
62
+ * line 6: keyword identityfile extra arguments at end of line
63
+ * terminating, 1 bad configuration options
64
+ *
65
+ * so every ssh the candidate ran, `git push` to GitHub included, failed until our block
66
+ * was deleted. Measured with `ssh -G`: the quoted form resolves to the path exactly as
67
+ * written, spaces and Windows backslashes (`\U`, `\.`) intact. A double quote cannot
68
+ * appear in a Windows path and has no portable escape in ssh_config, so it is not handled.
69
+ */
70
+ export declare function quoteConfigArg(value: string): string;
54
71
  /**
55
72
  * A stanza carrying ONE directive, written ahead of the candidate's own config.
56
73
  *
@@ -66,8 +83,33 @@ export declare function buildSshConfigBlock(alias: string, keyPath: string, p: P
66
83
  * losing to the user's settings exactly as it did before, and a directive added to the
67
84
  * main block later cannot silently inherit precedence it was never reviewed for. Keep
68
85
  * this stanza to ControlPath alone.
86
+ *
87
+ * On Windows the same precedence is what carries ENG-2676, and it has to go further: its
88
+ * `none` must beat EVERY ControlPath the candidate has, preamble included, because there
89
+ * any ControlPath is fatal rather than merely too long. So the Windows stanza goes at the
90
+ * very top of the file (`upsertSshConfigContent`) and closes with a bare `Host *`, which
91
+ * hands everything after it — the candidate's preamble — back to every host exactly as
92
+ * before. Nothing of theirs moves; our one directive is simply read first.
93
+ */
94
+ export declare function buildControlPathBlock(alias: string, p: ParsedSsh, platform?: NodeJS.Platform): string;
95
+ /**
96
+ * The ControlPath we write: `~/.ssh/cm-%C` (ENG-1936), except `none` on Windows.
97
+ *
98
+ * ENG-2676. Win32-OpenSSH has no Unix-domain socket to multiplex over, so ANY ControlPath
99
+ * — with or without a ControlMaster — kills the connection before it offers a key:
100
+ *
101
+ * debug1: getpeername failed: Not a socket
102
+ * getsockname failed: Not a socket
103
+ * Read from remote host <cid>.vm.litmushiring.com: Unknown error
104
+ * Transferred: sent 0, received 0 bytes, in 0.0 seconds
105
+ *
106
+ * VS Code shows it as "Could not establish connection". Because the client dies before
107
+ * the bastion ever sees a key, nothing on our side logs it: no `resolve-ssh` call, no
108
+ * backend error, a healthy container. `none` switches sharing off outright, which is
109
+ * the only setting that works there. The socket-length problem `%C` exists for is a
110
+ * macOS one and cannot arise on Windows, since there is no socket.
69
111
  */
70
- export declare function buildControlPathBlock(alias: string, p: ParsedSsh): string;
112
+ export declare function controlPathFor(platform: NodeJS.Platform): string;
71
113
  /**
72
114
  * Replace (or insert) this alias's regions in the ~/.ssh/config content. Idempotent:
73
115
  * re-running `connect` swaps them and never touches the rest of the user's config.
@@ -82,8 +124,14 @@ export declare function buildControlPathBlock(alias: string, p: ParsedSsh): stri
82
124
  * (or in an `Include`d file pulled in before their first stanza) is still read first and
83
125
  * still wins. Beating it would mean reordering the candidate's own config, which is not
84
126
  * a thing `litmus connect` should do to fix its own socket path.
127
+ *
128
+ * Except on Windows (ENG-2676), where that residual is a dead connection rather than a
129
+ * long path. There the control block goes at the very top instead, which reorders
130
+ * nothing: it closes with `Host *`, so the preamble after it still applies to every
131
+ * host, and only our one ControlPath is read ahead of it. `platform` must match the one
132
+ * `buildControlPathBlock` was given; both default to the running platform.
85
133
  */
86
- export declare function upsertSshConfigContent(existing: string, alias: string, block: string, controlBlock?: string): string;
134
+ export declare function upsertSshConfigContent(existing: string, alias: string, block: string, controlBlock?: string, platform?: NodeJS.Platform): string;
87
135
  /** Short, stable alias from the workspace hostname (or token as a fallback). */
88
136
  export declare function aliasFor(sshHostname: string | null | undefined, token: string): string;
89
137
  /**
@@ -575,6 +623,13 @@ export declare function resolveIde(flag: string | undefined, log?: (line: string
575
623
  ide: IdeChoice;
576
624
  chosen: boolean;
577
625
  }>;
626
+ /**
627
+ * Whether the post-start poll should keep going. The ordinary window covers every
628
+ * status; only a workspace the backend still reports `starting` earns the longer one,
629
+ * so a start that was answered `queued` (the row is back to `not_created`) is not held
630
+ * for eight minutes against something that is not coming.
631
+ */
632
+ export declare function keepWaitingForWorkspace(elapsedMs: number, status: string | undefined): boolean;
578
633
  /**
579
634
  * The candidate's own assessment page.
580
635
  *
@@ -1 +1 @@
1
- {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/commands/connect.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/commands/connect.ts"],"names":[],"mappings":"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"}
@@ -10,8 +10,10 @@ import { fetchInitMetadata, startWorkspace, getConnection, fetchEditorChatConfig
10
10
  import { applyEditorChatConfig } from "../lib/chat-model-config.js";
11
11
  import { resolveApiBase } from "../lib/api-base.js";
12
12
  import { editorInstallCandidates, editorSpawnSpec } from "../lib/editor-binary.js";
13
+ import { isWsl, whichOnPath, windowsSideEditor, wslEditorHint } from "../lib/wsl.js";
13
14
  import { parseEditorVersion, parseWorkspaceProbe, remoteProbeCommand, seedVerdict, verifyVerdict, VERIFY_SCHEDULE_MS, } from "../lib/editor-server.js";
14
15
  import { fatal, success, info, warn, setErrorContext } from "../lib/errors.js";
16
+ import { oldWindowsSshHint, parseOpenSshVersion, proxyJumpBroken, readEditorSettings, readSshVersionBanner, sshExecutableFor, } from "../lib/ssh-client.js";
15
17
  import { editorEndingForewarning } from "../lib/session-end.js";
16
18
  import { announceCursorAutoRestart, attemptQuit, platformSupportsGracefulCursorRestart, realCursorProcessIo, renderCursorRestartOutcome, } from "../lib/cursor-restart.js";
17
19
  /**
@@ -114,7 +116,8 @@ export function parseSshCommand(cmd) {
114
116
  * This does NOT turn multiplexing on; with no ControlMaster the directive is inert.
115
117
  * It only makes sure that a candidate who HAS enabled it multiplexes over a path that
116
118
  * fits. Every stanza gets it, because a ProxyJump opens its own connection to the
117
- * bastion and that one is the long path.
119
+ * bastion and that one is the long path. "Inert" holds everywhere EXCEPT Windows, where
120
+ * any ControlPath at all breaks the connection — see `controlPathFor` (ENG-2676).
118
121
  *
119
122
  * WRITING THE DIRECTIVE IS ONLY HALF OF IT. ssh takes the FIRST value it sees for a
120
123
  * keyword, so a global `Host *` sitting earlier in the file beats anything we write
@@ -124,12 +127,12 @@ export function parseSshCommand(cmd) {
124
127
  * copy here is what applies when there is nothing global to lose to, and keeps this
125
128
  * block correct on its own.
126
129
  */
127
- export function buildSshConfigBlock(alias, keyPath, p) {
130
+ export function buildSshConfigBlock(alias, keyPath, p, platform = process.platform) {
128
131
  const common = [
129
- ` IdentityFile ${keyPath}`,
132
+ ` IdentityFile ${quoteConfigArg(keyPath)}`,
130
133
  ` IdentitiesOnly yes`,
131
134
  ` StrictHostKeyChecking accept-new`,
132
- ` ControlPath ~/.ssh/cm-%C`,
135
+ ` ControlPath ${controlPathFor(platform)}`,
133
136
  ];
134
137
  const out = [CONNECT_BEGIN(alias)];
135
138
  if (p.jumpHost) {
@@ -142,6 +145,24 @@ export function buildSshConfigBlock(alias, keyPath, p) {
142
145
  out.push(CONNECT_END(alias));
143
146
  return out.join("\n") + "\n";
144
147
  }
148
+ /**
149
+ * ENG-2681 — an ssh_config argument, quoted so a space cannot split it.
150
+ *
151
+ * `keyPath` is built from the candidate's home directory, and a home with a space in it
152
+ * (`C:\Users\<two-word account name>`, common for Windows local accounts) split the unquoted line into
153
+ * two arguments. ssh does not skip a bad line; it rejects the whole file:
154
+ *
155
+ * line 6: keyword identityfile extra arguments at end of line
156
+ * terminating, 1 bad configuration options
157
+ *
158
+ * so every ssh the candidate ran, `git push` to GitHub included, failed until our block
159
+ * was deleted. Measured with `ssh -G`: the quoted form resolves to the path exactly as
160
+ * written, spaces and Windows backslashes (`\U`, `\.`) intact. A double quote cannot
161
+ * appear in a Windows path and has no portable escape in ssh_config, so it is not handled.
162
+ */
163
+ export function quoteConfigArg(value) {
164
+ return `"${value}"`;
165
+ }
145
166
  /** A line that opens a conditional block, i.e. the point after which anything we write
146
167
  * is no longer first for the keywords a preceding stanza already set. */
147
168
  const OPENS_A_STANZA = /^[ \t]*(Host|Match)[ \t=]/i;
@@ -160,18 +181,57 @@ const OPENS_A_STANZA = /^[ \t]*(Host|Match)[ \t=]/i;
160
181
  * losing to the user's settings exactly as it did before, and a directive added to the
161
182
  * main block later cannot silently inherit precedence it was never reviewed for. Keep
162
183
  * this stanza to ControlPath alone.
184
+ *
185
+ * On Windows the same precedence is what carries ENG-2676, and it has to go further: its
186
+ * `none` must beat EVERY ControlPath the candidate has, preamble included, because there
187
+ * any ControlPath is fatal rather than merely too long. So the Windows stanza goes at the
188
+ * very top of the file (`upsertSshConfigContent`) and closes with a bare `Host *`, which
189
+ * hands everything after it — the candidate's preamble — back to every host exactly as
190
+ * before. Nothing of theirs moves; our one directive is simply read first.
163
191
  */
164
- export function buildControlPathBlock(alias, p) {
192
+ export function buildControlPathBlock(alias, p, platform = process.platform) {
165
193
  const hosts = p.jumpHost ? `${alias} ${alias}-bastion` : alias;
194
+ if (platform === "win32") {
195
+ return [
196
+ CONTROL_BEGIN(alias),
197
+ `# ControlPath only — first in the file because Windows OpenSSH cannot share connections,`,
198
+ `# and any ControlPath (yours or ours) breaks it. The Host * line hands everything after`,
199
+ `# it back to every host, so your own settings apply exactly as before.`,
200
+ `Host ${hosts}`,
201
+ ` ControlPath ${controlPathFor(platform)}`,
202
+ `Host *`,
203
+ CONTROL_END(alias),
204
+ ].join("\n") + "\n";
205
+ }
166
206
  return [
167
207
  CONTROL_BEGIN(alias),
168
208
  `# ControlPath only — ahead of your own config so a global ControlPath cannot build`,
169
209
  `# an over-long socket path out of the workspace id. Nothing else is overridden.`,
170
210
  `Host ${hosts}`,
171
- ` ControlPath ~/.ssh/cm-%C`,
211
+ ` ControlPath ${controlPathFor(platform)}`,
172
212
  CONTROL_END(alias),
173
213
  ].join("\n") + "\n";
174
214
  }
215
+ /**
216
+ * The ControlPath we write: `~/.ssh/cm-%C` (ENG-1936), except `none` on Windows.
217
+ *
218
+ * ENG-2676. Win32-OpenSSH has no Unix-domain socket to multiplex over, so ANY ControlPath
219
+ * — with or without a ControlMaster — kills the connection before it offers a key:
220
+ *
221
+ * debug1: getpeername failed: Not a socket
222
+ * getsockname failed: Not a socket
223
+ * Read from remote host <cid>.vm.litmushiring.com: Unknown error
224
+ * Transferred: sent 0, received 0 bytes, in 0.0 seconds
225
+ *
226
+ * VS Code shows it as "Could not establish connection". Because the client dies before
227
+ * the bastion ever sees a key, nothing on our side logs it: no `resolve-ssh` call, no
228
+ * backend error, a healthy container. `none` switches sharing off outright, which is
229
+ * the only setting that works there. The socket-length problem `%C` exists for is a
230
+ * macOS one and cannot arise on Windows, since there is no socket.
231
+ */
232
+ export function controlPathFor(platform) {
233
+ return platform === "win32" ? "none" : "~/.ssh/cm-%C";
234
+ }
175
235
  /** Drop one marker-delimited region, wherever in the file it sits. */
176
236
  function withoutRegion(existing, begin, end) {
177
237
  const bi = existing.indexOf(begin);
@@ -196,6 +256,13 @@ function insertAhead(existing, block) {
196
256
  const after = lines.slice(at).join("\n");
197
257
  return (preamble.trim().length ? preamble + "\n\n" : "") + block + "\n" + after;
198
258
  }
259
+ /** Put `block` at the very top of the file. Only for a block that closes with `Host *`
260
+ * (the Windows ControlPath stanza), which is what keeps the preamble below it global. */
261
+ function prepend(existing, block) {
262
+ if (!existing.trim().length)
263
+ return block;
264
+ return block + "\n" + existing.replace(/^\n+/, "");
265
+ }
199
266
  /** Exactly one blank line before the block, however the prior content ended. Normalised
200
267
  * rather than merely separated, so re-running never accretes blank lines where a
201
268
  * removed region used to be — the output has to be a fixed point. */
@@ -218,12 +285,20 @@ function append(existing, block) {
218
285
  * (or in an `Include`d file pulled in before their first stanza) is still read first and
219
286
  * still wins. Beating it would mean reordering the candidate's own config, which is not
220
287
  * a thing `litmus connect` should do to fix its own socket path.
288
+ *
289
+ * Except on Windows (ENG-2676), where that residual is a dead connection rather than a
290
+ * long path. There the control block goes at the very top instead, which reorders
291
+ * nothing: it closes with `Host *`, so the preamble after it still applies to every
292
+ * host, and only our one ControlPath is read ahead of it. `platform` must match the one
293
+ * `buildControlPathBlock` was given; both default to the running platform.
221
294
  */
222
- export function upsertSshConfigContent(existing, alias, block, controlBlock) {
295
+ export function upsertSshConfigContent(existing, alias, block, controlBlock, platform = process.platform) {
223
296
  let out = withoutRegion(existing, CONNECT_BEGIN(alias), CONNECT_END(alias));
224
297
  out = withoutRegion(out, CONTROL_BEGIN(alias), CONTROL_END(alias));
225
298
  out = append(out, block);
226
- return controlBlock ? insertAhead(out, controlBlock) : out;
299
+ if (!controlBlock)
300
+ return out;
301
+ return platform === "win32" ? prepend(out, controlBlock) : insertAhead(out, controlBlock);
227
302
  }
228
303
  /** Short, stable alias from the workspace hostname (or token as a fallback). */
229
304
  export function aliasFor(sshHostname, token) {
@@ -1045,6 +1120,28 @@ export async function resolveIde(flag, log = console.log, reentry) {
1045
1120
  announceCursorFullQuit(log, reentry);
1046
1121
  return { ide, chosen: answered.trim() !== "" };
1047
1122
  }
1123
+ // How long `litmus connect` waits for a started workspace to become reachable.
1124
+ const WORKSPACE_READY_WAIT_MS = 120000;
1125
+ // ENG-2578: and how long while the backend still says `starting`. A host that has not
1126
+ // pulled the pinned image yet pulls it inside the first provision it gets (measured at
1127
+ // 221-264s for one variant after a pin moved), so `/workspace/start` answers `starting`
1128
+ // at its own 120s deadline and the backend keeps finishing the provision in the
1129
+ // background for `_LATE_PROVISION_TIMEOUT` more (backend/services/shared_host_workspace.py,
1130
+ // 480s). Wait that long, plus a margin for the write that lands it. Nothing enforces the
1131
+ // pair across the language boundary; keep them in step.
1132
+ const SERVER_LATE_PROVISION_MS = 480000;
1133
+ const WORKSPACE_STARTING_WAIT_MS = SERVER_LATE_PROVISION_MS + 30000;
1134
+ /**
1135
+ * Whether the post-start poll should keep going. The ordinary window covers every
1136
+ * status; only a workspace the backend still reports `starting` earns the longer one,
1137
+ * so a start that was answered `queued` (the row is back to `not_created`) is not held
1138
+ * for eight minutes against something that is not coming.
1139
+ */
1140
+ export function keepWaitingForWorkspace(elapsedMs, status) {
1141
+ if (elapsedMs < WORKSPACE_READY_WAIT_MS)
1142
+ return true;
1143
+ return status === "starting" && elapsedMs < WORKSPACE_STARTING_WAIT_MS;
1144
+ }
1048
1145
  /**
1049
1146
  * The candidate's own assessment page.
1050
1147
  *
@@ -1410,10 +1507,11 @@ export async function runConnect(token, ideOpt, opts = {}) {
1410
1507
  });
1411
1508
  return;
1412
1509
  }
1413
- // 4. Poll until healthy (cold provision is ~5s; give it up to ~2 min).
1510
+ // 4. Poll until healthy (cold provision is ~5s; give it up to ~2 min, longer while
1511
+ // the backend still reports it `starting` — see `keepWaitingForWorkspace`).
1414
1512
  let conn = null;
1415
- const deadline = Date.now() + 120000;
1416
- while (Date.now() < deadline) {
1513
+ const pollStartedAt = Date.now();
1514
+ while (keepWaitingForWorkspace(Date.now() - pollStartedAt, conn?.workspace_status)) {
1417
1515
  try {
1418
1516
  conn = await getConnection(backendUrl, token);
1419
1517
  }
@@ -1427,7 +1525,9 @@ export async function runConnect(token, ideOpt, opts = {}) {
1427
1525
  const ready = conn?.healthy && (ide === "browser" || !!conn.ssh_command);
1428
1526
  if (ready)
1429
1527
  break;
1430
- boot.text = `Starting your workspace... (${conn?.workspace_status ?? "provisioning"})`;
1528
+ boot.text = Date.now() - pollStartedAt < WORKSPACE_READY_WAIT_MS
1529
+ ? `Starting your workspace... (${conn?.workspace_status ?? "provisioning"})`
1530
+ : "Still starting your workspace... the first start on a fresh host can take a few minutes";
1431
1531
  await sleep(2000);
1432
1532
  }
1433
1533
  if (!conn?.healthy || (ide !== "browser" && !conn.ssh_command)) {
@@ -1473,6 +1573,20 @@ export async function runConnect(token, ideOpt, opts = {}) {
1473
1573
  // Prune stale host keys first so a re-provisioned container's new key is
1474
1574
  // accepted, not refused (ENG-1221).
1475
1575
  const parsed = parseSshCommand(conn.ssh_command);
1576
+ // ENG-2682 — Windows' own OpenSSH below 8.1 cannot run the ProxyJump every bastion
1577
+ // route depends on (see `lib/ssh-client.ts`). Refuse before writing anything, with the
1578
+ // version named and a way out, rather than hand an editor a connection it will lose
1579
+ // before it reaches us. The client judged is the one this route will run: the editor's
1580
+ // `remote.SSH.path` when it names one, PATH's ssh otherwise.
1581
+ if (process.platform === "win32" && parsed.jumpHost && ide !== "browser") {
1582
+ const executable = sshExecutableFor(ide, ide === "ssh" ? null : readEditorSettings(ide));
1583
+ const client = parseOpenSshVersion(readSshVersionBanner(executable) ?? "");
1584
+ if (client && proxyJumpBroken(client)) {
1585
+ const where = executable === "ssh" ? "" : ` at ${executable}, set in remote.SSH.path`;
1586
+ fatal(`Your Windows SSH (${client.label}${where}) is too old to reach the workspace.`, oldWindowsSshHint(Boolean(conn.browser_ide_url)), { severity: "warning" });
1587
+ return;
1588
+ }
1589
+ }
1476
1590
  const alias = aliasFor(conn.ssh_hostname, token);
1477
1591
  pruneStaleHostKeys(parsed);
1478
1592
  const block = buildSshConfigBlock(alias, KEY_PATH, parsed);
@@ -1496,6 +1610,17 @@ export async function runConnect(token, ideOpt, opts = {}) {
1496
1610
  // installed" on a machine where the binary was sitting in the application bundle the
1497
1611
  // whole time. Null here means we genuinely could not find a way to drive it.
1498
1612
  const resolved = resolveEditorCommand(editor);
1613
+ // ENG-2683 — inside WSL, the editor on PATH is usually the WINDOWS app, and its
1614
+ // Remote-SSH reads Windows' ssh config, not the WSL one written above (see
1615
+ // `lib/wsl.ts`). Launching it would fail in the editor with nothing on our side, so say
1616
+ // so here, where both the editor and the alias are known.
1617
+ if (resolved && isWsl()) {
1618
+ const program = whichOnPath(resolved.command, process.env.PATH);
1619
+ if (program && windowsSideEditor(program)) {
1620
+ fatal(`${editor.label} here is the Windows app, but \`litmus connect\` is running inside WSL.`, wslEditorHint(editor.label, alias, Boolean(conn.browser_ide_url)), { severity: "warning" });
1621
+ return;
1622
+ }
1623
+ }
1499
1624
  // ENG-2152 — resolve WHAT THAT COMMAND ACTUALLY IS before anything acts on the choice.
1500
1625
  // `bin` is a name on PATH, and on a machine where Cursor is the default editor `code`
1501
1626
  // can be Cursor's own CLI. The verdict is taken here, above the Copilot Chat step,