litmus-cli 1.4.21 → 1.4.24

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 (51) hide show
  1. package/README.md +16 -0
  2. package/dist/commands/connect.d.ts +231 -2
  3. package/dist/commands/connect.d.ts.map +1 -1
  4. package/dist/commands/connect.js +311 -12
  5. package/dist/commands/connect.js.map +1 -1
  6. package/dist/commands/doctor.d.ts.map +1 -1
  7. package/dist/commands/doctor.js +5 -1
  8. package/dist/commands/doctor.js.map +1 -1
  9. package/dist/commands/pause.d.ts +36 -0
  10. package/dist/commands/pause.d.ts.map +1 -0
  11. package/dist/commands/pause.js +110 -0
  12. package/dist/commands/pause.js.map +1 -0
  13. package/dist/commands/push.d.ts +7 -1
  14. package/dist/commands/push.d.ts.map +1 -1
  15. package/dist/commands/push.js +40 -1
  16. package/dist/commands/push.js.map +1 -1
  17. package/dist/commands/status.d.ts.map +1 -1
  18. package/dist/commands/status.js +6 -1
  19. package/dist/commands/status.js.map +1 -1
  20. package/dist/commands/submit.d.ts.map +1 -1
  21. package/dist/commands/submit.js +48 -2
  22. package/dist/commands/submit.js.map +1 -1
  23. package/dist/index.js +22 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/lib/api.d.ts +27 -0
  26. package/dist/lib/api.d.ts.map +1 -1
  27. package/dist/lib/api.js +45 -0
  28. package/dist/lib/api.js.map +1 -1
  29. package/dist/lib/config.d.ts +21 -0
  30. package/dist/lib/config.d.ts.map +1 -1
  31. package/dist/lib/config.js +53 -1
  32. package/dist/lib/config.js.map +1 -1
  33. package/dist/lib/git-bundle.d.ts +19 -0
  34. package/dist/lib/git-bundle.d.ts.map +1 -0
  35. package/dist/lib/git-bundle.js +71 -0
  36. package/dist/lib/git-bundle.js.map +1 -0
  37. package/dist/lib/session-end.d.ts +111 -0
  38. package/dist/lib/session-end.d.ts.map +1 -0
  39. package/dist/lib/session-end.js +126 -0
  40. package/dist/lib/session-end.js.map +1 -0
  41. package/dist/lib/submit-route.d.ts +68 -0
  42. package/dist/lib/submit-route.d.ts.map +1 -0
  43. package/dist/lib/submit-route.js +73 -0
  44. package/dist/lib/submit-route.js.map +1 -0
  45. package/dist/lib/watcher.js +23 -2
  46. package/dist/lib/watcher.js.map +1 -1
  47. package/dist/lib/workspace-token.d.ts +69 -0
  48. package/dist/lib/workspace-token.d.ts.map +1 -0
  49. package/dist/lib/workspace-token.js +93 -0
  50. package/dist/lib/workspace-token.js.map +1 -0
  51. package/package.json +1 -1
@@ -0,0 +1,111 @@
1
+ /**
2
+ * What happens to a NATIVE editor window when the session ends — ENG-2154.
3
+ *
4
+ * ## The decision, which this ticket was asked to make explicitly
5
+ *
6
+ * **We do not close the candidate's editor window. We tell them what the window
7
+ * is doing instead.** Elena asked for the opposite ("when I finish submitting
8
+ * can you exit out of the ID editor, because the file tree is still there even
9
+ * though I'm disconnecting the codebase"), so the reasons are worth stating
10
+ * rather than leaving in a PR description.
11
+ *
12
+ * MEASURED FIRST, on a real VS Code 1.136 / Remote-SSH 0.128.0 window connected
13
+ * over ssh into a container whose transport was then killed. The window does not
14
+ * go quiet: the title bar and the Explorer root still read `[SSH: <host>]`, the
15
+ * status bar flips to `Disconnected from SSH: <host>`, and the resolver raises a
16
+ * modal reporting that it could not establish the connection and offering to retry.
17
+ * So the state a candidate is left in is not a silent stale file tree — it is an
18
+ * ERROR DIALOG, arriving seconds after they pressed submit. That is the ENG-2066
19
+ * misreading pointed at a different surface: a workspace that stops working with
20
+ * no explanation reads as a FAILED SUBMISSION. Disambiguating it is the whole
21
+ * job; making the window vanish is not.
22
+ *
23
+ * And closing it is not reachable anyway, for three independent reasons:
24
+ *
25
+ * 1. **Nothing of ours is running on the laptop at submit time.** `litmus
26
+ * connect` spawns the editor detached and returns; a v2 candidate then
27
+ * submits from the container (`litmus push` in the editor's own remote
28
+ * terminal) or from the assessment page. Closing the window would mean
29
+ * leaving a background daemon on a candidate's machine for the life of the
30
+ * assessment, purely to close a window.
31
+ * 2. **We could not honour "only the window we opened."** `code --remote` hands
32
+ * the request to an already-running Electron app and exits; the window is
33
+ * not a process we own. Every close mechanism available (osascript UI
34
+ * scripting, PowerShell `CloseMainWindow`, wmctrl) picks its target by
35
+ * window title, which is a guess that can land on a window the candidate
36
+ * opened themselves. The ticket forbids exactly that.
37
+ * 3. **macOS gates it behind a permission the candidate has not granted.** UI
38
+ * scripting needs an Automation/Accessibility grant; without it the attempt
39
+ * is refused (`-1743`), and the first attempt in a fresh grant state blocks
40
+ * on a system dialog — which is what ENG-1502 measured when it made its own
41
+ * close best-effort and detached.
42
+ *
43
+ * A window that vanishes while somebody is reading their own code is worse than
44
+ * one that says what happened, and (1)-(3) mean we could only ever have vanished
45
+ * it unreliably. So: say it, and leave the window to them.
46
+ *
47
+ * ## Why the copy lives in a module
48
+ *
49
+ * The same answer is needed at two moments, in two commands, and a second copy
50
+ * of it would rot — the argument `submit-guidance.ts` and the candidate page's
51
+ * `workspace-guidance.ts` already make:
52
+ *
53
+ * - `litmus connect` says it AHEAD of time, because it is the only laptop-side
54
+ * moment we have, and it is the only thing that reaches a candidate whose
55
+ * session ends some other way (they submit from the assessment page, the
56
+ * company ends the trial, the deadline cron fires).
57
+ * - `litmus push` says it AT the moment, in the terminal that is very often
58
+ * the disconnecting window's own.
59
+ *
60
+ * ## What the copy may claim
61
+ *
62
+ * NO EDITOR-SPECIFIC CONTROL IS NAMED. The dialog's buttons were measured on VS
63
+ * Code only; Cursor resolves `ssh-remote+` through `anysphere.remote-ssh`, a
64
+ * different extension whose dialog we have not measured, and naming a button a
65
+ * candidate cannot find costs us the trust they need in the rest of the sentence
66
+ * (the same line `workspace-guidance.ts` draws over its tool list). What is true
67
+ * of both is the shape: the window stays, it reports a lost connection, and
68
+ * closing it is theirs to do. That bound is tighter than it first looks and
69
+ * Greptile caught the first draft crossing it — describing WHAT THE EDITOR WILL
70
+ * ASK ("and offers to try again") is as unmeasured on Cursor as naming the
71
+ * button would be, and a candidate told to expect a prompt that never appears
72
+ * is left doubting the sentence that was meant to reassure them. Only the fact
73
+ * of a reported disconnection is verified on both.
74
+ *
75
+ * It also may not claim the assessment is FINISHED. A submit can still owe a
76
+ * post-submit walkthrough, and `runPush` prints the next step right after this;
77
+ * the claim here is only about the workspace and the window.
78
+ */
79
+ /**
80
+ * The state the candidate's native editor window is now in, said once.
81
+ *
82
+ * Printed after a submit that ENDED the session — a fresh capture, an
83
+ * `already_submitted`, or a confirmed empty submission. Never after a refusal, a
84
+ * cancellation or a failure: in all three the workspace is still live and the
85
+ * window is still connected, so this would be a lie about both.
86
+ *
87
+ * Written as a conditional ("if ... is open") because `litmus push` runs from
88
+ * four places — a native remote window's terminal, the browser IDE's terminal, a
89
+ * plain ssh session, and a laptop with an explicit token — and cannot tell them
90
+ * apart. The browser IDE's own tab is deliberately not mentioned: ENG-2066 ends
91
+ * that one by navigating it, so describing it here would contradict what that
92
+ * candidate is about to see.
93
+ */
94
+ export declare function editorEndingLines(): string[];
95
+ /**
96
+ * The same fact, said before it happens, on the native-editor path of
97
+ * `litmus connect`.
98
+ *
99
+ * Two lines, and deliberately no more. This is the ONLY route that reaches a
100
+ * candidate who ends their session from somewhere we do not run — the assessment
101
+ * page's Submit, End trial, the deadline cron — and for them it is the whole
102
+ * warning. But it is also printed at the top of a session, minutes or days
103
+ * before it is true, next to a Cursor notice that is already long; anything more
104
+ * than the fact and what to do with it is noise at the wrong moment.
105
+ *
106
+ * @param editorLabel what the candidate calls the editor being opened, from
107
+ * `NATIVE_EDITORS` — this one CAN be named, because `connect` knows which one
108
+ * it is launching. The dialog's controls still cannot be.
109
+ */
110
+ export declare function editorEndingForewarning(editorLabel: string): string[];
111
+ //# sourceMappingURL=session-end.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-end.d.ts","sourceRoot":"","sources":["../../src/lib/session-end.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6EG;AAEH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,EAAE,CAS5C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAKrE"}
@@ -0,0 +1,126 @@
1
+ import chalk from "chalk";
2
+ /**
3
+ * What happens to a NATIVE editor window when the session ends — ENG-2154.
4
+ *
5
+ * ## The decision, which this ticket was asked to make explicitly
6
+ *
7
+ * **We do not close the candidate's editor window. We tell them what the window
8
+ * is doing instead.** Elena asked for the opposite ("when I finish submitting
9
+ * can you exit out of the ID editor, because the file tree is still there even
10
+ * though I'm disconnecting the codebase"), so the reasons are worth stating
11
+ * rather than leaving in a PR description.
12
+ *
13
+ * MEASURED FIRST, on a real VS Code 1.136 / Remote-SSH 0.128.0 window connected
14
+ * over ssh into a container whose transport was then killed. The window does not
15
+ * go quiet: the title bar and the Explorer root still read `[SSH: <host>]`, the
16
+ * status bar flips to `Disconnected from SSH: <host>`, and the resolver raises a
17
+ * modal reporting that it could not establish the connection and offering to retry.
18
+ * So the state a candidate is left in is not a silent stale file tree — it is an
19
+ * ERROR DIALOG, arriving seconds after they pressed submit. That is the ENG-2066
20
+ * misreading pointed at a different surface: a workspace that stops working with
21
+ * no explanation reads as a FAILED SUBMISSION. Disambiguating it is the whole
22
+ * job; making the window vanish is not.
23
+ *
24
+ * And closing it is not reachable anyway, for three independent reasons:
25
+ *
26
+ * 1. **Nothing of ours is running on the laptop at submit time.** `litmus
27
+ * connect` spawns the editor detached and returns; a v2 candidate then
28
+ * submits from the container (`litmus push` in the editor's own remote
29
+ * terminal) or from the assessment page. Closing the window would mean
30
+ * leaving a background daemon on a candidate's machine for the life of the
31
+ * assessment, purely to close a window.
32
+ * 2. **We could not honour "only the window we opened."** `code --remote` hands
33
+ * the request to an already-running Electron app and exits; the window is
34
+ * not a process we own. Every close mechanism available (osascript UI
35
+ * scripting, PowerShell `CloseMainWindow`, wmctrl) picks its target by
36
+ * window title, which is a guess that can land on a window the candidate
37
+ * opened themselves. The ticket forbids exactly that.
38
+ * 3. **macOS gates it behind a permission the candidate has not granted.** UI
39
+ * scripting needs an Automation/Accessibility grant; without it the attempt
40
+ * is refused (`-1743`), and the first attempt in a fresh grant state blocks
41
+ * on a system dialog — which is what ENG-1502 measured when it made its own
42
+ * close best-effort and detached.
43
+ *
44
+ * A window that vanishes while somebody is reading their own code is worse than
45
+ * one that says what happened, and (1)-(3) mean we could only ever have vanished
46
+ * it unreliably. So: say it, and leave the window to them.
47
+ *
48
+ * ## Why the copy lives in a module
49
+ *
50
+ * The same answer is needed at two moments, in two commands, and a second copy
51
+ * of it would rot — the argument `submit-guidance.ts` and the candidate page's
52
+ * `workspace-guidance.ts` already make:
53
+ *
54
+ * - `litmus connect` says it AHEAD of time, because it is the only laptop-side
55
+ * moment we have, and it is the only thing that reaches a candidate whose
56
+ * session ends some other way (they submit from the assessment page, the
57
+ * company ends the trial, the deadline cron fires).
58
+ * - `litmus push` says it AT the moment, in the terminal that is very often
59
+ * the disconnecting window's own.
60
+ *
61
+ * ## What the copy may claim
62
+ *
63
+ * NO EDITOR-SPECIFIC CONTROL IS NAMED. The dialog's buttons were measured on VS
64
+ * Code only; Cursor resolves `ssh-remote+` through `anysphere.remote-ssh`, a
65
+ * different extension whose dialog we have not measured, and naming a button a
66
+ * candidate cannot find costs us the trust they need in the rest of the sentence
67
+ * (the same line `workspace-guidance.ts` draws over its tool list). What is true
68
+ * of both is the shape: the window stays, it reports a lost connection, and
69
+ * closing it is theirs to do. That bound is tighter than it first looks and
70
+ * Greptile caught the first draft crossing it — describing WHAT THE EDITOR WILL
71
+ * ASK ("and offers to try again") is as unmeasured on Cursor as naming the
72
+ * button would be, and a candidate told to expect a prompt that never appears
73
+ * is left doubting the sentence that was meant to reassure them. Only the fact
74
+ * of a reported disconnection is verified on both.
75
+ *
76
+ * It also may not claim the assessment is FINISHED. A submit can still owe a
77
+ * post-submit walkthrough, and `runPush` prints the next step right after this;
78
+ * the claim here is only about the workspace and the window.
79
+ */
80
+ /**
81
+ * The state the candidate's native editor window is now in, said once.
82
+ *
83
+ * Printed after a submit that ENDED the session — a fresh capture, an
84
+ * `already_submitted`, or a confirmed empty submission. Never after a refusal, a
85
+ * cancellation or a failure: in all three the workspace is still live and the
86
+ * window is still connected, so this would be a lie about both.
87
+ *
88
+ * Written as a conditional ("if ... is open") because `litmus push` runs from
89
+ * four places — a native remote window's terminal, the browser IDE's terminal, a
90
+ * plain ssh session, and a laptop with an explicit token — and cannot tell them
91
+ * apart. The browser IDE's own tab is deliberately not mentioned: ENG-2066 ends
92
+ * that one by navigating it, so describing it here would contradict what that
93
+ * candidate is about to see.
94
+ */
95
+ export function editorEndingLines() {
96
+ return [
97
+ "",
98
+ " If VS Code or Cursor is open on this workspace, that window stays open on your",
99
+ " machine and will report a lost connection. That is not a failed submission — it is",
100
+ " this session ending, and nothing is left to save.",
101
+ ` ${chalk.dim("Close the window whenever you are ready; we do not close it for you.")}`,
102
+ "",
103
+ ];
104
+ }
105
+ /**
106
+ * The same fact, said before it happens, on the native-editor path of
107
+ * `litmus connect`.
108
+ *
109
+ * Two lines, and deliberately no more. This is the ONLY route that reaches a
110
+ * candidate who ends their session from somewhere we do not run — the assessment
111
+ * page's Submit, End trial, the deadline cron — and for them it is the whole
112
+ * warning. But it is also printed at the top of a session, minutes or days
113
+ * before it is true, next to a Cursor notice that is already long; anything more
114
+ * than the fact and what to do with it is noise at the wrong moment.
115
+ *
116
+ * @param editorLabel what the candidate calls the editor being opened, from
117
+ * `NATIVE_EDITORS` — this one CAN be named, because `connect` knows which one
118
+ * it is launching. The dialog's controls still cannot be.
119
+ */
120
+ export function editorEndingForewarning(editorLabel) {
121
+ return [
122
+ chalk.dim(` When your assessment ends, this ${editorLabel} window loses its connection and`),
123
+ chalk.dim(" reports an error. That is the session ending, not a failure — close it then."),
124
+ ];
125
+ }
126
+ //# sourceMappingURL=session-end.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-end.js","sourceRoot":"","sources":["../../src/lib/session-end.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6EG;AAEH;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO;QACL,EAAE;QACF,kFAAkF;QAClF,sFAAsF;QACtF,qDAAqD;QACrD,KAAK,KAAK,CAAC,GAAG,CAAC,sEAAsE,CAAC,EAAE;QACxF,EAAE;KACH,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,uBAAuB,CAAC,WAAmB;IACzD,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,qCAAqC,WAAW,kCAAkC,CAAC;QAC7F,KAAK,CAAC,GAAG,CAAC,gFAAgF,CAAC;KAC5F,CAAA;AACH,CAAC"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Which command actually hands work in, for the install this CLI is running from
3
+ * (ENG-2130).
4
+ *
5
+ * There are two flows and each has exactly one submit command:
6
+ *
7
+ * * ON A LAPTOP (the CLI flow) it is `litmus submit`, which zips the local
8
+ * checkout and uploads it to `/cli/submit`.
9
+ * * INSIDE A CODESPACES v2 WORKSPACE it is `litmus push`, which POSTs
10
+ * `/workspace/submit` — the same endpoint the assessment page's Submit
11
+ * button calls, capturing the live container filesystem host-side (ENG-621).
12
+ * There is nothing local to zip.
13
+ *
14
+ * WHY THIS MODULE EXISTS RATHER THAN A CHECK PER SITE. Before ENG-2064 the CLI
15
+ * was not in the container at all, so `litmus submit` there was `command not
16
+ * found` — useless but unambiguous. That ticket put the CLI on the container's
17
+ * PATH and wired the injected `LITMUS_ASSESSMENT_TOKEN` into `push` alone, so
18
+ * `submit` became present, runnable, and unable to authenticate: it reads its
19
+ * bearer from `.litmus/config.json`, and `hostd._stage_litmus_config` writes
20
+ * that file with DELIBERATELY no `token` because it rides the submission zip and
21
+ * the R2 durability tar, where a live credential must never sit. The upload
22
+ * therefore went out as a literal `Authorization: Bearer undefined` and was
23
+ * refused — AFTER the archive had been built, so a candidate read it as an
24
+ * upload failure rather than as the wrong command. Every surface that names a
25
+ * submit command was naming the laptop one to both audiences, so the answer is
26
+ * derived in one place and the sites ask.
27
+ *
28
+ * NOT FIXED BY GIVING `submit` THE ENV FALLBACK `push` HAS. That would make the
29
+ * command "work", but it routes a v2 candidate down `/cli/submit`, which grades
30
+ * an uploaded zip, while v2's canonical submit grades the live filesystem — the
31
+ * two flows would then disagree about what a submission is. That is a product
32
+ * decision, not a patch. And it must NOT be fixed by writing the token into
33
+ * `.litmus/config.json`, for the reason that file has none today.
34
+ *
35
+ * The predicate is `isWorkspaceInstall`, read from our own module path for the
36
+ * reasons its own docstring gives (an env marker is absent in an sshd session,
37
+ * and a marker injected alongside the token disappears in exactly the case we
38
+ * need to describe). Nothing security-relevant hangs off any of this: it only
39
+ * decides which command a candidate is told to run.
40
+ */
41
+ /** The submit command inside a Codespaces v2 workspace. */
42
+ export declare const WORKSPACE_SUBMIT_COMMAND = "litmus push";
43
+ /** The submit command on the candidate's own machine. */
44
+ export declare const LOCAL_SUBMIT_COMMAND = "litmus submit";
45
+ /**
46
+ * The command that submits, for the install `moduleUrl` belongs to.
47
+ *
48
+ * Callers pass their own `import.meta.url`. Use this anywhere candidate-facing
49
+ * copy names the submit command, so a workspace terminal is never told to run
50
+ * the laptop one.
51
+ */
52
+ export declare function submitCommandFor(moduleUrl: string): string;
53
+ /** A refusal to run, as `fatal(message, hint)` takes it. */
54
+ export type SubmitRefusal = {
55
+ message: string;
56
+ hint: string;
57
+ };
58
+ /**
59
+ * Why `litmus submit` must not run here, or `null` when it may.
60
+ *
61
+ * Non-null only for the in-workspace install. The caller is expected to refuse
62
+ * on it BEFORE doing any work — the whole defect is that the failure used to
63
+ * arrive after a full archive build, which is what made it read as an upload
64
+ * problem. Both routes named in the hint reach `/workspace/submit`, so this
65
+ * points at the two things that work and sends the candidate nowhere new.
66
+ */
67
+ export declare function workspaceSubmitRefusal(moduleUrl: string): SubmitRefusal | null;
68
+ //# sourceMappingURL=submit-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submit-route.d.ts","sourceRoot":"","sources":["../../src/lib/submit-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,2DAA2D;AAC3D,eAAO,MAAM,wBAAwB,gBAAgB,CAAA;AAErD,yDAAyD;AACzD,eAAO,MAAM,oBAAoB,kBAAkB,CAAA;AAEnD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,4DAA4D;AAC5D,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAO9E"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Which command actually hands work in, for the install this CLI is running from
3
+ * (ENG-2130).
4
+ *
5
+ * There are two flows and each has exactly one submit command:
6
+ *
7
+ * * ON A LAPTOP (the CLI flow) it is `litmus submit`, which zips the local
8
+ * checkout and uploads it to `/cli/submit`.
9
+ * * INSIDE A CODESPACES v2 WORKSPACE it is `litmus push`, which POSTs
10
+ * `/workspace/submit` — the same endpoint the assessment page's Submit
11
+ * button calls, capturing the live container filesystem host-side (ENG-621).
12
+ * There is nothing local to zip.
13
+ *
14
+ * WHY THIS MODULE EXISTS RATHER THAN A CHECK PER SITE. Before ENG-2064 the CLI
15
+ * was not in the container at all, so `litmus submit` there was `command not
16
+ * found` — useless but unambiguous. That ticket put the CLI on the container's
17
+ * PATH and wired the injected `LITMUS_ASSESSMENT_TOKEN` into `push` alone, so
18
+ * `submit` became present, runnable, and unable to authenticate: it reads its
19
+ * bearer from `.litmus/config.json`, and `hostd._stage_litmus_config` writes
20
+ * that file with DELIBERATELY no `token` because it rides the submission zip and
21
+ * the R2 durability tar, where a live credential must never sit. The upload
22
+ * therefore went out as a literal `Authorization: Bearer undefined` and was
23
+ * refused — AFTER the archive had been built, so a candidate read it as an
24
+ * upload failure rather than as the wrong command. Every surface that names a
25
+ * submit command was naming the laptop one to both audiences, so the answer is
26
+ * derived in one place and the sites ask.
27
+ *
28
+ * NOT FIXED BY GIVING `submit` THE ENV FALLBACK `push` HAS. That would make the
29
+ * command "work", but it routes a v2 candidate down `/cli/submit`, which grades
30
+ * an uploaded zip, while v2's canonical submit grades the live filesystem — the
31
+ * two flows would then disagree about what a submission is. That is a product
32
+ * decision, not a patch. And it must NOT be fixed by writing the token into
33
+ * `.litmus/config.json`, for the reason that file has none today.
34
+ *
35
+ * The predicate is `isWorkspaceInstall`, read from our own module path for the
36
+ * reasons its own docstring gives (an env marker is absent in an sshd session,
37
+ * and a marker injected alongside the token disappears in exactly the case we
38
+ * need to describe). Nothing security-relevant hangs off any of this: it only
39
+ * decides which command a candidate is told to run.
40
+ */
41
+ import { isWorkspaceInstall } from "./workspace-token.js";
42
+ /** The submit command inside a Codespaces v2 workspace. */
43
+ export const WORKSPACE_SUBMIT_COMMAND = "litmus push";
44
+ /** The submit command on the candidate's own machine. */
45
+ export const LOCAL_SUBMIT_COMMAND = "litmus submit";
46
+ /**
47
+ * The command that submits, for the install `moduleUrl` belongs to.
48
+ *
49
+ * Callers pass their own `import.meta.url`. Use this anywhere candidate-facing
50
+ * copy names the submit command, so a workspace terminal is never told to run
51
+ * the laptop one.
52
+ */
53
+ export function submitCommandFor(moduleUrl) {
54
+ return isWorkspaceInstall(moduleUrl) ? WORKSPACE_SUBMIT_COMMAND : LOCAL_SUBMIT_COMMAND;
55
+ }
56
+ /**
57
+ * Why `litmus submit` must not run here, or `null` when it may.
58
+ *
59
+ * Non-null only for the in-workspace install. The caller is expected to refuse
60
+ * on it BEFORE doing any work — the whole defect is that the failure used to
61
+ * arrive after a full archive build, which is what made it read as an upload
62
+ * problem. Both routes named in the hint reach `/workspace/submit`, so this
63
+ * points at the two things that work and sends the candidate nowhere new.
64
+ */
65
+ export function workspaceSubmitRefusal(moduleUrl) {
66
+ if (!isWorkspaceInstall(moduleUrl))
67
+ return null;
68
+ return {
69
+ message: "`litmus submit` cannot submit a Litmus workspace: it uploads a local checkout, and your work lives in this container.",
70
+ hint: `Run \`${WORKSPACE_SUBMIT_COMMAND}\` in this terminal instead, or click Submit assessment on your assessment page.`,
71
+ };
72
+ }
73
+ //# sourceMappingURL=submit-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submit-route.js","sourceRoot":"","sources":["../../src/lib/submit-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAEzD,2DAA2D;AAC3D,MAAM,CAAC,MAAM,wBAAwB,GAAG,aAAa,CAAA;AAErD,yDAAyD;AACzD,MAAM,CAAC,MAAM,oBAAoB,GAAG,eAAe,CAAA;AAEnD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAiB;IAChD,OAAO,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,oBAAoB,CAAA;AACxF,CAAC;AAQD;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,SAAiB;IACtD,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/C,OAAO;QACL,OAAO,EACL,uHAAuH;QACzH,IAAI,EAAE,SAAS,wBAAwB,kFAAkF;KAC1H,CAAA;AACH,CAAC"}
@@ -26,6 +26,7 @@ import { getEffectiveDeadline, writeConfig, validateRebuiltTimes, computeTimeSyn
26
26
  import { CLI_VERSION } from "./version.js";
27
27
  import { armDeadlinePoll, createAutoSubmitController } from "./deadline.js";
28
28
  import { resumeChainFromLog } from "./chain.js";
29
+ import { buildGitBundle, currentHead } from "./git-bundle.js";
29
30
  import { fetchInitMetadata, ServerError } from "./api.js";
30
31
  import { ServerTimesSchema } from "./schemas.js";
31
32
  import { detectInstalledHooks } from "./ai-tracking.js";
@@ -1429,6 +1430,12 @@ function getRecentCommits() {
1429
1430
  return [];
1430
1431
  }
1431
1432
  }
1433
+ // ── Git history (ENG-2238) ──────────────────────────────────────
1434
+ // Capture lives in lib/git-bundle.ts; what belongs here is the one piece of
1435
+ // session state: HEAD at the last bundle the SERVER ACKED, so an unchanged
1436
+ // history is not re-uploaded every five minutes and a failed upload is still
1437
+ // retried on the next tick.
1438
+ let lastBundledHead = null;
1432
1439
  function uploadSnapshot() {
1433
1440
  if (!uploadConfig)
1434
1441
  return;
@@ -1449,7 +1456,17 @@ function uploadSnapshot() {
1449
1456
  // interview agent's context. Without this, a reconstruction restores the
1450
1457
  // candidate's code but none of their AI-usage evidence.
1451
1458
  const activityLog = readActivityLogTail(projectDir);
1452
- const body = JSON.stringify({ files, git_diff: gitDiff, recent_commits: recentCommits, activity_log: activityLog });
1459
+ const head = currentHead(projectDir);
1460
+ const bundle = head && head !== lastBundledHead ? buildGitBundle(projectDir) : null;
1461
+ const body = JSON.stringify({
1462
+ files,
1463
+ git_diff: gitDiff,
1464
+ recent_commits: recentCommits,
1465
+ activity_log: activityLog,
1466
+ // Omitted entirely when unchanged or unavailable; the server keeps the last
1467
+ // good bundle rather than reading an absent field as "no history".
1468
+ ...(bundle ? { git_bundle_gz: bundle.toString("base64") } : {}),
1469
+ });
1453
1470
  const url = new URL(`${uploadConfig.backendUrl}/cli/snapshot`);
1454
1471
  const reqFn = url.protocol === "https:" ? https.request : http.request;
1455
1472
  const req = reqFn(url, {
@@ -1468,7 +1485,11 @@ function uploadSnapshot() {
1468
1485
  process.stderr.write(`[watcher] snapshot upload failed: ${res.statusCode}\n`);
1469
1486
  }
1470
1487
  else {
1471
- process.stderr.write(`[watcher] snapshot uploaded: ${fileCount} files, ${(Buffer.byteLength(body) / 1024).toFixed(0)}KB\n`);
1488
+ // Only now is the history durably ours: an upload that never landed
1489
+ // must be retried on the next tick, not suppressed as already sent.
1490
+ if (bundle && head)
1491
+ lastBundledHead = head;
1492
+ process.stderr.write(`[watcher] snapshot uploaded: ${fileCount} files, ${(Buffer.byteLength(body) / 1024).toFixed(0)}KB${bundle ? ` (+git history ${(bundle.length / 1024).toFixed(0)}KB)` : ""}\n`);
1472
1493
  }
1473
1494
  });
1474
1495
  });