litmus-cli 1.4.11 → 1.4.13
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.
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +10 -8
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +5 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +49 -25
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +15 -3
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/submit.js +2 -1
- package/dist/commands/submit.js.map +1 -1
- package/dist/lib/ai-tracking.d.ts +32 -2
- package/dist/lib/ai-tracking.d.ts.map +1 -1
- package/dist/lib/ai-tracking.js +153 -3
- package/dist/lib/ai-tracking.js.map +1 -1
- package/dist/lib/backfill-tools.d.ts +42 -0
- package/dist/lib/backfill-tools.d.ts.map +1 -1
- package/dist/lib/backfill-tools.js +266 -1
- package/dist/lib/backfill-tools.js.map +1 -1
- package/dist/lib/backfill.d.ts +2 -2
- package/dist/lib/backfill.d.ts.map +1 -1
- package/dist/lib/backfill.js.map +1 -1
- package/dist/lib/capture-nudge.d.ts +118 -0
- package/dist/lib/capture-nudge.d.ts.map +1 -0
- package/dist/lib/capture-nudge.js +342 -0
- package/dist/lib/capture-nudge.js.map +1 -0
- package/dist/lib/hook-logger.cjs +28 -4
- package/dist/lib/notice-ownership.d.ts +13 -0
- package/dist/lib/notice-ownership.d.ts.map +1 -0
- package/dist/lib/notice-ownership.js +123 -0
- package/dist/lib/notice-ownership.js.map +1 -0
- package/dist/lib/notify.d.ts +25 -0
- package/dist/lib/notify.d.ts.map +1 -0
- package/dist/lib/notify.js +68 -0
- package/dist/lib/notify.js.map +1 -0
- package/dist/lib/shadow.d.ts +9 -0
- package/dist/lib/shadow.d.ts.map +1 -1
- package/dist/lib/shadow.js +26 -3
- package/dist/lib/shadow.js.map +1 -1
- package/dist/lib/watcher.js +96 -3
- package/dist/lib/watcher.js.map +1 -1
- package/dist/lib/zip.d.ts.map +1 -1
- package/dist/lib/zip.js +22 -1
- package/dist/lib/zip.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
// ── Ownership registry for candidate-facing notice files (ENG-1857) ──
|
|
6
|
+
// The notice files (LITMUS-AI-NOTICE.md, LITMUS-REPAIR.md) sit world-readable
|
|
7
|
+
// at the assessment root, so nothing IN a file can prove this CLI wrote it —
|
|
8
|
+
// any sentinel or marker line can be copied into a look-alike, and a
|
|
9
|
+
// candidate's edits to a generated notice would still carry it. Ownership
|
|
10
|
+
// therefore lives OUTSIDE the file: a registry records the sha256 of exactly
|
|
11
|
+
// the content we wrote, and only a byte-identical file is ever treated as
|
|
12
|
+
// ours. The moment a candidate edits our notice — or plants their own — the
|
|
13
|
+
// hash stops matching and the file is theirs: its change events flow, it
|
|
14
|
+
// ships in the submission zip, and no cleanup path may overwrite or remove it.
|
|
15
|
+
//
|
|
16
|
+
// The registry lives in HOME scope (`~/.litmus/notice-owners.json`, keyed by
|
|
17
|
+
// project realpath), not in the project's own `.litmus/`, so it is outside
|
|
18
|
+
// everything that operates on the assessment tree — the candidate's editor
|
|
19
|
+
// and agents, `git clean`, the zip, the watcher. To be explicit about the
|
|
20
|
+
// boundary this does NOT move: everything on a candidate-owned machine is
|
|
21
|
+
// ultimately candidate-writable (activity.jsonl included), so no client-side
|
|
22
|
+
// state can be adversarially trustworthy. This registry's job is preventing
|
|
23
|
+
// ACCIDENTAL destruction or suppression of candidate work; a candidate who
|
|
24
|
+
// deliberately forges an entry can only self-suppress a root file with one of
|
|
25
|
+
// these two fixed names — which gains them nothing over simply keeping that
|
|
26
|
+
// file outside the assessment directory, where it was never evidence at all.
|
|
27
|
+
const OWNERS_FILE_NAME = "notice-owners.json";
|
|
28
|
+
// Same ceiling as the notices themselves (~1KB each): anything larger at one
|
|
29
|
+
// of these paths is candidate work regardless of what the registry says.
|
|
30
|
+
const MAX_NOTICE_BYTES = 64 * 1024;
|
|
31
|
+
// Resolved per call (not at module load): tests fake HOME, and vitest caches
|
|
32
|
+
// modules — a load-time constant would lock the first test's home in place.
|
|
33
|
+
function ownersPath() {
|
|
34
|
+
return path.join(os.homedir(), ".litmus", OWNERS_FILE_NAME);
|
|
35
|
+
}
|
|
36
|
+
/** Registry keys are project realpaths: `process.cwd()` is realpath-resolved
|
|
37
|
+
* but callers may hold raw paths, and on macOS `/var` is a symlink to
|
|
38
|
+
* `/private/var` — raw-string keys would silently never match. */
|
|
39
|
+
function projectKey(projectDir) {
|
|
40
|
+
try {
|
|
41
|
+
return fs.realpathSync(projectDir);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return projectDir;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function contentHash(content) {
|
|
48
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
49
|
+
}
|
|
50
|
+
/** Tolerant load: unreadable or malformed means an empty registry — doubt is
|
|
51
|
+
* always resolved toward "the candidate's file". */
|
|
52
|
+
function loadOwners() {
|
|
53
|
+
try {
|
|
54
|
+
const raw = JSON.parse(fs.readFileSync(ownersPath(), "utf8"));
|
|
55
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
56
|
+
return {};
|
|
57
|
+
const registry = {};
|
|
58
|
+
for (const [project, entries] of Object.entries(raw)) {
|
|
59
|
+
if (typeof entries !== "object" || entries === null || Array.isArray(entries))
|
|
60
|
+
continue;
|
|
61
|
+
const owned = {};
|
|
62
|
+
for (const [name, hash] of Object.entries(entries)) {
|
|
63
|
+
if (typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash))
|
|
64
|
+
owned[name] = hash;
|
|
65
|
+
}
|
|
66
|
+
if (Object.keys(owned).length)
|
|
67
|
+
registry[project] = owned;
|
|
68
|
+
}
|
|
69
|
+
return registry;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function saveOwners(registry) {
|
|
76
|
+
try {
|
|
77
|
+
fs.mkdirSync(path.dirname(ownersPath()), { recursive: true });
|
|
78
|
+
fs.writeFileSync(ownersPath(), JSON.stringify(registry), "utf8");
|
|
79
|
+
}
|
|
80
|
+
catch { /* best effort — worst case a notice we wrote is treated as the candidate's */ }
|
|
81
|
+
}
|
|
82
|
+
/** Record that this CLI just wrote `content` to the root file `fileName`.
|
|
83
|
+
* Call immediately after every write of a notice file. */
|
|
84
|
+
export function recordOwnedNotice(projectDir, fileName, content) {
|
|
85
|
+
const registry = loadOwners();
|
|
86
|
+
const key = projectKey(projectDir);
|
|
87
|
+
registry[key] = { ...registry[key], [fileName]: contentHash(content) };
|
|
88
|
+
saveOwners(registry);
|
|
89
|
+
}
|
|
90
|
+
/** Forget a notice we removed (or gave up on). */
|
|
91
|
+
export function clearOwnedNotice(projectDir, fileName) {
|
|
92
|
+
const registry = loadOwners();
|
|
93
|
+
const key = projectKey(projectDir);
|
|
94
|
+
const owned = registry[key];
|
|
95
|
+
if (!owned || !(fileName in owned))
|
|
96
|
+
return;
|
|
97
|
+
delete owned[fileName];
|
|
98
|
+
if (!Object.keys(owned).length)
|
|
99
|
+
delete registry[key];
|
|
100
|
+
saveOwners(registry);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* True only when the root file `fileName` exists and is byte-identical to
|
|
104
|
+
* content this CLI recorded writing for this project. Never throws; a missing
|
|
105
|
+
* file, missing record, unreadable content, oversize file, or hash mismatch
|
|
106
|
+
* are all "not ours".
|
|
107
|
+
*/
|
|
108
|
+
export function isOwnedNoticeFile(projectDir, fileName) {
|
|
109
|
+
const recorded = loadOwners()[projectKey(projectDir)]?.[fileName];
|
|
110
|
+
if (!recorded)
|
|
111
|
+
return false;
|
|
112
|
+
try {
|
|
113
|
+
const abs = path.join(projectDir, fileName);
|
|
114
|
+
const stat = fs.statSync(abs);
|
|
115
|
+
if (!stat.isFile() || stat.size > MAX_NOTICE_BYTES)
|
|
116
|
+
return false;
|
|
117
|
+
return contentHash(fs.readFileSync(abs, "utf8")) === recorded;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=notice-ownership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notice-ownership.js","sourceRoot":"","sources":["../../src/lib/notice-ownership.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort desktop notification (ENG-1857) — zero dependencies, silent
|
|
3
|
+
* failure. Used by the watcher to reach a candidate whose terminal (and
|
|
4
|
+
* tracker.log) they are not looking at. It must never throw and never block:
|
|
5
|
+
* every path is a fire-and-forget execFile with a 5s timeout, and a missing
|
|
6
|
+
* binary (no notify-send, locked-down PowerShell) is simply a no-op.
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from "child_process";
|
|
9
|
+
/**
|
|
10
|
+
* The command for this platform, or null when there is nothing sensible to
|
|
11
|
+
* run. Pure — split out so tests can check construction (quote escaping)
|
|
12
|
+
* without spawning anything.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildNotifyCommand(platform: NodeJS.Platform, title: string, message: string): {
|
|
15
|
+
cmd: string;
|
|
16
|
+
args: string[];
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* Show a desktop notification. Never throws, never blocks the caller;
|
|
20
|
+
* failure of any kind (missing binary, DE without a notification daemon,
|
|
21
|
+
* headless session) is silently ignored — the notice file is the durable
|
|
22
|
+
* fallback surface.
|
|
23
|
+
*/
|
|
24
|
+
export declare function desktopNotify(title: string, message: string, exec?: typeof execFile): void;
|
|
25
|
+
//# sourceMappingURL=notify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/lib/notify.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAYxC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,CAAC,QAAQ,EACzB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,GACd;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CA4BxC;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,OAAO,QAAmB,GAC/B,IAAI,CAaN"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort desktop notification (ENG-1857) — zero dependencies, silent
|
|
3
|
+
* failure. Used by the watcher to reach a candidate whose terminal (and
|
|
4
|
+
* tracker.log) they are not looking at. It must never throw and never block:
|
|
5
|
+
* every path is a fire-and-forget execFile with a 5s timeout, and a missing
|
|
6
|
+
* binary (no notify-send, locked-down PowerShell) is simply a no-op.
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from "child_process";
|
|
9
|
+
/** AppleScript string literal: backslashes and double quotes escaped. */
|
|
10
|
+
function appleScriptString(s) {
|
|
11
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
12
|
+
}
|
|
13
|
+
/** PowerShell single-quoted string literal: single quotes doubled. */
|
|
14
|
+
function powerShellString(s) {
|
|
15
|
+
return `'${s.replace(/'/g, "''")}'`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The command for this platform, or null when there is nothing sensible to
|
|
19
|
+
* run. Pure — split out so tests can check construction (quote escaping)
|
|
20
|
+
* without spawning anything.
|
|
21
|
+
*/
|
|
22
|
+
export function buildNotifyCommand(platform, title, message) {
|
|
23
|
+
if (platform === "darwin") {
|
|
24
|
+
return {
|
|
25
|
+
cmd: "osascript",
|
|
26
|
+
args: ["-e", `display notification ${appleScriptString(message)} with title ${appleScriptString(title)}`],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (platform === "win32") {
|
|
30
|
+
// WinRT toast via PowerShell — present on every supported Windows, no
|
|
31
|
+
// module install needed. Text is injected as single-quoted PS literals
|
|
32
|
+
// and lands in XML text nodes (CreateTextNode), so no XML escaping is
|
|
33
|
+
// needed on top.
|
|
34
|
+
const script = [
|
|
35
|
+
"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null",
|
|
36
|
+
"$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)",
|
|
37
|
+
"$texts = $template.GetElementsByTagName('text')",
|
|
38
|
+
`$texts.Item(0).AppendChild($template.CreateTextNode(${powerShellString(title)})) | Out-Null`,
|
|
39
|
+
`$texts.Item(1).AppendChild($template.CreateTextNode(${powerShellString(message)})) | Out-Null`,
|
|
40
|
+
"$toast = [Windows.UI.Notifications.ToastNotification]::new($template)",
|
|
41
|
+
`[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier(${powerShellString("Litmus")}).Show($toast)`,
|
|
42
|
+
].join("; ");
|
|
43
|
+
return { cmd: "powershell", args: ["-NoProfile", "-NonInteractive", "-Command", script] };
|
|
44
|
+
}
|
|
45
|
+
if (platform === "linux") {
|
|
46
|
+
// notify-send if present; execFile's ENOENT is swallowed below when not.
|
|
47
|
+
return { cmd: "notify-send", args: [title, message] };
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Show a desktop notification. Never throws, never blocks the caller;
|
|
53
|
+
* failure of any kind (missing binary, DE without a notification daemon,
|
|
54
|
+
* headless session) is silently ignored — the notice file is the durable
|
|
55
|
+
* fallback surface.
|
|
56
|
+
*/
|
|
57
|
+
export function desktopNotify(title, message, exec = execFile) {
|
|
58
|
+
try {
|
|
59
|
+
const command = buildNotifyCommand(process.platform, title, message);
|
|
60
|
+
if (!command)
|
|
61
|
+
return;
|
|
62
|
+
const child = exec(command.cmd, command.args, { timeout: 5000, windowsHide: true }, () => { });
|
|
63
|
+
// Detach from the event loop so a slow notifier can't hold the watcher up.
|
|
64
|
+
child.unref?.();
|
|
65
|
+
}
|
|
66
|
+
catch { /* never let a notification failure surface */ }
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=notify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../src/lib/notify.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAExC,yEAAyE;AACzE,SAAS,iBAAiB,CAAC,CAAS;IAClC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAA;AAC7D,CAAC;AAED,sEAAsE;AACtE,SAAS,gBAAgB,CAAC,CAAS;IACjC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAA;AACrC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,QAAyB,EACzB,KAAa,EACb,OAAe;IAEf,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO;YACL,GAAG,EAAE,WAAW;YAChB,IAAI,EAAE,CAAC,IAAI,EAAE,wBAAwB,iBAAiB,CAAC,OAAO,CAAC,eAAe,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;SAC1G,CAAA;IACH,CAAC;IACD,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,sEAAsE;QACtE,uEAAuE;QACvE,sEAAsE;QACtE,iBAAiB;QACjB,MAAM,MAAM,GAAG;YACb,wHAAwH;YACxH,gJAAgJ;YAChJ,iDAAiD;YACjD,uDAAuD,gBAAgB,CAAC,KAAK,CAAC,eAAe;YAC7F,uDAAuD,gBAAgB,CAAC,OAAO,CAAC,eAAe;YAC/F,uEAAuE;YACvE,4EAA4E,gBAAgB,CAAC,QAAQ,CAAC,gBAAgB;SACvH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACZ,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,YAAY,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,CAAA;IAC3F,CAAC;IACD,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,yEAAyE;QACzE,OAAO,EAAE,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAA;IACvD,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAa,EACb,OAAe,EACf,OAAwB,QAAQ;IAEhC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,CAAC,CAAA;QACpE,IAAI,CAAC,OAAO;YAAE,OAAM;QACpB,MAAM,KAAK,GAAG,IAAI,CAChB,OAAO,CAAC,GAAG,EACX,OAAO,CAAC,IAAI,EACZ,EAAE,OAAO,EAAE,IAAK,EAAE,WAAW,EAAE,IAAI,EAAE,EACrC,GAAG,EAAE,GAAoD,CAAC,CAC3D,CAAA;QACD,2EAA2E;QAC3E,KAAK,CAAC,KAAK,EAAE,EAAE,CAAA;IACjB,CAAC;IAAC,MAAM,CAAC,CAAC,8CAA8C,CAAC,CAAC;AAC5D,CAAC"}
|
package/dist/lib/shadow.d.ts
CHANGED
|
@@ -77,6 +77,15 @@ export declare function deleteShadowForProject(projectDir: string): void;
|
|
|
77
77
|
*/
|
|
78
78
|
export declare function pruneShadows(registryPaths: string[]): number;
|
|
79
79
|
export declare const REPAIR_FILE_NAME = "LITMUS-REPAIR.md";
|
|
80
|
+
/** The repair file's title line. A LABEL, not an ownership proof — the file
|
|
81
|
+
* is world-readable, so any content can be copied into a look-alike.
|
|
82
|
+
* Ownership is the recorded content hash (notice-ownership.ts): write and
|
|
83
|
+
* remove below, and the watcher/zip via isCliNoticeArtifact, only ever touch
|
|
84
|
+
* a file byte-identical to what this CLI recorded writing (ENG-1857). A
|
|
85
|
+
* repair file left by an older CLI (no recorded hash) is treated as the
|
|
86
|
+
* candidate's: never removed, and it ships in the zip — the pre-ENG-1857
|
|
87
|
+
* status quo, and the safe direction when in doubt. */
|
|
88
|
+
export declare const REPAIR_FILE_MARKER = "# Litmus needs a one-minute repair";
|
|
80
89
|
/** Written by the watcher only when self-heal could not act (server
|
|
81
90
|
* unreachable, no usable shadow, AND the in-memory restore failed to write).
|
|
82
91
|
* Rewritten every heartbeat while the outage lasts — a cleanup pass that
|
package/dist/lib/shadow.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../src/lib/shadow.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../src/lib/shadow.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAG/C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,WAAW,YAAa,SAAQ,YAAY;IAChD;wEACoE;IACpE,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;CAClB;AAMD,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAOD,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGjE;AA6BD,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAcnF;AAiBD,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAQ9E;AAED,qEAAqE;AACrE,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAO9E;AAcD;;+DAE+D;AAC/D,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAS5F;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAU9E;AAUD,eAAO,MAAM,gBAAgB,gBAAgB,CAAA;AAE7C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,oEAAoE;AACpE,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC9D,IAAI,CASN;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAYlF;AAED,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,eAAe,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAOzH;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,eAAe,GAAG,MAAM,GAAG,IAAI,CAQhH;AAED,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,eAAe,GAAG,MAAM,GAAG,IAAI,CAQjH;AAID,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAQpE;AAED;+DAC+D;AAC/D,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAG/D;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAe5D;AAID,eAAO,MAAM,gBAAgB,qBAAqB,CAAA;AAElD;;;;;;;wDAOwD;AACxD,eAAO,MAAM,kBAAkB,uCAAuC,CAAA;AAEtE;;;;;;;;;6EAS6E;AAC7E,wBAAgB,iBAAiB,IAAI,MAAM,CAoB1C;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAUxD;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CASzD"}
|
package/dist/lib/shadow.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { clearOwnedNotice, isOwnedNoticeFile, recordOwnedNotice } from "./notice-ownership.js";
|
|
4
5
|
function litmusHome() {
|
|
5
6
|
return path.join(os.homedir(), ".litmus");
|
|
6
7
|
}
|
|
@@ -255,6 +256,15 @@ export function pruneShadows(registryPaths) {
|
|
|
255
256
|
}
|
|
256
257
|
// ── Candidate-facing repair file (last resort) ────────────────────
|
|
257
258
|
export const REPAIR_FILE_NAME = "LITMUS-REPAIR.md";
|
|
259
|
+
/** The repair file's title line. A LABEL, not an ownership proof — the file
|
|
260
|
+
* is world-readable, so any content can be copied into a look-alike.
|
|
261
|
+
* Ownership is the recorded content hash (notice-ownership.ts): write and
|
|
262
|
+
* remove below, and the watcher/zip via isCliNoticeArtifact, only ever touch
|
|
263
|
+
* a file byte-identical to what this CLI recorded writing (ENG-1857). A
|
|
264
|
+
* repair file left by an older CLI (no recorded hash) is treated as the
|
|
265
|
+
* candidate's: never removed, and it ships in the zip — the pre-ENG-1857
|
|
266
|
+
* status quo, and the safe direction when in doubt. */
|
|
267
|
+
export const REPAIR_FILE_MARKER = "# Litmus needs a one-minute repair";
|
|
258
268
|
/** Written by the watcher only when self-heal could not act (server
|
|
259
269
|
* unreachable, no usable shadow, AND the in-memory restore failed to write).
|
|
260
270
|
* Rewritten every heartbeat while the outage lasts — a cleanup pass that
|
|
@@ -267,7 +277,7 @@ export const REPAIR_FILE_NAME = "LITMUS-REPAIR.md";
|
|
|
267
277
|
* `litmus doctor` run bare prompts for the token pointing at that panel. */
|
|
268
278
|
export function repairFileContent() {
|
|
269
279
|
return [
|
|
270
|
-
|
|
280
|
+
REPAIR_FILE_MARKER,
|
|
271
281
|
"",
|
|
272
282
|
"Your assessment's tracking settings went missing, so your activity is not",
|
|
273
283
|
"being recorded and `litmus submit` will not work until they are restored.",
|
|
@@ -288,13 +298,26 @@ export function repairFileContent() {
|
|
|
288
298
|
}
|
|
289
299
|
export function writeRepairFile(projectDir) {
|
|
290
300
|
try {
|
|
291
|
-
|
|
301
|
+
const target = path.join(projectDir, REPAIR_FILE_NAME);
|
|
302
|
+
// Never clobber a file we didn't verifiably write ourselves (ENG-1857) —
|
|
303
|
+
// losing candidate work would be worse than a missing repair prompt.
|
|
304
|
+
if (fs.existsSync(target) && !isOwnedNoticeFile(projectDir, REPAIR_FILE_NAME))
|
|
305
|
+
return;
|
|
306
|
+
const content = repairFileContent();
|
|
307
|
+
fs.writeFileSync(target, content, "utf8");
|
|
308
|
+
recordOwnedNotice(projectDir, REPAIR_FILE_NAME, content);
|
|
292
309
|
}
|
|
293
310
|
catch { /* best effort */ }
|
|
294
311
|
}
|
|
295
312
|
export function removeRepairFile(projectDir) {
|
|
296
313
|
try {
|
|
297
|
-
|
|
314
|
+
const target = path.join(projectDir, REPAIR_FILE_NAME);
|
|
315
|
+
// Unlink only the byte-identical file WE recorded writing (ENG-1857): a
|
|
316
|
+
// candidate-owned or candidate-edited file must survive every cleanup path.
|
|
317
|
+
if (!isOwnedNoticeFile(projectDir, REPAIR_FILE_NAME))
|
|
318
|
+
return;
|
|
319
|
+
fs.rmSync(target, { force: true });
|
|
320
|
+
clearOwnedNotice(projectDir, REPAIR_FILE_NAME);
|
|
298
321
|
}
|
|
299
322
|
catch { /* best effort */ }
|
|
300
323
|
}
|
package/dist/lib/shadow.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shadow.js","sourceRoot":"","sources":["../../src/lib/shadow.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,IAAI,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"shadow.js","sourceRoot":"","sources":["../../src/lib/shadow.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,IAAI,MAAM,MAAM,CAAA;AAEvB,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAyC9F,SAAS,UAAU;IACjB,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAA;AAC3C,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,aAAa,CAAC,CAAA;AAC/C,CAAC;AAED,yEAAyE;AACzE,SAAS,MAAM,CAAC,YAAoB;IAClC,OAAO,sBAAsB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAA;AACxE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,YAAoB;IAC/C,MAAM,EAAE,GAAG,MAAM,CAAC,YAAY,CAAC,CAAA;IAC/B,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AACnD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,YAAoB;IAChD,MAAM,EAAE,GAAG,MAAM,CAAC,YAAY,CAAC,CAAA;IAC/B,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AACjE,CAAC;AAED;;qDAEqD;AACrD,SAAS,YAAY,CAAC,CAAS;IAC7B,IAAI,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;IAC1B,MAAM,IAAI,GAAa,EAAE,CAAA;IACzB,SAAS,CAAC;QACR,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAA;YAClC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QAChE,CAAC;QAAC,MAAM,CAAC,CAAC,qBAAqB,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QACjC,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;QAC3C,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;QAC9B,IAAI,GAAG,MAAM,CAAA;IACf,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,QAAgB,EAAE,OAAe;IACpD,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACzD,MAAM,GAAG,GAAG,GAAG,QAAQ,QAAQ,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAA;IAC1D,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;IACtC,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;AAC9B,CAAC;AAED,qEAAqE;AAErE,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB,CAAC,UAAkB,EAAE,MAAoB;IACxE,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IAC7C,IAAI,CAAC,GAAG;QAAE,OAAO,KAAK,CAAA;IACtB,MAAM,MAAM,GAAiB;QAC3B,GAAG,MAAM;QACT,UAAU,EAAE,YAAY,CAAC,UAAU,CAAC;QACpC,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACrC,CAAA;IACD,IAAI,CAAC;QACH,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;QAC3E,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,GAAW;IACpC,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,CAAC,GAAG,MAA+B,CAAA;IACzC,2EAA2E;IAC3E,KAAK,MAAM,GAAG,IAAI,CAAC,cAAc,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,CAAU,EAAE,CAAC;QAC3F,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,KAAK,QAAQ,IAAI,CAAE,CAAC,CAAC,GAAG,CAAY;YAAE,OAAO,IAAI,CAAA;IACpE,CAAC;IACD,OAAO,CAAiB,CAAA;AAC1B,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,YAAoB;IACvD,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,CAAC,CAAA;IACtC,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAA;IACrB,IAAI,CAAC;QACH,OAAO,iBAAiB,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,CAAC,CAAA;IAClF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,sBAAsB,CAAC,UAAkB;IACvD,MAAM,MAAM,GAAG,YAAY,CAAC,UAAU,CAAC,CAAA;IACvC,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,EAAE,CAAC;QACpC,MAAM,MAAM,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAA;QAC1C,IAAI,MAAM,IAAI,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,MAAM;YAAE,OAAO,MAAM,CAAA;IACzE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,aAAa;IACpB,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,WAAW,CAAC,aAAa,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;aAC5D,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;aAC9B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED,qEAAqE;AAErE;;+DAE+D;AAC/D,MAAM,UAAU,sBAAsB,CAAC,YAAoB,EAAE,GAAW,EAAE,IAAY;IACpF,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,CAAC,CAAA;IACtC,IAAI,CAAC,GAAG;QAAE,OAAM;IAChB,IAAI,CAAC;QACH,WAAW,CACT,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,EAC5B,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAwB,CAAC,CACzF,CAAA;IACH,CAAC;IAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,YAAoB;IACxD,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,CAAC,CAAA;IACtC,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAA;IACrB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAyB,CAAA;QACxG,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,IAAI;YAAE,OAAO,IAAI,CAAA;QAClG,OAAO,MAAqB,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,qEAAqE;AAErE,sEAAsE;AACtE,2EAA2E;AAC3E,6EAA6E;AAC7E,8EAA8E;AAC9E,oEAAoE;AACpE,4CAA4C;AAC5C,MAAM,CAAC,MAAM,gBAAgB,GAAG,aAAa,CAAA;AAS7C,oEAAoE;AACpE,MAAM,UAAU,qBAAqB,CACnC,YAAoB,EACpB,IAA+D;IAE/D,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,CAAC,CAAA;IACtC,IAAI,CAAC,GAAG;QAAE,OAAM;IAChB,IAAI,CAAC;QACH,WAAW,CACT,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,EAChC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAA6B,EAAE,IAAI,EAAE,CAAC,CAAC,CACrG,CAAA;IACH,CAAC;IAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,YAAoB;IACvD,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,CAAC,CAAA;IACtC,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAA;IACrB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,EAAE,MAAM,CAAC,CAA8B,CAAA;QACjH,KAAK,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,YAAY,CAAU,EAAE,CAAC;YACjE,IAAI,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC;gBAAE,OAAO,IAAI,CAAA;QAClE,CAAC;QACD,OAAO,MAA0B,CAAA;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,YAAoB,EAAE,IAAqC,EAAE,OAAe;IACjH,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,CAAC,CAAA;IACtC,IAAI,CAAC,GAAG;QAAE,OAAM;IAChB,IAAI,CAAC;QACH,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACtC,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAA;IACjD,CAAC;IAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,YAAoB,EAAE,IAAqC;IAC/F,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,CAAC,CAAA;IACtC,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAA;IACrB,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,YAAoB,EAAE,IAAqC;IAChG,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,CAAC,CAAA;IACtC,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAA;IACrB,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAA;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,qEAAqE;AAErE,MAAM,UAAU,yBAAyB,CAAC,YAAoB;IAC5D,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,CAAC,CAAA;IACtC,IAAI,CAAC,GAAG;QAAE,OAAM;IAChB,IAAI,CAAC;QAAC,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;IACpF,MAAM,OAAO,GAAG,aAAa,CAAC,YAAY,CAAC,CAAA;IAC3C,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,CAAC;YAAC,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;IACzE,CAAC;AACH,CAAC;AAED;+DAC+D;AAC/D,MAAM,UAAU,sBAAsB,CAAC,UAAkB;IACvD,MAAM,MAAM,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAA;IACjD,IAAI,MAAM;QAAE,yBAAyB,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;AAC5D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,aAAuB;IAClD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACrE,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,MAAM,EAAE,IAAI,aAAa,EAAE,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,oBAAoB,CAAC,EAAE,CAAC,CAAA;QACvC,IAAI,CAAC,MAAM;YAAE,SAAQ,CAAC,+DAA+D;QACrF,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;QAC3C,IAAI,SAAS,GAAG,KAAK,CAAA;QACrB,IAAI,CAAC;YAAC,SAAS,GAAG,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,sBAAsB,CAAC,CAAC;QACvE,IAAI,CAAC,SAAS,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACvC,yBAAyB,CAAC,EAAE,CAAC,CAAA;YAC7B,MAAM,EAAE,CAAA;QACV,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,qEAAqE;AAErE,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAA;AAElD;;;;;;;wDAOwD;AACxD,MAAM,CAAC,MAAM,kBAAkB,GAAG,oCAAoC,CAAA;AAEtE;;;;;;;;;6EAS6E;AAC7E,MAAM,UAAU,iBAAiB;IAC/B,OAAO;QACL,kBAAkB;QAClB,EAAE;QACF,2EAA2E;QAC3E,2EAA2E;QAC3E,EAAE;QACF,uEAAuE;QACvE,uEAAuE;QACvE,EAAE;QACF,KAAK;QACL,kCAAkC;QAClC,eAAe;QACf,KAAK;QACL,EAAE;QACF,uEAAuE;QACvE,yEAAyE;QACzE,qBAAqB;QACrB,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACd,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,UAAkB;IAChD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAA;QACtD,yEAAyE;QACzE,qEAAqE;QACrE,IAAI,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,UAAU,EAAE,gBAAgB,CAAC;YAAE,OAAM;QACrF,MAAM,OAAO,GAAG,iBAAiB,EAAE,CAAA;QACnC,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;QACzC,iBAAiB,CAAC,UAAU,EAAE,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAC1D,CAAC;IAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,UAAkB;IACjD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAA;QACtD,wEAAwE;QACxE,4EAA4E;QAC5E,IAAI,CAAC,iBAAiB,CAAC,UAAU,EAAE,gBAAgB,CAAC;YAAE,OAAM;QAC5D,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAClC,gBAAgB,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAA;IAChD,CAAC;IAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;AAC/B,CAAC"}
|
package/dist/lib/watcher.js
CHANGED
|
@@ -30,10 +30,12 @@ import { fetchInitMetadata, ServerError } from "./api.js";
|
|
|
30
30
|
import { ServerTimesSchema } from "./schemas.js";
|
|
31
31
|
import { detectInstalledHooks } from "./ai-tracking.js";
|
|
32
32
|
import { setupAiTracking } from "../commands/init.js";
|
|
33
|
-
import { writeShadowChainCursor, readShadowChainCursor, writeShadowTrackerFile, writeShadowReviveInfo, readShadowConfigById, findShadowConfigForDir, deleteShadowForAssessment, writeRepairFile, removeRepairFile, } from "./shadow.js";
|
|
33
|
+
import { writeShadowChainCursor, readShadowChainCursor, writeShadowTrackerFile, writeShadowReviveInfo, readShadowConfigById, findShadowConfigForDir, deleteShadowForAssessment, writeRepairFile, removeRepairFile, REPAIR_FILE_NAME, } from "./shadow.js";
|
|
34
34
|
import { createKeepAliveGuard } from "./keepalive.js";
|
|
35
35
|
import { getProcessList, getConnections, extractConnection, resolvePidNames } from "./platform.js";
|
|
36
36
|
import { classifyClaudeProcesses, refineClaudeTool } from "./detection.js";
|
|
37
|
+
import { decideNudge, loadNudgeState, saveNudgeState, initialPromptCursor, scanPromptCount, writeAiNoticeFile, removeAiNoticeFile, nudgeNotificationCopy, isCliNoticeArtifact, AI_NOTICE_FILE_NAME, } from "./capture-nudge.js";
|
|
38
|
+
import { desktopNotify } from "./notify.js";
|
|
37
39
|
// ENG-1491: an uncaught throw no longer always kills the watcher — on a
|
|
38
40
|
// multi-day assessment one bad callback used to silently end tracking until
|
|
39
41
|
// something respawned it. The guard keeps the two exits that are still
|
|
@@ -339,6 +341,7 @@ function uploadEvents() {
|
|
|
339
341
|
if (litmusConfig)
|
|
340
342
|
deleteShadowForAssessment(litmusConfig.assessmentId);
|
|
341
343
|
removeRepairFile(projectDir);
|
|
344
|
+
removeOwnedAiNotice(); // ENG-1857: session over — the nudge goes with it
|
|
342
345
|
shutdown();
|
|
343
346
|
}
|
|
344
347
|
}
|
|
@@ -411,8 +414,25 @@ const IGNORED = [
|
|
|
411
414
|
/(?:^|[/\\])venv[/\\]/,
|
|
412
415
|
/(?:^|[/\\])\.venv[/\\]/,
|
|
413
416
|
];
|
|
417
|
+
// Candidate-facing notice files this CLI writes at the assessment root
|
|
418
|
+
// (ENG-1857). They are guidance to the candidate, not candidate work, so
|
|
419
|
+
// they must never become judge-visible signals: not as `change` events from
|
|
420
|
+
// our own write (IGNORED via the check below), and not in the interview-
|
|
421
|
+
// agent snapshot (SNAPSHOT_IGNORE via the same check). LITMUS-REPAIR.md
|
|
422
|
+
// predates this feature and was missing from both lists — fixed alongside.
|
|
423
|
+
//
|
|
424
|
+
// The suppression is root-only and content-gated (see isCliNoticeArtifact):
|
|
425
|
+
// a nested file with the same basename — or a root file the candidate wrote
|
|
426
|
+
// themselves — is candidate work, and its events flow normally. The cache
|
|
427
|
+
// remembers the last ownership verdict per filename so the deletion of OUR
|
|
428
|
+
// notice (content no longer readable) is still classified as ours; it is
|
|
429
|
+
// seeded at boot for the respawn-then-remove case where this watcher never
|
|
430
|
+
// saw the file change.
|
|
431
|
+
const noticeOwnership = new Map();
|
|
432
|
+
isCliNoticeArtifact(projectDir, AI_NOTICE_FILE_NAME, noticeOwnership);
|
|
433
|
+
isCliNoticeArtifact(projectDir, REPAIR_FILE_NAME, noticeOwnership);
|
|
414
434
|
function shouldIgnore(relPath) {
|
|
415
|
-
return IGNORED.some((re) => re.test(relPath));
|
|
435
|
+
return isCliNoticeArtifact(projectDir, relPath, noticeOwnership) || IGNORED.some((re) => re.test(relPath));
|
|
416
436
|
}
|
|
417
437
|
// ── Hash chain for tamper evidence ──────────────────────────────
|
|
418
438
|
// Resume from the last chained event already in the log rather than restarting
|
|
@@ -973,6 +993,7 @@ const AI_TOOL_PATTERNS = {
|
|
|
973
993
|
codeium: /\bcodeium\b/,
|
|
974
994
|
codex: /\bcodex\b/,
|
|
975
995
|
copilot: /\bcopilot\b/,
|
|
996
|
+
gemini: /\bgemini\b/,
|
|
976
997
|
windsurf: /\bwindsurf\b/,
|
|
977
998
|
ollama: /\bollama\b/,
|
|
978
999
|
tabnine: /\btabnine\b/,
|
|
@@ -984,6 +1005,68 @@ const AI_TOOL_PATTERNS = {
|
|
|
984
1005
|
pearai: /\bpearai\b/,
|
|
985
1006
|
ghostwriter: /\bghostwriter\b/,
|
|
986
1007
|
};
|
|
1008
|
+
// ── Capture nudge (ENG-1857) ─────────────────────────────────────
|
|
1009
|
+
// A hook-capturable AI tool running with ZERO captured prompts is the silent
|
|
1010
|
+
// Codex/Copilot trust-gate signature: the candidate's AI work is invisibly
|
|
1011
|
+
// not being recorded, and they'd only find out from their report. Warn THEM,
|
|
1012
|
+
// live. Strictly candidate-facing: a desktop notification, a notice file at
|
|
1013
|
+
// the assessment root, and stderr (-> .litmus/tracker.log, excluded from all
|
|
1014
|
+
// grader signals via substrate.py's _is_capture_artifact). It must emit NO
|
|
1015
|
+
// activity event — the grader renders every non-response activity event
|
|
1016
|
+
// verbatim in the judge-visible timeline and merges the in-zip
|
|
1017
|
+
// activity.jsonl, so an event here would leak straight to reviewers.
|
|
1018
|
+
//
|
|
1019
|
+
// State persists in .litmus/nudge-state.json so the routine watcher respawns
|
|
1020
|
+
// (litmus status/doctor revive watchers) don't re-nag from scratch.
|
|
1021
|
+
let nudgeState = loadNudgeState(projectDir);
|
|
1022
|
+
// Incremental ai_prompt/ai_response counter over activity.jsonl: byte-offset
|
|
1023
|
+
// cursor, only appended bytes are read each cycle. A watcher restart rescans
|
|
1024
|
+
// from 0 — the same cost status.ts already pays on every run.
|
|
1025
|
+
let promptCursor = initialPromptCursor();
|
|
1026
|
+
/**
|
|
1027
|
+
* Remove OUR notice, if that is what sits at the path. The ownership cache
|
|
1028
|
+
* is refreshed first (while the content is still readable) so the unlink's
|
|
1029
|
+
* own file event is classified as ours and suppressed; a candidate-owned
|
|
1030
|
+
* file is left alone by removeAiNoticeFile's sentinel gate.
|
|
1031
|
+
*/
|
|
1032
|
+
function removeOwnedAiNotice() {
|
|
1033
|
+
isCliNoticeArtifact(projectDir, AI_NOTICE_FILE_NAME, noticeOwnership);
|
|
1034
|
+
removeAiNoticeFile(projectDir);
|
|
1035
|
+
}
|
|
1036
|
+
function runCaptureNudge(aiTools) {
|
|
1037
|
+
try {
|
|
1038
|
+
// Once anything was captured the latch is permanent; stop reading the log.
|
|
1039
|
+
if (promptCursor.count === 0 && !nudgeState.latched) {
|
|
1040
|
+
promptCursor = scanPromptCount(activityLogPath, promptCursor);
|
|
1041
|
+
}
|
|
1042
|
+
const deadlineMs = litmusConfig ? getEffectiveDeadline(litmusConfig) : null;
|
|
1043
|
+
const msRemaining = deadlineMs === null ? null : deadlineMs - Date.now();
|
|
1044
|
+
const decision = decideNudge(nudgeState, aiTools, promptCursor.count, Date.now(), msRemaining);
|
|
1045
|
+
nudgeState = decision.state;
|
|
1046
|
+
saveNudgeState(projectDir, nudgeState);
|
|
1047
|
+
if (decision.action === "nudge") {
|
|
1048
|
+
// Write first: when a candidate-owned file occupies the path we never
|
|
1049
|
+
// clobber it, and the notification then drops its "Details:" pointer
|
|
1050
|
+
// (which would name a file carrying THEIR content).
|
|
1051
|
+
const noticeWritten = writeAiNoticeFile(projectDir);
|
|
1052
|
+
if (noticeWritten) {
|
|
1053
|
+
isCliNoticeArtifact(projectDir, AI_NOTICE_FILE_NAME, noticeOwnership); // refresh ownership
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
process.stderr.write(`[watcher] ${AI_NOTICE_FILE_NAME} exists and is not ours; notification only\n`);
|
|
1057
|
+
}
|
|
1058
|
+
const copy = nudgeNotificationCopy(decision.tool, noticeWritten);
|
|
1059
|
+
desktopNotify(copy.title, copy.message);
|
|
1060
|
+
// stderr only — this lands in tracker.log, never in activity.jsonl.
|
|
1061
|
+
process.stderr.write(`[watcher] capture nudge fired (${decision.tool}, nudge ${nudgeState.nudgeCount}): AI tool present, zero prompts captured\n`);
|
|
1062
|
+
}
|
|
1063
|
+
else if (decision.action === "remove_notice") {
|
|
1064
|
+
removeOwnedAiNotice();
|
|
1065
|
+
process.stderr.write("[watcher] AI capture confirmed; nudge latched off\n");
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
catch { /* the nudge must never break the watcher */ }
|
|
1069
|
+
}
|
|
987
1070
|
function detectEnvironment() {
|
|
988
1071
|
try {
|
|
989
1072
|
const ps = getProcessList();
|
|
@@ -1011,6 +1094,11 @@ function detectEnvironment() {
|
|
|
1011
1094
|
event.aiTools = aiTools;
|
|
1012
1095
|
emit(event);
|
|
1013
1096
|
}
|
|
1097
|
+
// ENG-1857: warn the candidate when a hook-capturable tool is running but
|
|
1098
|
+
// nothing is being captured. Runs on EVERY scan (an empty aiTools is what
|
|
1099
|
+
// resets the consecutive-presence counters). Candidate-facing only — see
|
|
1100
|
+
// runCaptureNudge.
|
|
1101
|
+
runCaptureNudge(aiTools);
|
|
1014
1102
|
}
|
|
1015
1103
|
catch { /* process listing may fail, skip */ }
|
|
1016
1104
|
}
|
|
@@ -1071,6 +1159,9 @@ const AI_NETWORK_HOSTS = {
|
|
|
1071
1159
|
"claude.ai": "claude_web",
|
|
1072
1160
|
"generativelanguage.googleapis.com": "gemini_api",
|
|
1073
1161
|
"gemini.google.com": "gemini_web",
|
|
1162
|
+
// The Gemini CLI's default Code Assist endpoint (OAuth login path) — it only
|
|
1163
|
+
// hits generativelanguage.googleapis.com when the user brings their own key.
|
|
1164
|
+
"cloudcode-pa.googleapis.com": "gemini_cli",
|
|
1074
1165
|
"copilot-proxy.githubusercontent.com": "copilot",
|
|
1075
1166
|
"githubcopilot.com": "copilot",
|
|
1076
1167
|
"api2.cursor.sh": "cursor",
|
|
@@ -1219,7 +1310,9 @@ const SNAPSHOT_IGNORE = [
|
|
|
1219
1310
|
/id_ed25519(\.pub)?$/i,
|
|
1220
1311
|
];
|
|
1221
1312
|
function shouldIgnoreForSnapshot(relPath) {
|
|
1222
|
-
|
|
1313
|
+
// OUR root notice files (LITMUS-AI-NOTICE.md, LITMUS-REPAIR.md) never ride
|
|
1314
|
+
// the snapshot; candidate files by those names do — see noticeOwnership.
|
|
1315
|
+
return isCliNoticeArtifact(projectDir, relPath, noticeOwnership) || SNAPSHOT_IGNORE.some((re) => re.test(relPath));
|
|
1223
1316
|
}
|
|
1224
1317
|
function collectProjectFiles() {
|
|
1225
1318
|
const files = {};
|