litmus-cli 1.4.40 → 1.4.41

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 (41) hide show
  1. package/README.md +7 -0
  2. package/dist/commands/connect.d.ts +2 -0
  3. package/dist/commands/connect.d.ts.map +1 -1
  4. package/dist/commands/connect.js +21 -0
  5. package/dist/commands/connect.js.map +1 -1
  6. package/dist/commands/doctor.d.ts +20 -2
  7. package/dist/commands/doctor.d.ts.map +1 -1
  8. package/dist/commands/doctor.js +29 -5
  9. package/dist/commands/doctor.js.map +1 -1
  10. package/dist/commands/submit.d.ts.map +1 -1
  11. package/dist/commands/submit.js +1 -42
  12. package/dist/commands/submit.js.map +1 -1
  13. package/dist/index.js +3 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/lib/ai-tracking.d.ts +13 -0
  16. package/dist/lib/ai-tracking.d.ts.map +1 -1
  17. package/dist/lib/ai-tracking.js +71 -0
  18. package/dist/lib/ai-tracking.js.map +1 -1
  19. package/dist/lib/api.d.ts +23 -0
  20. package/dist/lib/api.d.ts.map +1 -1
  21. package/dist/lib/api.js +68 -0
  22. package/dist/lib/api.js.map +1 -1
  23. package/dist/lib/connect-skills.d.ts +39 -0
  24. package/dist/lib/connect-skills.d.ts.map +1 -0
  25. package/dist/lib/connect-skills.js +138 -0
  26. package/dist/lib/connect-skills.js.map +1 -0
  27. package/dist/lib/hook-logger.cjs +137 -7
  28. package/dist/lib/session-over.d.ts +32 -0
  29. package/dist/lib/session-over.d.ts.map +1 -0
  30. package/dist/lib/session-over.js +99 -0
  31. package/dist/lib/session-over.js.map +1 -0
  32. package/dist/lib/skill-import.d.ts +82 -0
  33. package/dist/lib/skill-import.d.ts.map +1 -0
  34. package/dist/lib/skill-import.js +228 -0
  35. package/dist/lib/skill-import.js.map +1 -0
  36. package/dist/lib/tracker.d.ts.map +1 -1
  37. package/dist/lib/tracker.js +10 -0
  38. package/dist/lib/tracker.js.map +1 -1
  39. package/dist/lib/watcher.js +8 -0
  40. package/dist/lib/watcher.js.map +1 -1
  41. package/package.json +1 -1
@@ -0,0 +1,138 @@
1
+ import { readFileSync } from "fs";
2
+ import readline from "readline";
3
+ import chalk from "chalk";
4
+ import { importSkills, planSkillImport, ServerError } from "./api.js";
5
+ import { info, success, warn } from "./errors.js";
6
+ import { collectSkillFiles, importSummaryLines, lastAnswered, overUploadCaps, planRequest, rememberAnswered, skillNames, skillSetDigest, SKILLS_PICKUP, } from "./skill-import.js";
7
+ const NAMES_SHOWN = 5;
8
+ const ASK_ATTEMPTS = 3;
9
+ export function describeSkills(names) {
10
+ const shown = names.slice(0, NAMES_SHOWN).join(", ");
11
+ const more = names.length - NAMES_SHOWN;
12
+ return more > 0 ? `${shown} and ${more} more` : shown;
13
+ }
14
+ function askLine(question) {
15
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
16
+ return new Promise((resolve) => rl.question(question, (a) => {
17
+ rl.close();
18
+ resolve(a);
19
+ }));
20
+ }
21
+ /** Enter or yes imports, no declines. Anything else is asked again, and never read as
22
+ * consent: after a few tries it is a no. */
23
+ async function askYesNo(ask, question) {
24
+ let q = question;
25
+ for (let i = 0; i < ASK_ATTEMPTS; i++) {
26
+ const a = (await ask(q)).trim().toLowerCase();
27
+ if (a === "" || a === "y" || a === "yes")
28
+ return true;
29
+ if (a === "n" || a === "no")
30
+ return false;
31
+ q = ` Please answer y or n. ${chalk.dim("[Y/n]")} `;
32
+ }
33
+ return false;
34
+ }
35
+ /** Not offered at all: the assessment does not allow it (403) or the backend has no
36
+ * such route (404). Nothing to tell the candidate. */
37
+ function notOffered(err) {
38
+ return err instanceof ServerError && (err.statusCode === 403 || err.statusCode === 404);
39
+ }
40
+ /**
41
+ * The server's plan for these files: one request for every source folder, so the
42
+ * server dedupes across them and applies the upload's skill cap to the whole set. A
43
+ * skill it cannot read is left out and named, with the server's reason, never the
44
+ * skills beside it. Null when there is nothing to offer.
45
+ */
46
+ async function planFiles(input, all) {
47
+ try {
48
+ const plan = await planSkillImport(input.backendUrl, input.token, planRequest(all));
49
+ for (const r of plan.refused)
50
+ warn(`Leaving out ~/.${r.path}: ${r.reason}`);
51
+ return plan.upload;
52
+ }
53
+ catch (err) {
54
+ if (notOffered(err)) {
55
+ if (input.verbose)
56
+ info(`Skill import not offered: ${err instanceof Error ? err.message : String(err)}`);
57
+ return null;
58
+ }
59
+ // A refusal of the set as a whole (over the skill cap, say): say why, offer nothing.
60
+ warn(`Couldn't offer your skills for import: ${err instanceof Error ? err.message : String(err)}`);
61
+ return null;
62
+ }
63
+ }
64
+ export async function offerSkillImport(input) {
65
+ if (input.flag === false)
66
+ return null;
67
+ const all = collectSkillFiles(input.home);
68
+ if (all.length === 0)
69
+ return null;
70
+ const upload = await planFiles(input, all);
71
+ if (!upload)
72
+ return null;
73
+ const wanted = new Set(upload);
74
+ const files = all.filter((f) => wanted.has(f.path));
75
+ const names = skillNames(upload);
76
+ if (names.length === 0)
77
+ return null;
78
+ const n = names.length;
79
+ const found = `Found ${n} AI skill${n === 1 ? "" : "s"} on this computer (${describeSkills(names)}).`;
80
+ const over = overUploadCaps(files);
81
+ if (over) {
82
+ info(`${found} Together they are ${over}, more than one import takes. Import the ones you want from your assessment page.`);
83
+ return null;
84
+ }
85
+ const pending = { files, names, digest: skillSetDigest(files), assessmentId: input.assessmentId };
86
+ if (input.flag === true)
87
+ return pending;
88
+ if (!(input.isTTY ?? process.stdin.isTTY)) {
89
+ info(`${found} Run with --skills to import ${n === 1 ? "it" : "them"} into your workspace.`);
90
+ return null;
91
+ }
92
+ // Asked once per set of skills: a candidate who reconnects every day is asked
93
+ // again only after they add or change one.
94
+ if (lastAnswered(input.assessmentId, input.home) === pending.digest)
95
+ return null;
96
+ const yes = await askYesNo(input.ask ?? askLine, `${found}\n Import ${n === 1 ? "it" : "them"} into your workspace? ${chalk.dim("[Y/n]")} `);
97
+ if (!yes) {
98
+ rememberAnswered(input.assessmentId, pending.digest, input.home);
99
+ info("Skipped. To import them later, add --skills to your `litmus connect` command, or use your assessment page.");
100
+ return null;
101
+ }
102
+ return pending;
103
+ }
104
+ /**
105
+ * Start the upload and return what to print when it is done. Never rejects: the
106
+ * workspace opens while this runs, and its outcome is reported at the end of connect.
107
+ */
108
+ export function startSkillImport(backendUrl, token, pending, home) {
109
+ info(`Importing your ${pending.names.length === 1 ? "skill" : "skills"} in the background...`);
110
+ return (async () => {
111
+ try {
112
+ const result = await importSkills(backendUrl, token, pending.files.map((f) => ({ path: f.path, data: readFileSync(f.abs) })));
113
+ rememberAnswered(pending.assessmentId, pending.digest, home);
114
+ const lines = importSummaryLines(result);
115
+ return () => {
116
+ if (result.installed.length > 0) {
117
+ success(lines[0]);
118
+ for (const line of lines.slice(1))
119
+ warn(line);
120
+ console.log(chalk.dim(` ${SKILLS_PICKUP}`));
121
+ }
122
+ else {
123
+ warn("No skills were imported.");
124
+ for (const line of lines)
125
+ warn(line);
126
+ }
127
+ };
128
+ }
129
+ catch (err) {
130
+ const message = err instanceof Error ? err.message : String(err);
131
+ return () => {
132
+ warn(`Couldn't import your skills: ${message}`);
133
+ info("Your workspace is fine. You can import skills from your assessment page instead.");
134
+ };
135
+ }
136
+ })();
137
+ }
138
+ //# sourceMappingURL=connect-skills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect-skills.js","sourceRoot":"","sources":["../../src/lib/connect-skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,QAAQ,MAAM,UAAU,CAAA;AAC/B,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACrE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,aAAa,GAEd,MAAM,mBAAmB,CAAA;AAkC1B,MAAM,WAAW,GAAG,CAAC,CAAA;AACrB,MAAM,YAAY,GAAG,CAAC,CAAA;AAEtB,MAAM,UAAU,cAAc,CAAC,KAAe;IAC5C,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACpD,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,WAAW,CAAA;IACvC,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAA;AACvD,CAAC;AAED,SAAS,OAAO,CAAC,QAAgB;IAC/B,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IACrF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;QAC1D,EAAE,CAAC,KAAK,EAAE,CAAA;QACV,OAAO,CAAC,CAAC,CAAC,CAAA;IACZ,CAAC,CAAC,CAAC,CAAA;AACL,CAAC;AAED;6CAC6C;AAC7C,KAAK,UAAU,QAAQ,CAAC,GAAmC,EAAE,QAAgB;IAC3E,IAAI,CAAC,GAAG,QAAQ,CAAA;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;QAC7C,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,KAAK;YAAE,OAAO,IAAI,CAAA;QACrD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI;YAAE,OAAO,KAAK,CAAA;QACzC,CAAC,GAAG,2BAA2B,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAA;IACtD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;uDACuD;AACvD,SAAS,UAAU,CAAC,GAAY;IAC9B,OAAO,GAAG,YAAY,WAAW,IAAI,CAAC,GAAG,CAAC,UAAU,KAAK,GAAG,IAAI,GAAG,CAAC,UAAU,KAAK,GAAG,CAAC,CAAA;AACzF,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,SAAS,CAAC,KAAsB,EAAE,GAAqB;IACpE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,eAAe,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,KAAK,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAA;QACnF,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO;YAAE,IAAI,CAAC,kBAAkB,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;QAC3E,OAAO,IAAI,CAAC,MAAM,CAAA;IACpB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACpB,IAAI,KAAK,CAAC,OAAO;gBAAE,IAAI,CAAC,6BAA6B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YACxG,OAAO,IAAI,CAAA;QACb,CAAC;QACD,qFAAqF;QACrF,IAAI,CAAC,0CAA0C,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAClG,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAAsB;IAC3D,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,IAAI,CAAA;IACrC,MAAM,GAAG,GAAG,iBAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACzC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAEjC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAC1C,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IACxB,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAA;IAC9B,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IACnD,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;IAChC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAEnC,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAA;IACtB,MAAM,KAAK,GAAG,SAAS,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,sBAAsB,cAAc,CAAC,KAAK,CAAC,IAAI,CAAA;IACrG,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,CAAA;IAClC,IAAI,IAAI,EAAE,CAAC;QACT,IAAI,CAAC,GAAG,KAAK,sBAAsB,IAAI,mFAAmF,CAAC,CAAA;QAC3H,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,OAAO,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,CAAA;IACjG,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;QAAE,OAAO,OAAO,CAAA;IAEvC,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC,GAAG,KAAK,gCAAgC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,uBAAuB,CAAC,CAAA;QAC5F,OAAO,IAAI,CAAA;IACb,CAAC;IACD,8EAA8E;IAC9E,2CAA2C;IAC3C,IAAI,YAAY,CAAC,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAEhF,MAAM,GAAG,GAAG,MAAM,QAAQ,CACxB,KAAK,CAAC,GAAG,IAAI,OAAO,EACpB,GAAG,KAAK,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,yBAAyB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAC5F,CAAA;IACD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,gBAAgB,CAAC,KAAK,CAAC,YAAY,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;QAChE,IAAI,CAAC,4GAA4G,CAAC,CAAA;QAClH,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAAkB,EAClB,KAAa,EACb,OAA2B,EAC3B,IAAa;IAEb,IAAI,CAAC,kBAAkB,OAAO,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,uBAAuB,CAAC,CAAA;IAC9F,OAAO,CAAC,KAAK,IAAI,EAAE;QACjB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,YAAY,CAC/B,UAAU,EACV,KAAK,EACL,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CACxE,CAAA;YACD,gBAAgB,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;YAC5D,MAAM,KAAK,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAA;YACxC,OAAO,GAAG,EAAE;gBACV,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAChC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;oBACjB,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;wBAAE,IAAI,CAAC,IAAI,CAAC,CAAA;oBAC7C,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,aAAa,EAAE,CAAC,CAAC,CAAA;gBAC9C,CAAC;qBAAM,CAAC;oBACN,IAAI,CAAC,0BAA0B,CAAC,CAAA;oBAChC,KAAK,MAAM,IAAI,IAAI,KAAK;wBAAE,IAAI,CAAC,IAAI,CAAC,CAAA;gBACtC,CAAC;YACH,CAAC,CAAA;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YAChE,OAAO,GAAG,EAAE;gBACV,IAAI,CAAC,gCAAgC,OAAO,EAAE,CAAC,CAAA;gBAC/C,IAAI,CAAC,kFAAkF,CAAC,CAAA;YAC1F,CAAC,CAAA;QACH,CAAC;IACH,CAAC,CAAC,EAAE,CAAA;AACN,CAAC"}
@@ -465,6 +465,96 @@ function versionHeader(config) {
465
465
  : {}
466
466
  }
467
467
 
468
+ // ── Session over (ENG-2768) ──────────────────────────────────────
469
+ // Only `litmus submit` releases the hooks and the registry. A session that
470
+ // ends any other way (web submit, deadline, the company ending it) leaves the
471
+ // hooks live, and the walk-up above finds the folder's config whatever the
472
+ // registry says, so every AI turn in an ended assessment folder kept sending
473
+ // the candidate's prompt and reply to a server that answers `400 Session over`
474
+ // every time: 12,229 of them over three days in production.
475
+ //
476
+ // So the refusal is remembered in the folder. The first refusal writes
477
+ // `.litmus/session-over`, and every later invocation that resolves to that
478
+ // folder stops before it records, appends, uploads or revives anything. The
479
+ // watcher writes the same marker when IT is told the session is over (see
480
+ // watcher.ts), which usually comes first.
481
+ //
482
+ // Only a refusal that can never turn into an acceptance counts: "Session over"
483
+ // (which `/cli/activity` also returns for a token that matches no candidate,
484
+ // ENG-2749) and "Invalid token" (its older spelling there, and other routes').
485
+ // A 5xx, a timeout or a 429 is transient and must never silence capture of a
486
+ // live session.
487
+ //
488
+ // The marker names the token it was written for, as a hash. A folder re-used
489
+ // by a later `litmus init` carries a new token, and the old marker does not
490
+ // silence it. A marker that names no token (unreadable, or hand-made) still
491
+ // silences: it can only ever stop capture, never start it.
492
+ //
493
+ // The same hash is also written as an empty file under
494
+ // ~/.litmus/sessions-over/, because the folder may not be writable, and a
495
+ // folder we could not mark would otherwise keep uploading for as long as
496
+ // another assessment keeps the user-scope hooks installed. Either copy is
497
+ // enough.
498
+ const SESSION_OVER_MARKER = "session-over"
499
+ const SESSIONS_OVER_DIR = path.join(os.homedir() || os.tmpdir(), ".litmus", "sessions-over")
500
+ const MAX_REFUSAL_BODY_BYTES = 16 * 1024
501
+ const FINAL_REFUSAL_CODES = new Set(["session over", "invalid token"])
502
+
503
+ function isFinalRefusal(status, rawBody) {
504
+ if (status < 400 || status >= 500 || status === 429) return false
505
+ let body
506
+ try { body = JSON.parse(rawBody) } catch { return false }
507
+ const detail = body && body.detail
508
+ const code = typeof detail === "string"
509
+ ? detail
510
+ : detail && typeof detail === "object" && typeof detail.error === "string" ? detail.error : null
511
+ return typeof code === "string" && FINAL_REFUSAL_CODES.has(code.trim().toLowerCase())
512
+ }
513
+
514
+ function tokenDigest(token) {
515
+ return typeof token === "string" && token
516
+ ? crypto.createHash("sha256").update(token).digest("hex")
517
+ : null
518
+ }
519
+
520
+ function writeSessionOverMarker(litmusDir, config, reason) {
521
+ const digest = tokenDigest(config && config.token)
522
+ try {
523
+ fs.writeFileSync(path.join(litmusDir, SESSION_OVER_MARKER), JSON.stringify({
524
+ reason,
525
+ at: new Date().toISOString(),
526
+ tokenSha256: digest,
527
+ }) + "\n")
528
+ } catch { /* unwritable folder; the home copy below still counts */ }
529
+ if (!digest) return
530
+ try {
531
+ fs.mkdirSync(SESSIONS_OVER_DIR, { recursive: true })
532
+ fs.writeFileSync(path.join(SESSIONS_OVER_DIR, digest), "")
533
+ } catch { /* best effort; the next refusal tries again */ }
534
+ }
535
+
536
+ function sessionOverMarked(litmusDir) {
537
+ const current = tokenDigest((readConfig(litmusDir) || {}).token)
538
+ if (current) {
539
+ try {
540
+ if (fs.statSync(path.join(SESSIONS_OVER_DIR, current)).isFile()) return true
541
+ } catch { /* not recorded at home */ }
542
+ }
543
+ let raw
544
+ try {
545
+ raw = fs.readFileSync(path.join(litmusDir, SESSION_OVER_MARKER), "utf8")
546
+ } catch {
547
+ return false
548
+ }
549
+ let marked = null
550
+ try {
551
+ const parsed = JSON.parse(raw)
552
+ if (parsed && typeof parsed.tokenSha256 === "string" && parsed.tokenSha256) marked = parsed.tokenSha256
553
+ } catch { /* names no token */ }
554
+ if (!marked) return true
555
+ return !current || current === marked
556
+ }
557
+
468
558
  /**
469
559
  * POST the event to /cli/activity and WAIT for it to complete, bounded.
470
560
  *
@@ -474,16 +564,17 @@ function versionHeader(config) {
474
564
  * the in-zip activity.jsonl) kept working, so nothing surfaced it.
475
565
  *
476
566
  * Resolves rather than rejects on every failure path: a telemetry upload must
477
- * never fail a candidate's prompt. Both timers are cleared on settle.
567
+ * never fail a candidate's prompt. Both timers are cleared on settle. Resolves
568
+ * `{ sessionOver }`, true only when the server refused the token for good.
478
569
  */
479
570
  function uploadEvent(config, event) {
480
- if (!config || !config.token || !config.backendUrl) return Promise.resolve()
571
+ if (!config || !config.token || !config.backendUrl) return Promise.resolve({ sessionOver: false })
481
572
 
482
573
  let url
483
574
  try {
484
575
  url = new URL(`${config.backendUrl}/cli/activity`)
485
576
  } catch {
486
- return Promise.resolve() // malformed backendUrl — nothing to do
577
+ return Promise.resolve({ sessionOver: false }) // malformed backendUrl — nothing to do
487
578
  }
488
579
 
489
580
  const body = JSON.stringify({ events: [event] })
@@ -492,11 +583,12 @@ function uploadEvent(config, event) {
492
583
  return new Promise((resolve) => {
493
584
  let settled = false
494
585
  let guard = null
586
+ let sessionOver = false
495
587
  const done = () => {
496
588
  if (settled) return
497
589
  settled = true
498
590
  if (guard) clearTimeout(guard)
499
- resolve()
591
+ resolve({ sessionOver })
500
592
  }
501
593
 
502
594
  const req = reqFn(url, {
@@ -518,7 +610,24 @@ function uploadEvent(config, event) {
518
610
  timeout: UPLOAD_TIMEOUT_MS,
519
611
  })
520
612
  // Drain the response so the socket can close and the event loop empty.
521
- req.on("response", (res) => { res.resume(); res.on("end", done); res.on("error", done) })
613
+ // A 4xx body is kept (capped) because it is the only place the server says
614
+ // the assessment is over for good (ENG-2768, see isFinalRefusal).
615
+ req.on("response", (res) => {
616
+ const status = res.statusCode || 0
617
+ const chunks = []
618
+ let kept = 0
619
+ res.on("data", (chunk) => {
620
+ if (status >= 400 && status < 500 && kept < MAX_REFUSAL_BODY_BYTES) {
621
+ chunks.push(chunk)
622
+ kept += chunk.length
623
+ }
624
+ })
625
+ res.on("end", () => {
626
+ if (chunks.length) sessionOver = isFinalRefusal(status, Buffer.concat(chunks).toString("utf8"))
627
+ done()
628
+ })
629
+ res.on("error", done)
630
+ })
522
631
  req.on("error", done) // network down, DNS, refused, destroyed
523
632
  req.on("timeout", () => { req.destroy(); done() })
524
633
  // Backstop in case none of the above fires (a socket wedged before connect).
@@ -1775,6 +1884,15 @@ async function main() {
1775
1884
  try { parsed = JSON.parse(raw) } catch { /* extractPrompt reports the failure */ }
1776
1885
  const cursorShaped = isCursorPayload(parsed)
1777
1886
 
1887
+ // ENG-2768: an assessment whose session is over records nothing more. Asked
1888
+ // here, before any transcript read, against the same placement the event
1889
+ // would get (the payload's workspace roots are the only roots any tool's
1890
+ // extractor reports), and asked again below once the event's own placement
1891
+ // is known.
1892
+ const earlyRoots = parsed && Array.isArray(parsed.workspace_roots) ? parsed.workspace_roots : []
1893
+ const earlyLitmusDir = resolveAssessmentDir(parsed, earlyRoots)
1894
+ if (earlyLitmusDir && sessionOverMarked(earlyLitmusDir)) return
1895
+
1778
1896
  // ENG-2477: a capture SELF-TEST probe. `litmus init` (and `litmus doctor`)
1779
1897
  // run this script the way the tool would — the tool's own payload shape, on
1780
1898
  // stdin, through the command string in the tool's own config file — plus
@@ -1826,7 +1944,7 @@ async function main() {
1826
1944
  if (effectiveTool === "cursor" && parsed && parsed.hook_event_name === CURSOR_EDIT_EVENT) {
1827
1945
  const roots = Array.isArray(parsed.workspace_roots) ? parsed.workspace_roots : []
1828
1946
  const editLitmusDir = resolveAssessmentDir(parsed, roots)
1829
- if (!editLitmusDir) return
1947
+ if (!editLitmusDir || sessionOverMarked(editLitmusDir)) return
1830
1948
  recordCursorFileEdit(parsed, parsed.session_id || parsed.conversation_id || null,
1831
1949
  path.dirname(editLitmusDir))
1832
1950
  return
@@ -1864,6 +1982,7 @@ async function main() {
1864
1982
 
1865
1983
  const litmusDir = resolveAssessmentDir(parsed, workspaceRoots)
1866
1984
  if (!litmusDir) return
1985
+ if (litmusDir !== earlyLitmusDir && sessionOverMarked(litmusDir)) return
1867
1986
 
1868
1987
  // Claim as LATE as possible — only once we know we have content and somewhere
1869
1988
  // to put it. Claiming earlier would let an invocation that then bails (no
@@ -1993,7 +2112,15 @@ async function main() {
1993
2112
 
1994
2113
  // 2. Upload to the server. Awaited, bounded — see uploadEvent.
1995
2114
  const config = readConfig(litmusDir)
1996
- await uploadEvent(config, event)
2115
+ const upload = await uploadEvent(config, event)
2116
+
2117
+ // ENG-2768: the server says this token will never be accepted again. Mark
2118
+ // the folder so the next turn stops at the top of main(), and skip the
2119
+ // revival below: there is no session left for a watcher to serve.
2120
+ if (upload.sessionOver) {
2121
+ writeSessionOverMarker(litmusDir, config, "server refused the token")
2122
+ return
2123
+ }
1997
2124
 
1998
2125
  // 3. Revive a dead watcher (ENG-1491). Strictly after the capture/upload
1999
2126
  // work — this is a courtesy, and it must never cost the prompt anything.
@@ -2008,6 +2135,9 @@ async function main() {
2008
2135
  // Exported for unit tests only; every hook invocation runs this file directly.
2009
2136
  module.exports = {
2010
2137
  watcherPresumedDead,
2138
+ isFinalRefusal,
2139
+ sessionOverMarked,
2140
+ SESSION_OVER_MARKER,
2011
2141
  reviveThrottleAllows,
2012
2142
  buildDrainFrame,
2013
2143
  trimEventToBytes,
@@ -0,0 +1,32 @@
1
+ /**
2
+ * ENG-2768: the "this session is over" marker the hook logger reads at the top
3
+ * of every invocation. Kept in step with `writeSessionOverMarker` and
4
+ * `sessionOverMarked` in hook-logger.cjs (that file is standalone CommonJS and
5
+ * cannot import this one): same names, same JSON shape.
6
+ *
7
+ * Two copies, either one enough. `.litmus/session-over` names the token as a
8
+ * sha256 hash, so a later `litmus init` into the same folder, which carries a
9
+ * new token, is not silenced by it. `~/.litmus/sessions-over/<hash>` is the
10
+ * same fact recorded outside the folder, for a folder that cannot be written.
11
+ */
12
+ export declare const SESSION_OVER_MARKER = "session-over";
13
+ export declare function sessionsOverDir(): string;
14
+ export declare function writeSessionOverMarker(projectDir: string, token: string | undefined, reason: string): boolean;
15
+ /** Has the session this folder's config belongs to been marked over? */
16
+ export declare function sessionOverMarked(projectDir: string): boolean;
17
+ /**
18
+ * What the watcher does with its own AI capture when the server says the
19
+ * session is over. The marker silences the hook logger in this folder even
20
+ * while another assessment keeps the user-scope hooks installed; the release
21
+ * drops the registry entry, the folder's own hooks and trust entries, and the
22
+ * user-scope hooks once no assessment is left. Only `litmus submit` used to do
23
+ * the release, so a session that ended any other way kept a live hook.
24
+ *
25
+ * Run by the watcher because it is alive exactly when the session ends, so no
26
+ * hook writer races it (the ENG-1289 concurrency problem). Marker first: it
27
+ * alone already stops capture, so a release that throws halfway loses nothing.
28
+ * The marker also keeps startTracker from bringing a watcher back here, whose
29
+ * hook self-heal would otherwise reinstall everything this just released.
30
+ */
31
+ export declare function retireAiCaptureForEndedSession(projectDir: string, token: string | undefined): void;
32
+ //# sourceMappingURL=session-over.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-over.d.ts","sourceRoot":"","sources":["../../src/lib/session-over.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,iBAAiB,CAAA;AAEjD,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAMD,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAmB7G;AAED,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAuB7D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,8BAA8B,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGlG"}
@@ -0,0 +1,99 @@
1
+ import { mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { createHash } from "crypto";
5
+ import { releaseAiTrackingForAssessment } from "./ai-tracking.js";
6
+ /**
7
+ * ENG-2768: the "this session is over" marker the hook logger reads at the top
8
+ * of every invocation. Kept in step with `writeSessionOverMarker` and
9
+ * `sessionOverMarked` in hook-logger.cjs (that file is standalone CommonJS and
10
+ * cannot import this one): same names, same JSON shape.
11
+ *
12
+ * Two copies, either one enough. `.litmus/session-over` names the token as a
13
+ * sha256 hash, so a later `litmus init` into the same folder, which carries a
14
+ * new token, is not silenced by it. `~/.litmus/sessions-over/<hash>` is the
15
+ * same fact recorded outside the folder, for a folder that cannot be written.
16
+ */
17
+ export const SESSION_OVER_MARKER = "session-over";
18
+ export function sessionsOverDir() {
19
+ return path.join(os.homedir() || os.tmpdir(), ".litmus", "sessions-over");
20
+ }
21
+ function tokenDigest(token) {
22
+ return typeof token === "string" && token ? createHash("sha256").update(token).digest("hex") : null;
23
+ }
24
+ export function writeSessionOverMarker(projectDir, token, reason) {
25
+ const digest = tokenDigest(token);
26
+ let written = false;
27
+ try {
28
+ writeFileSync(path.join(projectDir, ".litmus", SESSION_OVER_MARKER), JSON.stringify({
29
+ reason,
30
+ at: new Date().toISOString(),
31
+ tokenSha256: digest,
32
+ }) + "\n");
33
+ written = true;
34
+ }
35
+ catch { /* unwritable folder; the home copy below still counts */ }
36
+ if (digest) {
37
+ try {
38
+ mkdirSync(sessionsOverDir(), { recursive: true });
39
+ writeFileSync(path.join(sessionsOverDir(), digest), "");
40
+ written = true;
41
+ }
42
+ catch { /* best effort */ }
43
+ }
44
+ return written;
45
+ }
46
+ /** Has the session this folder's config belongs to been marked over? */
47
+ export function sessionOverMarked(projectDir) {
48
+ let current = null;
49
+ try {
50
+ current = tokenDigest(JSON.parse(readFileSync(path.join(projectDir, ".litmus", "config.json"), "utf8")).token);
51
+ }
52
+ catch { /* no readable config */ }
53
+ if (current) {
54
+ try {
55
+ if (statSync(path.join(sessionsOverDir(), current)).isFile())
56
+ return true;
57
+ }
58
+ catch { /* not recorded at home */ }
59
+ }
60
+ let raw;
61
+ try {
62
+ raw = readFileSync(path.join(projectDir, ".litmus", SESSION_OVER_MARKER), "utf8");
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ let marked = null;
68
+ try {
69
+ const parsed = JSON.parse(raw);
70
+ if (typeof parsed?.tokenSha256 === "string" && parsed.tokenSha256)
71
+ marked = parsed.tokenSha256;
72
+ }
73
+ catch { /* names no token */ }
74
+ if (!marked)
75
+ return true;
76
+ return !current || current === marked;
77
+ }
78
+ /**
79
+ * What the watcher does with its own AI capture when the server says the
80
+ * session is over. The marker silences the hook logger in this folder even
81
+ * while another assessment keeps the user-scope hooks installed; the release
82
+ * drops the registry entry, the folder's own hooks and trust entries, and the
83
+ * user-scope hooks once no assessment is left. Only `litmus submit` used to do
84
+ * the release, so a session that ended any other way kept a live hook.
85
+ *
86
+ * Run by the watcher because it is alive exactly when the session ends, so no
87
+ * hook writer races it (the ENG-1289 concurrency problem). Marker first: it
88
+ * alone already stops capture, so a release that throws halfway loses nothing.
89
+ * The marker also keeps startTracker from bringing a watcher back here, whose
90
+ * hook self-heal would otherwise reinstall everything this just released.
91
+ */
92
+ export function retireAiCaptureForEndedSession(projectDir, token) {
93
+ writeSessionOverMarker(projectDir, token, "watcher: server signaled session over");
94
+ try {
95
+ releaseAiTrackingForAssessment(projectDir);
96
+ }
97
+ catch { /* best effort */ }
98
+ }
99
+ //# sourceMappingURL=session-over.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-over.js","sourceRoot":"","sources":["../../src/lib/session-over.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * ENG-2814 — find the candidate's own AI tool skills on THIS machine, for `litmus
3
+ * connect` to offer to import into their workspace.
4
+ *
5
+ * Runs on the laptop because that is the only place the skills are: the workspace
6
+ * cannot see the candidate's home. The upload goes to the same `/workspace/skills`
7
+ * routes the assessment page's Import skills dialog uses, and NOTHING about which
8
+ * files are skills is decided here: `POST /workspace/skills/plan` runs the upload's
9
+ * own parser over the paths and SKILL.md heads and answers with the files to send
10
+ * (`backend/services/candidate_skills.py`). One parser cannot disagree with itself.
11
+ *
12
+ * Every folder an AI tool reads Agent Skills (`<name>/SKILL.md`) from is looked in.
13
+ * A folder that does not exist costs nothing; a skill found in two of them (a
14
+ * `.claude/skills` copy of an `.agents/skills` one, say) is imported once, and the
15
+ * server reports the other as a duplicate.
16
+ */
17
+ export declare const SKILL_SOURCES: ReadonlyArray<{
18
+ dir: string;
19
+ tools: string;
20
+ }>;
21
+ export declare const PLAN_HEAD_CHARS: number;
22
+ export interface LocalSkillFile {
23
+ /** Sent to the server. The source folder without its dot (`claude/skills/x/SKILL.md`):
24
+ * the server skips hidden folders other than the ones it knows hold skills, and
25
+ * `.cursor` or `.gemini` are not among them. */
26
+ path: string;
27
+ abs: string;
28
+ size: number;
29
+ mtimeMs: number;
30
+ }
31
+ /**
32
+ * Every file under each skills folder that exists.
33
+ *
34
+ * A link is followed only where it stays inside what the candidate agreed to share.
35
+ * An entry directly under a skills folder may be a link (a skill kept in dotfiles and
36
+ * linked in is common), and it sets the boundary: below it, a file or folder whose
37
+ * real path leaves that entry is not collected, so a link in a skill cannot carry an
38
+ * unrelated file off the laptop under the skill's name.
39
+ */
40
+ export declare function collectSkillFiles(home?: string): LocalSkillFile[];
41
+ export declare const MAX_UPLOAD_BYTES: number;
42
+ export declare const MAX_UPLOAD_FILES = 1000;
43
+ /** Why these files cannot be uploaded in one import, or null when they fit. */
44
+ export declare function overUploadCaps(files: LocalSkillFile[]): string | null;
45
+ export interface SkillPlanRequest {
46
+ /** Leave out a skill the server cannot read and name it in `refused`, rather than
47
+ * refusing every skill over one malformed SKILL.md. */
48
+ skipInvalid: true;
49
+ paths: string[];
50
+ skillFiles: Record<string, string>;
51
+ unreadable: string[];
52
+ truncated: string[];
53
+ }
54
+ /** The plan route's body: every path, and the head of each SKILL.md. */
55
+ export declare function planRequest(files: LocalSkillFile[]): SkillPlanRequest;
56
+ /** The skill folder names among the paths the server said to send, for the prompt. */
57
+ export declare function skillNames(kept: string[]): string[];
58
+ /**
59
+ * What the candidate was last asked about, per assessment, so a candidate who
60
+ * reconnects every day of a work trial is asked again only when their skills changed.
61
+ * A digest of the kept files' paths, sizes and modification times: cheap, and any
62
+ * edit to a skill changes it.
63
+ */
64
+ export declare function skillSetDigest(files: LocalSkillFile[]): string;
65
+ export declare function lastAnswered(assessmentId: string, home?: string): string | null;
66
+ /** Best effort: failing to remember only means being asked again next time. */
67
+ export declare function rememberAnswered(assessmentId: string, digest: string, home?: string): void;
68
+ export interface SkillImportResult {
69
+ installed: string[];
70
+ skipped: {
71
+ name: string;
72
+ dest: string;
73
+ reason: string;
74
+ }[];
75
+ ignored: string[];
76
+ duplicates: string[];
77
+ aider: string | null;
78
+ }
79
+ /** What the import did, in the same words the assessment page's dialog uses. */
80
+ export declare function importSummaryLines(result: SkillImportResult): string[];
81
+ export declare const SKILLS_PICKUP = "Start a new Claude Code or Codex session to use them. In Copilot, run /skills reload.";
82
+ //# sourceMappingURL=skill-import.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-import.d.ts","sourceRoot":"","sources":["../../src/lib/skill-import.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,EAAE,aAAa,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAOvE,CAAA;AAUD,eAAO,MAAM,eAAe,QAAY,CAAA;AAExC,MAAM,WAAW,cAAc;IAC7B;;qDAEiD;IACjD,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAQD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,MAAqB,GAAG,cAAc,EAAE,CAiD/E;AAID,eAAO,MAAM,gBAAgB,QAAmB,CAAA;AAChD,eAAO,MAAM,gBAAgB,OAAO,CAAA;AAEpC,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,MAAM,GAAG,IAAI,CAKrE;AAED,MAAM,WAAW,gBAAgB;IAC/B;4DACwD;IACxD,WAAW,EAAE,IAAI,CAAA;IACjB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,gBAAgB,CAkBrE;AAED,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAOnD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,MAAM,CAM9D;AAMD,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,GAAE,MAAqB,GAAG,MAAM,GAAG,IAAI,CAQ7F;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,MAAqB,GAAG,IAAI,CAexG;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACzD,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAQD,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,EAAE,CAgBtE;AAED,eAAO,MAAM,aAAa,0FAA0F,CAAA"}