terminalhire 0.18.1 → 0.18.2

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.
@@ -391,6 +391,11 @@ ${body}`;
391
391
  for (const tok of tokens) {
392
392
  const r = resolveToken(tok);
393
393
  if (!r) continue;
394
+ const synGate = AMBIGUOUS_SYNONYM[tok] ?? AMBIGUOUS_SYNONYM[tok.replace(/^[.\-+#]+|[.\-+#]+$/g, "")];
395
+ if (synGate && r.id === synGate.id) {
396
+ if (synGate.cue.test(text)) ids.add(r.id);
397
+ continue;
398
+ }
394
399
  if (NON_EXTRACTABLE.has(r.id)) continue;
395
400
  if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
396
401
  const cue = AMBIGUOUS[r.id];
@@ -408,7 +413,7 @@ ${body}`;
408
413
  function coreTagsFromTitle(title) {
409
414
  return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
410
415
  }
411
- var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
416
+ var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, AMBIGUOUS_SYNONYM, ENG_INTENT, NON_ENG_TITLE;
412
417
  var init_extract = __esm({
413
418
  "../../packages/core/src/vocab/extract.ts"() {
414
419
  "use strict";
@@ -445,6 +450,16 @@ var init_extract = __esm({
445
450
  r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
446
451
  ml: /\b(machine[\s-]?learning|pytorch|tensorflow|scikit|sklearn|keras|neural|model training|deep[\s-]?learning|numpy|pandas|ml\s+(?:engineer|platform|researcher|infrastructure)|(?:ml|ai)\s+research)\b/i
447
452
  };
453
+ AMBIGUOUS_SYNONYM = {
454
+ // "prompt" is the LLM skill only in AI context. In raw JD prose it is "prompt
455
+ // delivery / response / payment / communication / attention". Accept only with
456
+ // the explicit "prompt engineering" phrase OR an LLM/AI ecosystem cue; the cue
457
+ // deliberately excludes the bare word "prompt(s)" itself so it can't self-satisfy.
458
+ prompt: {
459
+ id: "prompt-engineering",
460
+ cue: /\bprompt[\s-]?engineer(?:ing|s)?\b|\b(llms?|gpt-?[0-9o]*|claude|gemini|llama|mistral|openai|anthropic|langchain|llama[\s-]?index|rag|retrieval[\s-]?augmented|embeddings?|fine[\s-]?tun(?:e|ed|ing)|vector[\s-]?(?:db|database|store)|agentic|ai agents?|chatbots?|generative ai|gen[\s-]?ai|genai|few[\s-]?shot|zero[\s-]?shot)\b/i
461
+ }
462
+ };
448
463
  ENG_INTENT = /\b(engineer|engineering|developer|dev\b|swe|sde|programmer|architect|full[\s-]?stack|front[\s-]?end|back[\s-]?end|devops|sre|software|coding|codebase|technical staff|tech(?:nical)? lead)\b/i;
449
464
  NON_ENG_TITLE = /\b(account executive|account manager|sales (?:rep|representative|development|manager|lead)|sdr|bdr|recruiter|recruiting|talent|marketing|administrative|business partner|billing coordinator|operations (?:administrator|coordinator)|customer success|project finance|controller|bookkeeper|graphic|brand)\b/i;
450
465
  }
@@ -385,6 +385,11 @@ ${body}`;
385
385
  for (const tok of tokens) {
386
386
  const r = resolveToken(tok);
387
387
  if (!r) continue;
388
+ const synGate = AMBIGUOUS_SYNONYM[tok] ?? AMBIGUOUS_SYNONYM[tok.replace(/^[.\-+#]+|[.\-+#]+$/g, "")];
389
+ if (synGate && r.id === synGate.id) {
390
+ if (synGate.cue.test(text)) ids.add(r.id);
391
+ continue;
392
+ }
388
393
  if (NON_EXTRACTABLE.has(r.id)) continue;
389
394
  if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
390
395
  const cue = AMBIGUOUS[r.id];
@@ -402,7 +407,7 @@ ${body}`;
402
407
  function coreTagsFromTitle(title) {
403
408
  return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
404
409
  }
405
- var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
410
+ var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, AMBIGUOUS_SYNONYM, ENG_INTENT, NON_ENG_TITLE;
406
411
  var init_extract = __esm({
407
412
  "../../packages/core/src/vocab/extract.ts"() {
408
413
  "use strict";
@@ -439,6 +444,16 @@ var init_extract = __esm({
439
444
  r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
440
445
  ml: /\b(machine[\s-]?learning|pytorch|tensorflow|scikit|sklearn|keras|neural|model training|deep[\s-]?learning|numpy|pandas|ml\s+(?:engineer|platform|researcher|infrastructure)|(?:ml|ai)\s+research)\b/i
441
446
  };
447
+ AMBIGUOUS_SYNONYM = {
448
+ // "prompt" is the LLM skill only in AI context. In raw JD prose it is "prompt
449
+ // delivery / response / payment / communication / attention". Accept only with
450
+ // the explicit "prompt engineering" phrase OR an LLM/AI ecosystem cue; the cue
451
+ // deliberately excludes the bare word "prompt(s)" itself so it can't self-satisfy.
452
+ prompt: {
453
+ id: "prompt-engineering",
454
+ cue: /\bprompt[\s-]?engineer(?:ing|s)?\b|\b(llms?|gpt-?[0-9o]*|claude|gemini|llama|mistral|openai|anthropic|langchain|llama[\s-]?index|rag|retrieval[\s-]?augmented|embeddings?|fine[\s-]?tun(?:e|ed|ing)|vector[\s-]?(?:db|database|store)|agentic|ai agents?|chatbots?|generative ai|gen[\s-]?ai|genai|few[\s-]?shot|zero[\s-]?shot)\b/i
455
+ }
456
+ };
442
457
  ENG_INTENT = /\b(engineer|engineering|developer|dev\b|swe|sde|programmer|architect|full[\s-]?stack|front[\s-]?end|back[\s-]?end|devops|sre|software|coding|codebase|technical staff|tech(?:nical)? lead)\b/i;
443
458
  NON_ENG_TITLE = /\b(account executive|account manager|sales (?:rep|representative|development|manager|lead)|sdr|bdr|recruiter|recruiting|talent|marketing|administrative|business partner|billing coordinator|operations (?:administrator|coordinator)|customer success|project finance|controller|bookkeeper|graphic|brand)\b/i;
444
459
  }
@@ -391,6 +391,11 @@ ${body}`;
391
391
  for (const tok of tokens) {
392
392
  const r = resolveToken(tok);
393
393
  if (!r) continue;
394
+ const synGate = AMBIGUOUS_SYNONYM[tok] ?? AMBIGUOUS_SYNONYM[tok.replace(/^[.\-+#]+|[.\-+#]+$/g, "")];
395
+ if (synGate && r.id === synGate.id) {
396
+ if (synGate.cue.test(text)) ids.add(r.id);
397
+ continue;
398
+ }
394
399
  if (NON_EXTRACTABLE.has(r.id)) continue;
395
400
  if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
396
401
  const cue = AMBIGUOUS[r.id];
@@ -408,7 +413,7 @@ ${body}`;
408
413
  function coreTagsFromTitle(title) {
409
414
  return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
410
415
  }
411
- var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
416
+ var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, AMBIGUOUS_SYNONYM, ENG_INTENT, NON_ENG_TITLE;
412
417
  var init_extract = __esm({
413
418
  "../../packages/core/src/vocab/extract.ts"() {
414
419
  "use strict";
@@ -445,6 +450,16 @@ var init_extract = __esm({
445
450
  r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
446
451
  ml: /\b(machine[\s-]?learning|pytorch|tensorflow|scikit|sklearn|keras|neural|model training|deep[\s-]?learning|numpy|pandas|ml\s+(?:engineer|platform|researcher|infrastructure)|(?:ml|ai)\s+research)\b/i
447
452
  };
453
+ AMBIGUOUS_SYNONYM = {
454
+ // "prompt" is the LLM skill only in AI context. In raw JD prose it is "prompt
455
+ // delivery / response / payment / communication / attention". Accept only with
456
+ // the explicit "prompt engineering" phrase OR an LLM/AI ecosystem cue; the cue
457
+ // deliberately excludes the bare word "prompt(s)" itself so it can't self-satisfy.
458
+ prompt: {
459
+ id: "prompt-engineering",
460
+ cue: /\bprompt[\s-]?engineer(?:ing|s)?\b|\b(llms?|gpt-?[0-9o]*|claude|gemini|llama|mistral|openai|anthropic|langchain|llama[\s-]?index|rag|retrieval[\s-]?augmented|embeddings?|fine[\s-]?tun(?:e|ed|ing)|vector[\s-]?(?:db|database|store)|agentic|ai agents?|chatbots?|generative ai|gen[\s-]?ai|genai|few[\s-]?shot|zero[\s-]?shot)\b/i
461
+ }
462
+ };
448
463
  ENG_INTENT = /\b(engineer|engineering|developer|dev\b|swe|sde|programmer|architect|full[\s-]?stack|front[\s-]?end|back[\s-]?end|devops|sre|software|coding|codebase|technical staff|tech(?:nical)? lead)\b/i;
449
464
  NON_ENG_TITLE = /\b(account executive|account manager|sales (?:rep|representative|development|manager|lead)|sdr|bdr|recruiter|recruiting|talent|marketing|administrative|business partner|billing coordinator|operations (?:administrator|coordinator)|customer success|project finance|controller|bookkeeper|graphic|brand)\b/i;
450
465
  }
@@ -917,6 +917,11 @@ ${body}`;
917
917
  for (const tok of tokens) {
918
918
  const r = resolveToken(tok);
919
919
  if (!r) continue;
920
+ const synGate = AMBIGUOUS_SYNONYM[tok] ?? AMBIGUOUS_SYNONYM[tok.replace(/^[.\-+#]+|[.\-+#]+$/g, "")];
921
+ if (synGate && r.id === synGate.id) {
922
+ if (synGate.cue.test(text)) ids.add(r.id);
923
+ continue;
924
+ }
920
925
  if (NON_EXTRACTABLE.has(r.id)) continue;
921
926
  if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
922
927
  const cue = AMBIGUOUS[r.id];
@@ -934,7 +939,7 @@ ${body}`;
934
939
  function coreTagsFromTitle(title) {
935
940
  return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
936
941
  }
937
- var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
942
+ var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, AMBIGUOUS_SYNONYM, ENG_INTENT, NON_ENG_TITLE;
938
943
  var init_extract = __esm({
939
944
  "../../packages/core/src/vocab/extract.ts"() {
940
945
  "use strict";
@@ -971,6 +976,16 @@ var init_extract = __esm({
971
976
  r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
972
977
  ml: /\b(machine[\s-]?learning|pytorch|tensorflow|scikit|sklearn|keras|neural|model training|deep[\s-]?learning|numpy|pandas|ml\s+(?:engineer|platform|researcher|infrastructure)|(?:ml|ai)\s+research)\b/i
973
978
  };
979
+ AMBIGUOUS_SYNONYM = {
980
+ // "prompt" is the LLM skill only in AI context. In raw JD prose it is "prompt
981
+ // delivery / response / payment / communication / attention". Accept only with
982
+ // the explicit "prompt engineering" phrase OR an LLM/AI ecosystem cue; the cue
983
+ // deliberately excludes the bare word "prompt(s)" itself so it can't self-satisfy.
984
+ prompt: {
985
+ id: "prompt-engineering",
986
+ cue: /\bprompt[\s-]?engineer(?:ing|s)?\b|\b(llms?|gpt-?[0-9o]*|claude|gemini|llama|mistral|openai|anthropic|langchain|llama[\s-]?index|rag|retrieval[\s-]?augmented|embeddings?|fine[\s-]?tun(?:e|ed|ing)|vector[\s-]?(?:db|database|store)|agentic|ai agents?|chatbots?|generative ai|gen[\s-]?ai|genai|few[\s-]?shot|zero[\s-]?shot)\b/i
987
+ }
988
+ };
974
989
  ENG_INTENT = /\b(engineer|engineering|developer|dev\b|swe|sde|programmer|architect|full[\s-]?stack|front[\s-]?end|back[\s-]?end|devops|sre|software|coding|codebase|technical staff|tech(?:nical)? lead)\b/i;
975
990
  NON_ENG_TITLE = /\b(account executive|account manager|sales (?:rep|representative|development|manager|lead)|sdr|bdr|recruiter|recruiting|talent|marketing|administrative|business partner|billing coordinator|operations (?:administrator|coordinator)|customer success|project finance|controller|bookkeeper|graphic|brand)\b/i;
976
991
  }
@@ -7931,6 +7946,13 @@ var init_jpi_login = __esm({
7931
7946
  });
7932
7947
 
7933
7948
  // bin/job-status-store.js
7949
+ var job_status_store_exports = {};
7950
+ __export(job_status_store_exports, {
7951
+ markClicked: () => markClicked,
7952
+ markStatus: () => markStatus,
7953
+ readStatusMap: () => readStatusMap,
7954
+ statusFilePath: () => statusFilePath
7955
+ });
7934
7956
  import {
7935
7957
  readFileSync as readFileSync7,
7936
7958
  writeFileSync as writeFileSync5,
@@ -7944,6 +7966,9 @@ import {
7944
7966
  } from "fs";
7945
7967
  import { join as join7, dirname } from "path";
7946
7968
  import { homedir as homedir6 } from "os";
7969
+ function statusFilePath() {
7970
+ return STATUS_FILE;
7971
+ }
7947
7972
  function atomicWriteJson(path, obj) {
7948
7973
  mkdirSync5(dirname(path), { recursive: true });
7949
7974
  const tmp = `${path}.tmp-${process.pid}`;
@@ -38044,6 +38069,23 @@ var init_match_slots = __esm({
38044
38069
  }
38045
38070
  });
38046
38071
 
38072
+ // bin/job-status-suppress.js
38073
+ function isEngaged(rec) {
38074
+ if (!rec) return false;
38075
+ if (rec.status === "applied" || rec.status === "dismissed") return true;
38076
+ if (rec.status === "saved") return false;
38077
+ return rec.clicked === true;
38078
+ }
38079
+ function suppressEngaged(results, statusMap) {
38080
+ const map = statusMap || {};
38081
+ return results.filter((r) => !isEngaged(map[r?.job?.id]));
38082
+ }
38083
+ var init_job_status_suppress = __esm({
38084
+ "bin/job-status-suppress.js"() {
38085
+ "use strict";
38086
+ }
38087
+ });
38088
+
38047
38089
  // bin/jpi-refresh.js
38048
38090
  var jpi_refresh_exports = {};
38049
38091
  __export(jpi_refresh_exports, {
@@ -38086,6 +38128,7 @@ async function run21() {
38086
38128
  try {
38087
38129
  const { readProfile: readProfile2, profileToFingerprint: profileToFingerprint2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
38088
38130
  const { match: match2 } = await Promise.resolve().then(() => (init_src(), src_exports));
38131
+ const { readStatusMap: readStatusMap2 } = await Promise.resolve().then(() => (init_job_status_store(), job_status_store_exports));
38089
38132
  const profile = await readProfile2();
38090
38133
  if (profile.skillTags.length > 0 && jobs.length > 0) {
38091
38134
  const fp = profileToFingerprint2(profile);
@@ -38094,7 +38137,8 @@ async function run21() {
38094
38137
  contributeNudge.onRamp = true;
38095
38138
  }
38096
38139
  const pool = contribute.length > 0 ? [...jobs, ...contribute] : jobs;
38097
- const results = match2(fp, pool, pool.length);
38140
+ let results = match2(fp, pool, pool.length);
38141
+ results = suppressEngaged(results, readStatusMap2());
38098
38142
  matchCount = results.length;
38099
38143
  const { roleTop, bountyTop, contributeTop } = budgetSlots(results, {
38100
38144
  thinProfile,
@@ -38282,6 +38326,7 @@ var init_jpi_refresh = __esm({
38282
38326
  init_directory2();
38283
38327
  init_cache_store();
38284
38328
  init_match_slots();
38329
+ init_job_status_suppress();
38285
38330
  init_config();
38286
38331
  init_web_session();
38287
38332
  GH_SESSION_COOKIE7 = "__jpi_gh_session";
@@ -391,6 +391,11 @@ ${body}`;
391
391
  for (const tok of tokens) {
392
392
  const r = resolveToken(tok);
393
393
  if (!r) continue;
394
+ const synGate = AMBIGUOUS_SYNONYM[tok] ?? AMBIGUOUS_SYNONYM[tok.replace(/^[.\-+#]+|[.\-+#]+$/g, "")];
395
+ if (synGate && r.id === synGate.id) {
396
+ if (synGate.cue.test(text)) ids.add(r.id);
397
+ continue;
398
+ }
394
399
  if (NON_EXTRACTABLE.has(r.id)) continue;
395
400
  if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
396
401
  const cue = AMBIGUOUS[r.id];
@@ -408,7 +413,7 @@ ${body}`;
408
413
  function coreTagsFromTitle(title) {
409
414
  return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
410
415
  }
411
- var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
416
+ var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, AMBIGUOUS_SYNONYM, ENG_INTENT, NON_ENG_TITLE;
412
417
  var init_extract = __esm({
413
418
  "../../packages/core/src/vocab/extract.ts"() {
414
419
  "use strict";
@@ -445,6 +450,16 @@ var init_extract = __esm({
445
450
  r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
446
451
  ml: /\b(machine[\s-]?learning|pytorch|tensorflow|scikit|sklearn|keras|neural|model training|deep[\s-]?learning|numpy|pandas|ml\s+(?:engineer|platform|researcher|infrastructure)|(?:ml|ai)\s+research)\b/i
447
452
  };
453
+ AMBIGUOUS_SYNONYM = {
454
+ // "prompt" is the LLM skill only in AI context. In raw JD prose it is "prompt
455
+ // delivery / response / payment / communication / attention". Accept only with
456
+ // the explicit "prompt engineering" phrase OR an LLM/AI ecosystem cue; the cue
457
+ // deliberately excludes the bare word "prompt(s)" itself so it can't self-satisfy.
458
+ prompt: {
459
+ id: "prompt-engineering",
460
+ cue: /\bprompt[\s-]?engineer(?:ing|s)?\b|\b(llms?|gpt-?[0-9o]*|claude|gemini|llama|mistral|openai|anthropic|langchain|llama[\s-]?index|rag|retrieval[\s-]?augmented|embeddings?|fine[\s-]?tun(?:e|ed|ing)|vector[\s-]?(?:db|database|store)|agentic|ai agents?|chatbots?|generative ai|gen[\s-]?ai|genai|few[\s-]?shot|zero[\s-]?shot)\b/i
461
+ }
462
+ };
448
463
  ENG_INTENT = /\b(engineer|engineering|developer|dev\b|swe|sde|programmer|architect|full[\s-]?stack|front[\s-]?end|back[\s-]?end|devops|sre|software|coding|codebase|technical staff|tech(?:nical)? lead)\b/i;
449
464
  NON_ENG_TITLE = /\b(account executive|account manager|sales (?:rep|representative|development|manager|lead)|sdr|bdr|recruiter|recruiting|talent|marketing|administrative|business partner|billing coordinator|operations (?:administrator|coordinator)|customer success|project finance|controller|bookkeeper|graphic|brand)\b/i;
450
465
  }
@@ -696,6 +696,11 @@ ${body}`;
696
696
  for (const tok of tokens) {
697
697
  const r = resolveToken(tok);
698
698
  if (!r) continue;
699
+ const synGate = AMBIGUOUS_SYNONYM[tok] ?? AMBIGUOUS_SYNONYM[tok.replace(/^[.\-+#]+|[.\-+#]+$/g, "")];
700
+ if (synGate && r.id === synGate.id) {
701
+ if (synGate.cue.test(text)) ids.add(r.id);
702
+ continue;
703
+ }
699
704
  if (NON_EXTRACTABLE.has(r.id)) continue;
700
705
  if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
701
706
  const cue = AMBIGUOUS[r.id];
@@ -713,7 +718,7 @@ ${body}`;
713
718
  function coreTagsFromTitle(title) {
714
719
  return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
715
720
  }
716
- var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
721
+ var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, AMBIGUOUS_SYNONYM, ENG_INTENT, NON_ENG_TITLE;
717
722
  var init_extract = __esm({
718
723
  "../../packages/core/src/vocab/extract.ts"() {
719
724
  "use strict";
@@ -750,6 +755,16 @@ var init_extract = __esm({
750
755
  r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
751
756
  ml: /\b(machine[\s-]?learning|pytorch|tensorflow|scikit|sklearn|keras|neural|model training|deep[\s-]?learning|numpy|pandas|ml\s+(?:engineer|platform|researcher|infrastructure)|(?:ml|ai)\s+research)\b/i
752
757
  };
758
+ AMBIGUOUS_SYNONYM = {
759
+ // "prompt" is the LLM skill only in AI context. In raw JD prose it is "prompt
760
+ // delivery / response / payment / communication / attention". Accept only with
761
+ // the explicit "prompt engineering" phrase OR an LLM/AI ecosystem cue; the cue
762
+ // deliberately excludes the bare word "prompt(s)" itself so it can't self-satisfy.
763
+ prompt: {
764
+ id: "prompt-engineering",
765
+ cue: /\bprompt[\s-]?engineer(?:ing|s)?\b|\b(llms?|gpt-?[0-9o]*|claude|gemini|llama|mistral|openai|anthropic|langchain|llama[\s-]?index|rag|retrieval[\s-]?augmented|embeddings?|fine[\s-]?tun(?:e|ed|ing)|vector[\s-]?(?:db|database|store)|agentic|ai agents?|chatbots?|generative ai|gen[\s-]?ai|genai|few[\s-]?shot|zero[\s-]?shot)\b/i
766
+ }
767
+ };
753
768
  ENG_INTENT = /\b(engineer|engineering|developer|dev\b|swe|sde|programmer|architect|full[\s-]?stack|front[\s-]?end|back[\s-]?end|devops|sre|software|coding|codebase|technical staff|tech(?:nical)? lead)\b/i;
754
769
  NON_ENG_TITLE = /\b(account executive|account manager|sales (?:rep|representative|development|manager|lead)|sdr|bdr|recruiter|recruiting|talent|marketing|administrative|business partner|billing coordinator|operations (?:administrator|coordinator)|customer success|project finance|controller|bookkeeper|graphic|brand)\b/i;
755
770
  }
@@ -362,6 +362,11 @@ ${body}`;
362
362
  for (const tok of tokens) {
363
363
  const r = resolveToken(tok);
364
364
  if (!r) continue;
365
+ const synGate = AMBIGUOUS_SYNONYM[tok] ?? AMBIGUOUS_SYNONYM[tok.replace(/^[.\-+#]+|[.\-+#]+$/g, "")];
366
+ if (synGate && r.id === synGate.id) {
367
+ if (synGate.cue.test(text)) ids.add(r.id);
368
+ continue;
369
+ }
365
370
  if (NON_EXTRACTABLE.has(r.id)) continue;
366
371
  if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
367
372
  const cue = AMBIGUOUS[r.id];
@@ -379,7 +384,7 @@ ${body}`;
379
384
  function coreTagsFromTitle(title) {
380
385
  return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
381
386
  }
382
- var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
387
+ var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, AMBIGUOUS_SYNONYM, ENG_INTENT, NON_ENG_TITLE;
383
388
  var init_extract = __esm({
384
389
  "../../packages/core/src/vocab/extract.ts"() {
385
390
  "use strict";
@@ -416,6 +421,16 @@ var init_extract = __esm({
416
421
  r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
417
422
  ml: /\b(machine[\s-]?learning|pytorch|tensorflow|scikit|sklearn|keras|neural|model training|deep[\s-]?learning|numpy|pandas|ml\s+(?:engineer|platform|researcher|infrastructure)|(?:ml|ai)\s+research)\b/i
418
423
  };
424
+ AMBIGUOUS_SYNONYM = {
425
+ // "prompt" is the LLM skill only in AI context. In raw JD prose it is "prompt
426
+ // delivery / response / payment / communication / attention". Accept only with
427
+ // the explicit "prompt engineering" phrase OR an LLM/AI ecosystem cue; the cue
428
+ // deliberately excludes the bare word "prompt(s)" itself so it can't self-satisfy.
429
+ prompt: {
430
+ id: "prompt-engineering",
431
+ cue: /\bprompt[\s-]?engineer(?:ing|s)?\b|\b(llms?|gpt-?[0-9o]*|claude|gemini|llama|mistral|openai|anthropic|langchain|llama[\s-]?index|rag|retrieval[\s-]?augmented|embeddings?|fine[\s-]?tun(?:e|ed|ing)|vector[\s-]?(?:db|database|store)|agentic|ai agents?|chatbots?|generative ai|gen[\s-]?ai|genai|few[\s-]?shot|zero[\s-]?shot)\b/i
432
+ }
433
+ };
419
434
  ENG_INTENT = /\b(engineer|engineering|developer|dev\b|swe|sde|programmer|architect|full[\s-]?stack|front[\s-]?end|back[\s-]?end|devops|sre|software|coding|codebase|technical staff|tech(?:nical)? lead)\b/i;
420
435
  NON_ENG_TITLE = /\b(account executive|account manager|sales (?:rep|representative|development|manager|lead)|sdr|bdr|recruiter|recruiting|talent|marketing|administrative|business partner|billing coordinator|operations (?:administrator|coordinator)|customer success|project finance|controller|bookkeeper|graphic|brand)\b/i;
421
436
  }
@@ -391,6 +391,11 @@ ${body}`;
391
391
  for (const tok of tokens) {
392
392
  const r = resolveToken(tok);
393
393
  if (!r) continue;
394
+ const synGate = AMBIGUOUS_SYNONYM[tok] ?? AMBIGUOUS_SYNONYM[tok.replace(/^[.\-+#]+|[.\-+#]+$/g, "")];
395
+ if (synGate && r.id === synGate.id) {
396
+ if (synGate.cue.test(text)) ids.add(r.id);
397
+ continue;
398
+ }
394
399
  if (NON_EXTRACTABLE.has(r.id)) continue;
395
400
  if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
396
401
  const cue = AMBIGUOUS[r.id];
@@ -408,7 +413,7 @@ ${body}`;
408
413
  function coreTagsFromTitle(title) {
409
414
  return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
410
415
  }
411
- var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
416
+ var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, AMBIGUOUS_SYNONYM, ENG_INTENT, NON_ENG_TITLE;
412
417
  var init_extract = __esm({
413
418
  "../../packages/core/src/vocab/extract.ts"() {
414
419
  "use strict";
@@ -445,6 +450,16 @@ var init_extract = __esm({
445
450
  r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
446
451
  ml: /\b(machine[\s-]?learning|pytorch|tensorflow|scikit|sklearn|keras|neural|model training|deep[\s-]?learning|numpy|pandas|ml\s+(?:engineer|platform|researcher|infrastructure)|(?:ml|ai)\s+research)\b/i
447
452
  };
453
+ AMBIGUOUS_SYNONYM = {
454
+ // "prompt" is the LLM skill only in AI context. In raw JD prose it is "prompt
455
+ // delivery / response / payment / communication / attention". Accept only with
456
+ // the explicit "prompt engineering" phrase OR an LLM/AI ecosystem cue; the cue
457
+ // deliberately excludes the bare word "prompt(s)" itself so it can't self-satisfy.
458
+ prompt: {
459
+ id: "prompt-engineering",
460
+ cue: /\bprompt[\s-]?engineer(?:ing|s)?\b|\b(llms?|gpt-?[0-9o]*|claude|gemini|llama|mistral|openai|anthropic|langchain|llama[\s-]?index|rag|retrieval[\s-]?augmented|embeddings?|fine[\s-]?tun(?:e|ed|ing)|vector[\s-]?(?:db|database|store)|agentic|ai agents?|chatbots?|generative ai|gen[\s-]?ai|genai|few[\s-]?shot|zero[\s-]?shot)\b/i
461
+ }
462
+ };
448
463
  ENG_INTENT = /\b(engineer|engineering|developer|dev\b|swe|sde|programmer|architect|full[\s-]?stack|front[\s-]?end|back[\s-]?end|devops|sre|software|coding|codebase|technical staff|tech(?:nical)? lead)\b/i;
449
464
  NON_ENG_TITLE = /\b(account executive|account manager|sales (?:rep|representative|development|manager|lead)|sdr|bdr|recruiter|recruiting|talent|marketing|administrative|business partner|billing coordinator|operations (?:administrator|coordinator)|customer success|project finance|controller|bookkeeper|graphic|brand)\b/i;
450
465
  }
@@ -6906,14 +6921,136 @@ var init_profile = __esm({
6906
6921
  }
6907
6922
  });
6908
6923
 
6924
+ // bin/job-status-store.js
6925
+ var job_status_store_exports = {};
6926
+ __export(job_status_store_exports, {
6927
+ markClicked: () => markClicked,
6928
+ markStatus: () => markStatus,
6929
+ readStatusMap: () => readStatusMap,
6930
+ statusFilePath: () => statusFilePath
6931
+ });
6932
+ import {
6933
+ readFileSync as readFileSync7,
6934
+ writeFileSync as writeFileSync6,
6935
+ renameSync as renameSync2,
6936
+ mkdirSync as mkdirSync6,
6937
+ existsSync as existsSync4,
6938
+ copyFileSync,
6939
+ openSync,
6940
+ closeSync,
6941
+ unlinkSync
6942
+ } from "fs";
6943
+ import { join as join7, dirname } from "path";
6944
+ import { homedir as homedir6 } from "os";
6945
+ function statusFilePath() {
6946
+ return STATUS_FILE;
6947
+ }
6948
+ function atomicWriteJson(path, obj) {
6949
+ mkdirSync6(dirname(path), { recursive: true });
6950
+ const tmp = `${path}.tmp-${process.pid}`;
6951
+ writeFileSync6(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
6952
+ renameSync2(tmp, path);
6953
+ }
6954
+ function sleepMs(ms) {
6955
+ try {
6956
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
6957
+ } catch {
6958
+ const end = Date.now() + ms;
6959
+ while (Date.now() < end) {
6960
+ }
6961
+ }
6962
+ }
6963
+ function withLock(fn) {
6964
+ const deadline = Date.now() + 2e3;
6965
+ for (; ; ) {
6966
+ let fd;
6967
+ try {
6968
+ mkdirSync6(dirname(LOCK_FILE), { recursive: true });
6969
+ fd = openSync(LOCK_FILE, "wx");
6970
+ } catch (err) {
6971
+ if (err && err.code === "EEXIST") {
6972
+ if (Date.now() > deadline) {
6973
+ try {
6974
+ unlinkSync(LOCK_FILE);
6975
+ } catch {
6976
+ }
6977
+ continue;
6978
+ }
6979
+ sleepMs(5);
6980
+ continue;
6981
+ }
6982
+ throw err;
6983
+ }
6984
+ try {
6985
+ return fn();
6986
+ } finally {
6987
+ try {
6988
+ closeSync(fd);
6989
+ } catch {
6990
+ }
6991
+ try {
6992
+ unlinkSync(LOCK_FILE);
6993
+ } catch {
6994
+ }
6995
+ }
6996
+ }
6997
+ }
6998
+ function readStatusMap() {
6999
+ if (!existsSync4(STATUS_FILE)) return {};
7000
+ let raw;
7001
+ try {
7002
+ raw = readFileSync7(STATUS_FILE, "utf8");
7003
+ } catch {
7004
+ return {};
7005
+ }
7006
+ try {
7007
+ const parsed = JSON.parse(raw);
7008
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
7009
+ throw new Error("status store is not an object");
7010
+ } catch {
7011
+ try {
7012
+ copyFileSync(STATUS_FILE, BAK_FILE);
7013
+ } catch {
7014
+ }
7015
+ return {};
7016
+ }
7017
+ }
7018
+ function markStatus(id, status) {
7019
+ return withLock(() => {
7020
+ const current = readStatusMap();
7021
+ const next = setStatus(current, id, status);
7022
+ atomicWriteJson(STATUS_FILE, next);
7023
+ return next[id];
7024
+ });
7025
+ }
7026
+ function markClicked(id) {
7027
+ return withLock(() => {
7028
+ const current = readStatusMap();
7029
+ const next = recordClick(current, id);
7030
+ atomicWriteJson(STATUS_FILE, next);
7031
+ return next[id];
7032
+ });
7033
+ }
7034
+ var TERMINALHIRE_DIR5, STATUS_FILE, LOCK_FILE, BAK_FILE;
7035
+ var init_job_status_store = __esm({
7036
+ "bin/job-status-store.js"() {
7037
+ "use strict";
7038
+ init_src();
7039
+ TERMINALHIRE_DIR5 = process.env.TERMINALHIRE_DIR || join7(homedir6(), ".terminalhire");
7040
+ STATUS_FILE = join7(TERMINALHIRE_DIR5, "job-status.json");
7041
+ LOCK_FILE = `${STATUS_FILE}.lock`;
7042
+ BAK_FILE = `${STATUS_FILE}.bak`;
7043
+ }
7044
+ });
7045
+
6909
7046
  // src/signal.ts
6910
7047
  var signal_exports = {};
6911
7048
  __export(signal_exports, {
6912
7049
  extractFingerprint: () => extractFingerprint
6913
7050
  });
6914
- import { readFileSync as readFileSync7, readdirSync } from "fs";
7051
+ import { readFileSync as readFileSync8, readdirSync } from "fs";
6915
7052
  import { execFileSync } from "child_process";
6916
- import { join as join7 } from "path";
7053
+ import { join as join8 } from "path";
6917
7054
  function safeGit(args, cwd) {
6918
7055
  try {
6919
7056
  return execFileSync("git", ["-C", cwd, ...args], {
@@ -6941,20 +7078,20 @@ function isEmployerContext(cwd) {
6941
7078
  }
6942
7079
  function readJsonSafe(path) {
6943
7080
  try {
6944
- return JSON.parse(readFileSync7(path, "utf8"));
7081
+ return JSON.parse(readFileSync8(path, "utf8"));
6945
7082
  } catch {
6946
7083
  return null;
6947
7084
  }
6948
7085
  }
6949
7086
  function readFileSafe(path) {
6950
7087
  try {
6951
- return readFileSync7(path, "utf8");
7088
+ return readFileSync8(path, "utf8");
6952
7089
  } catch {
6953
7090
  return "";
6954
7091
  }
6955
7092
  }
6956
7093
  function tokensFromPackageJson(cwd) {
6957
- const pkg = readJsonSafe(join7(cwd, "package.json"));
7094
+ const pkg = readJsonSafe(join8(cwd, "package.json"));
6958
7095
  if (!pkg || typeof pkg !== "object") return [];
6959
7096
  const p = pkg;
6960
7097
  const deps = {
@@ -6968,9 +7105,9 @@ function workspaceMemberDirs(cwd) {
6968
7105
  const dirs = [cwd];
6969
7106
  for (const group of ["apps", "packages"]) {
6970
7107
  try {
6971
- const groupDir = join7(cwd, group);
7108
+ const groupDir = join8(cwd, group);
6972
7109
  for (const e of readdirSync(groupDir, { withFileTypes: true })) {
6973
- if (e.isDirectory() && !e.isSymbolicLink()) dirs.push(join7(groupDir, e.name));
7110
+ if (e.isDirectory() && !e.isSymbolicLink()) dirs.push(join8(groupDir, e.name));
6974
7111
  }
6975
7112
  } catch {
6976
7113
  }
@@ -6978,18 +7115,18 @@ function workspaceMemberDirs(cwd) {
6978
7115
  return dirs;
6979
7116
  }
6980
7117
  function tokensFromRequirementsTxt(cwd) {
6981
- const content = readFileSafe(join7(cwd, "requirements.txt"));
7118
+ const content = readFileSafe(join8(cwd, "requirements.txt"));
6982
7119
  if (!content) return [];
6983
7120
  return content.split("\n").map((l) => l.trim().split(/[>=<!\[;]/)[0].trim().toLowerCase()).filter(Boolean);
6984
7121
  }
6985
7122
  function tokensFromGoMod(cwd) {
6986
- const content = readFileSafe(join7(cwd, "go.mod"));
7123
+ const content = readFileSafe(join8(cwd, "go.mod"));
6987
7124
  if (!content) return [];
6988
7125
  const requires = Array.from(content.matchAll(/^\s+([^\s]+)\s+v/gm)).map((m) => m[1].split("/").pop() ?? "").filter(Boolean);
6989
7126
  return ["go", ...requires];
6990
7127
  }
6991
7128
  function tokensFromCargoToml(cwd) {
6992
- const content = readFileSafe(join7(cwd, "Cargo.toml"));
7129
+ const content = readFileSafe(join8(cwd, "Cargo.toml"));
6993
7130
  if (!content) return [];
6994
7131
  const deps = [];
6995
7132
  let inDeps = false;
@@ -7010,7 +7147,7 @@ function tokensFromFileExtensions(cwd) {
7010
7147
  const tokens = [];
7011
7148
  const scanDirs = [cwd];
7012
7149
  try {
7013
- const srcDir = join7(cwd, "src");
7150
+ const srcDir = join8(cwd, "src");
7014
7151
  readdirSync(srcDir);
7015
7152
  scanDirs.push(srcDir);
7016
7153
  } catch {
@@ -7180,22 +7317,22 @@ __export(spinner_seen_exports, {
7180
7317
  seenFilePath: () => seenFilePath
7181
7318
  });
7182
7319
  import {
7183
- readFileSync as readFileSync8,
7184
- writeFileSync as writeFileSync6,
7185
- renameSync as renameSync2,
7186
- mkdirSync as mkdirSync6
7320
+ readFileSync as readFileSync9,
7321
+ writeFileSync as writeFileSync7,
7322
+ renameSync as renameSync3,
7323
+ mkdirSync as mkdirSync7
7187
7324
  } from "fs";
7188
- import { join as join8, dirname } from "path";
7189
- import { homedir as homedir6 } from "os";
7325
+ import { join as join9, dirname as dirname2 } from "path";
7326
+ import { homedir as homedir7 } from "os";
7190
7327
  function seenFilePath() {
7191
- const dir = process.env["TERMINALHIRE_DIR"] || join8(homedir6(), ".terminalhire");
7192
- return join8(dir, "seen-history.json");
7328
+ const dir = process.env["TERMINALHIRE_DIR"] || join9(homedir7(), ".terminalhire");
7329
+ return join9(dir, "seen-history.json");
7193
7330
  }
7194
- function atomicWriteJson(path, obj) {
7195
- mkdirSync6(dirname(path), { recursive: true });
7331
+ function atomicWriteJson2(path, obj) {
7332
+ mkdirSync7(dirname2(path), { recursive: true });
7196
7333
  const tmp = `${path}.tmp-${process.pid}`;
7197
- writeFileSync6(tmp, JSON.stringify(obj) + "\n", { encoding: "utf8", mode: 384 });
7198
- renameSync2(tmp, path);
7334
+ writeFileSync7(tmp, JSON.stringify(obj) + "\n", { encoding: "utf8", mode: 384 });
7335
+ renameSync3(tmp, path);
7199
7336
  }
7200
7337
  function emptyHistory() {
7201
7338
  return { surface: 0, entries: {} };
@@ -7223,7 +7360,7 @@ function capEntries(entries) {
7223
7360
  function loadSeenHistory(now = Date.now()) {
7224
7361
  let raw;
7225
7362
  try {
7226
- raw = JSON.parse(readFileSync8(seenFilePath(), "utf8"));
7363
+ raw = JSON.parse(readFileSync9(seenFilePath(), "utf8"));
7227
7364
  } catch {
7228
7365
  return emptyHistory();
7229
7366
  }
@@ -7247,7 +7384,7 @@ function recordSurface(ids, now = Date.now()) {
7247
7384
  }
7248
7385
  const next = { surface, entries: capEntries(pruneEntries(entries, now)) };
7249
7386
  try {
7250
- atomicWriteJson(seenFilePath(), next);
7387
+ atomicWriteJson2(seenFilePath(), next);
7251
7388
  } catch {
7252
7389
  }
7253
7390
  return next;
@@ -7264,35 +7401,35 @@ var init_spinner_seen = __esm({
7264
7401
 
7265
7402
  // bin/spinner-io.js
7266
7403
  import {
7267
- readFileSync as readFileSync9,
7268
- writeFileSync as writeFileSync7,
7269
- existsSync as existsSync4,
7270
- mkdirSync as mkdirSync7,
7271
- renameSync as renameSync3
7404
+ readFileSync as readFileSync10,
7405
+ writeFileSync as writeFileSync8,
7406
+ existsSync as existsSync5,
7407
+ mkdirSync as mkdirSync8,
7408
+ renameSync as renameSync4
7272
7409
  } from "fs";
7273
- import { join as join9, dirname as dirname2 } from "path";
7274
- import { homedir as homedir7 } from "os";
7410
+ import { join as join10, dirname as dirname3 } from "path";
7411
+ import { homedir as homedir8 } from "os";
7275
7412
  function thDir() {
7276
- return process.env["TERMINALHIRE_DIR"] || join9(homedir7(), ".terminalhire");
7413
+ return process.env["TERMINALHIRE_DIR"] || join10(homedir8(), ".terminalhire");
7277
7414
  }
7278
7415
  function claudeSettingsPath() {
7279
- return process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join9(homedir7(), ".claude", "settings.json");
7416
+ return process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join10(homedir8(), ".claude", "settings.json");
7280
7417
  }
7281
7418
  function spinnerStateFilePath() {
7282
- return join9(thDir(), "spinner-state.json");
7419
+ return join10(thDir(), "spinner-state.json");
7283
7420
  }
7284
7421
  function readJson(path, fallback) {
7285
7422
  try {
7286
- return existsSync4(path) ? JSON.parse(readFileSync9(path, "utf8")) : fallback;
7423
+ return existsSync5(path) ? JSON.parse(readFileSync10(path, "utf8")) : fallback;
7287
7424
  } catch {
7288
7425
  return fallback;
7289
7426
  }
7290
7427
  }
7291
- function atomicWriteJson2(path, obj) {
7292
- mkdirSync7(dirname2(path), { recursive: true });
7428
+ function atomicWriteJson3(path, obj) {
7429
+ mkdirSync8(dirname3(path), { recursive: true });
7293
7430
  const tmp = `${path}.tmp-${process.pid}`;
7294
- writeFileSync7(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
7295
- renameSync3(tmp, path);
7431
+ writeFileSync8(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
7432
+ renameSync4(tmp, path);
7296
7433
  }
7297
7434
  function readState() {
7298
7435
  const SPINNER_STATE_FILE = spinnerStateFilePath();
@@ -7309,9 +7446,9 @@ function applySpinnerVerbs(ourVerbs, mode = "replace") {
7309
7446
  const userVerbs = existing && Array.isArray(existing.verbs) ? existing.verbs.filter((v) => !prevOurs.has(v)) : [];
7310
7447
  const newVerbs = [...verbs, ...userVerbs];
7311
7448
  settings.spinnerVerbs = { mode: mode === "append" ? "append" : "replace", verbs: newVerbs };
7312
- atomicWriteJson2(CLAUDE_SETTINGS, settings);
7449
+ atomicWriteJson3(CLAUDE_SETTINGS, settings);
7313
7450
  const st = readState();
7314
- atomicWriteJson2(SPINNER_STATE_FILE, { ...st, verbs, mode, ts: Date.now() });
7451
+ atomicWriteJson3(SPINNER_STATE_FILE, { ...st, verbs, mode, ts: Date.now() });
7315
7452
  return { applied: verbs.length, total: newVerbs.length };
7316
7453
  }
7317
7454
  function clearSpinnerVerbs() {
@@ -7331,11 +7468,11 @@ function clearSpinnerVerbs() {
7331
7468
  } else {
7332
7469
  delete settings.spinnerVerbs;
7333
7470
  }
7334
- atomicWriteJson2(CLAUDE_SETTINGS, settings);
7471
+ atomicWriteJson3(CLAUDE_SETTINGS, settings);
7335
7472
  }
7336
7473
  try {
7337
7474
  const st = readState();
7338
- atomicWriteJson2(SPINNER_STATE_FILE, { ...st, verbs: [], mode: st.mode || "replace", ts: Date.now() });
7475
+ atomicWriteJson3(SPINNER_STATE_FILE, { ...st, verbs: [], mode: st.mode || "replace", ts: Date.now() });
7339
7476
  } catch {
7340
7477
  }
7341
7478
  return { cleared: true, keptUserVerbs };
@@ -7351,9 +7488,9 @@ function applySpinnerTips(ourTips) {
7351
7488
  const userTips = existing.filter((t) => !prevOurs.has(t));
7352
7489
  settings.spinnerTipsEnabled = true;
7353
7490
  settings.spinnerTipsOverride = { excludeDefault: true, tips: [...tips, ...userTips] };
7354
- atomicWriteJson2(CLAUDE_SETTINGS, settings);
7491
+ atomicWriteJson3(CLAUDE_SETTINGS, settings);
7355
7492
  const st = readState();
7356
- atomicWriteJson2(SPINNER_STATE_FILE, { ...st, tips, ts: Date.now() });
7493
+ atomicWriteJson3(SPINNER_STATE_FILE, { ...st, tips, ts: Date.now() });
7357
7494
  return { applied: tips.length };
7358
7495
  }
7359
7496
  function clearSpinnerTips() {
@@ -7372,11 +7509,11 @@ function clearSpinnerTips() {
7372
7509
  delete settings.spinnerTipsOverride;
7373
7510
  delete settings.spinnerTipsEnabled;
7374
7511
  }
7375
- atomicWriteJson2(CLAUDE_SETTINGS, settings);
7512
+ atomicWriteJson3(CLAUDE_SETTINGS, settings);
7376
7513
  }
7377
7514
  try {
7378
7515
  const st = readState();
7379
- atomicWriteJson2(SPINNER_STATE_FILE, { ...st, tips: [], ts: Date.now() });
7516
+ atomicWriteJson3(SPINNER_STATE_FILE, { ...st, tips: [], ts: Date.now() });
7380
7517
  } catch {
7381
7518
  }
7382
7519
  return { cleared: true };
@@ -7388,9 +7525,9 @@ var init_spinner_io = __esm({
7388
7525
  });
7389
7526
 
7390
7527
  // bin/spinner-config.js
7391
- import { join as join10 } from "path";
7528
+ import { join as join11 } from "path";
7392
7529
  function configFilePath() {
7393
- return join10(thDir(), "config.json");
7530
+ return join11(thDir(), "config.json");
7394
7531
  }
7395
7532
  function readSpinnerConfig() {
7396
7533
  const CONFIG_FILE2 = configFilePath();
@@ -7820,9 +7957,9 @@ __export(version_nudge_exports, {
7820
7957
  readLatestVersionFromCache: () => readLatestVersionFromCache,
7821
7958
  readLocalVersion: () => readLocalVersion
7822
7959
  });
7823
- import { readFileSync as readFileSync10, existsSync as existsSync5 } from "fs";
7824
- import { join as join11 } from "path";
7825
- import { homedir as homedir8 } from "os";
7960
+ import { readFileSync as readFileSync11, existsSync as existsSync6 } from "fs";
7961
+ import { join as join12 } from "path";
7962
+ import { homedir as homedir9 } from "os";
7826
7963
  import { fileURLToPath as fileURLToPath2 } from "url";
7827
7964
  function parseVersion(v) {
7828
7965
  if (typeof v !== "string") return null;
@@ -7847,12 +7984,12 @@ function buildStaleNudge(local, latest) {
7847
7984
  function readLocalVersion() {
7848
7985
  try {
7849
7986
  const candidates = [
7850
- join11(__dirname, "..", "..", "package.json"),
7851
- join11(__dirname, "..", "package.json")
7987
+ join12(__dirname, "..", "..", "package.json"),
7988
+ join12(__dirname, "..", "package.json")
7852
7989
  ];
7853
7990
  for (const p of candidates) {
7854
- if (existsSync5(p)) {
7855
- const pkg = JSON.parse(readFileSync10(p, "utf8"));
7991
+ if (existsSync6(p)) {
7992
+ const pkg = JSON.parse(readFileSync11(p, "utf8"));
7856
7993
  if (pkg.version) return pkg.version;
7857
7994
  }
7858
7995
  }
@@ -7862,7 +7999,7 @@ function readLocalVersion() {
7862
7999
  }
7863
8000
  function readLatestVersionFromCache() {
7864
8001
  try {
7865
- const cache = JSON.parse(readFileSync10(INDEX_CACHE_FILE2, "utf8"));
8002
+ const cache = JSON.parse(readFileSync11(INDEX_CACHE_FILE2, "utf8"));
7866
8003
  const v = cache?.index?.cliVersion;
7867
8004
  return typeof v === "string" ? v : null;
7868
8005
  } catch {
@@ -7879,7 +8016,7 @@ var init_version_nudge = __esm({
7879
8016
  "bin/version-nudge.js"() {
7880
8017
  "use strict";
7881
8018
  __dirname = fileURLToPath2(new URL(".", import.meta.url));
7882
- INDEX_CACHE_FILE2 = join11(process.env.TERMINALHIRE_DIR || join11(homedir8(), ".terminalhire"), "index-cache.json");
8019
+ INDEX_CACHE_FILE2 = join12(process.env.TERMINALHIRE_DIR || join12(homedir9(), ".terminalhire"), "index-cache.json");
7883
8020
  }
7884
8021
  });
7885
8022
 
@@ -8034,6 +8171,18 @@ function budgetSlots(results, opts = {}) {
8034
8171
  return { roleTop, bountyTop, contributeTop };
8035
8172
  }
8036
8173
 
8174
+ // bin/job-status-suppress.js
8175
+ function isEngaged(rec) {
8176
+ if (!rec) return false;
8177
+ if (rec.status === "applied" || rec.status === "dismissed") return true;
8178
+ if (rec.status === "saved") return false;
8179
+ return rec.clicked === true;
8180
+ }
8181
+ function suppressEngaged(results, statusMap) {
8182
+ const map = statusMap || {};
8183
+ return results.filter((r) => !isEngaged(map[r?.job?.id]));
8184
+ }
8185
+
8037
8186
  // src/config.ts
8038
8187
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync } from "fs";
8039
8188
  import { join as join3 } from "path";
@@ -8143,6 +8292,7 @@ async function run() {
8143
8292
  try {
8144
8293
  const { readProfile: readProfile2, profileToFingerprint: profileToFingerprint2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
8145
8294
  const { match: match2 } = await Promise.resolve().then(() => (init_src(), src_exports));
8295
+ const { readStatusMap: readStatusMap2 } = await Promise.resolve().then(() => (init_job_status_store(), job_status_store_exports));
8146
8296
  const profile = await readProfile2();
8147
8297
  if (profile.skillTags.length > 0 && jobs.length > 0) {
8148
8298
  const fp = profileToFingerprint2(profile);
@@ -8151,7 +8301,8 @@ async function run() {
8151
8301
  contributeNudge.onRamp = true;
8152
8302
  }
8153
8303
  const pool = contribute.length > 0 ? [...jobs, ...contribute] : jobs;
8154
- const results = match2(fp, pool, pool.length);
8304
+ let results = match2(fp, pool, pool.length);
8305
+ results = suppressEngaged(results, readStatusMap2());
8155
8306
  matchCount = results.length;
8156
8307
  const { roleTop, bountyTop, contributeTop } = budgetSlots(results, {
8157
8308
  thinProfile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.18.1",
3
+ "version": "0.18.2",
4
4
  "description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Matching runs on your machine; your profile never leaves it.",
5
5
  "repository": {
6
6
  "type": "git",