terminalhire 0.4.0 → 0.4.4

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.
@@ -362,7 +362,7 @@ var init_graph_data = __esm({
362
362
  { id: "airflow", parents: ["data-engineering"], synonyms: ["apache-airflow"] },
363
363
  { id: "dbt", parents: ["data-engineering"] },
364
364
  { id: "ml", synonyms: ["machine-learning"], related: [{ to: "pytorch", w: 0.5 }, { to: "tensorflow", w: 0.5 }, { to: "scikit-learn", w: 0.5 }, { to: "data-engineering", w: 0.4 }] },
365
- { id: "llm", parents: ["ml"], synonyms: ["llms", "genai", "generative-ai"], related: [{ to: "langchain", w: 0.5 }, { to: "rag", w: 0.55 }, { to: "openai", w: 0.45 }, { to: "anthropic", w: 0.45 }] },
365
+ { id: "llm", parents: ["ml"], synonyms: ["llms", "genai", "generative-ai", "gpt"], related: [{ to: "langchain", w: 0.5 }, { to: "rag", w: 0.55 }, { to: "openai", w: 0.45 }, { to: "anthropic", w: 0.45 }] },
366
366
  { id: "pytorch", parents: ["ml"], synonyms: ["torch"], related: [{ to: "tensorflow", w: 0.5 }] },
367
367
  { id: "tensorflow", parents: ["ml"], synonyms: ["keras", "tf-keras"] },
368
368
  { id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }, { to: "data-engineering", w: 0.45 }, { to: "spark", w: 0.4 }] },
@@ -375,6 +375,14 @@ var init_graph_data = __esm({
375
375
  { id: "anthropic", parents: ["llm"], synonyms: ["claude"] },
376
376
  { id: "rag", parents: ["llm"], synonyms: ["retrieval-augmented-generation"] },
377
377
  { id: "mlops", parents: ["ml"], related: [{ to: "devops", w: 0.4 }] },
378
+ { id: "agents", parents: ["llm"], synonyms: ["agentic", "ai-agents", "multi-agent"], related: [{ to: "rag", w: 0.4 }] },
379
+ { id: "mcp", parents: ["agents"], synonyms: ["model-context-protocol"], related: [{ to: "llm", w: 0.45 }] },
380
+ { id: "inference", parents: ["ml"], synonyms: ["model-inference", "llm-inference", "model-serving"], related: [{ to: "mlops", w: 0.5 }, { to: "llm", w: 0.4 }] },
381
+ { id: "embeddings", parents: ["ml"], synonyms: ["embedding", "vector-embeddings"], related: [{ to: "rag", w: 0.55 }, { to: "llm", w: 0.45 }] },
382
+ { id: "prompt-engineering", parents: ["llm"], synonyms: ["prompting", "prompt"] },
383
+ { id: "fine-tuning", parents: ["ml"], synonyms: ["finetuning", "fine-tune", "rlhf"], related: [{ to: "llm", w: 0.5 }] },
384
+ { id: "computer-vision", parents: ["ml"], synonyms: ["image-recognition", "object-detection"] },
385
+ { id: "recsys", parents: ["ml"], synonyms: ["recommender-systems", "recommendation-systems", "recommendation"] },
378
386
  // ── Mobile ──────────────────────────────────────────────────────────────────
379
387
  { id: "mobile", related: [{ to: "ios", w: 0.5 }, { to: "android", w: 0.5 }] },
380
388
  { id: "ios", parents: ["mobile", "swift"], related: [{ to: "android", w: 0.4 }] },
@@ -797,7 +805,10 @@ var init_vocabulary = __esm({
797
805
  function ghHeaders(token) {
798
806
  const headers = {
799
807
  Accept: "application/vnd.github+json",
800
- "X-GitHub-Api-Version": "2022-11-28"
808
+ "X-GitHub-Api-Version": "2022-11-28",
809
+ // GitHub's REST API REQUIRES a User-Agent; serverless runtimes don't always
810
+ // send a default (omitting it yields a 403 "administrative rules" error).
811
+ "User-Agent": "terminalhire"
801
812
  };
802
813
  if (token) headers["Authorization"] = `Bearer ${token}`;
803
814
  return headers;
@@ -941,16 +952,25 @@ async function fetchRepoMeta(owner, name, token, cache) {
941
952
  cache.set(key, meta);
942
953
  return meta;
943
954
  }
944
- async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */ new Map()) {
955
+ function emptyCredential(status) {
956
+ return { status, byDomain: {}, qualifyingTotal: 0, computedAt: (/* @__PURE__ */ new Date()).toISOString() };
957
+ }
958
+ async function fetchPublicOrgs(login, token) {
959
+ try {
960
+ const orgs = await ghFetch(
961
+ `/users/${login}/orgs?per_page=100`,
962
+ token
963
+ );
964
+ return new Set(orgs.map((o) => o.login.toLowerCase()));
965
+ } catch {
966
+ return /* @__PURE__ */ new Set();
967
+ }
968
+ }
969
+ async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates = {
970
+ minStars: MIN_STARS,
971
+ minContributors: MIN_CONTRIBUTORS
972
+ }) {
945
973
  const computedAt = (/* @__PURE__ */ new Date()).toISOString();
946
- const empty = (status) => ({
947
- status,
948
- byDomain: {},
949
- qualifyingTotal: 0,
950
- computedAt
951
- });
952
- if (!token) return empty("no-token");
953
- const ownedOrgs = await fetchOwnedOrgs(token);
954
974
  const loginLc = login.toLowerCase();
955
975
  let items;
956
976
  try {
@@ -961,8 +981,9 @@ async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */
961
981
  );
962
982
  items = res.items ?? [];
963
983
  } catch (err) {
964
- const msg = String(err);
965
- return empty(/HTTP 403|HTTP 429|rate limit/i.test(msg) ? "rate-limited" : "failed");
984
+ const msg = err instanceof Error ? err.message : String(err);
985
+ console.warn("[acceptance] search failed:", msg);
986
+ return emptyCredential(/HTTP 403|HTTP 429|rate limit/i.test(msg) ? "rate-limited" : "failed");
966
987
  }
967
988
  const byDomain = {};
968
989
  let qualifyingTotal = 0;
@@ -976,8 +997,8 @@ async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */
976
997
  const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache);
977
998
  if (!meta) continue;
978
999
  if (meta.archived || meta.fork) continue;
979
- if (meta.stars < MIN_STARS) continue;
980
- if (meta.contributors !== void 0 && meta.contributors < MIN_CONTRIBUTORS) continue;
1000
+ if (meta.stars < gates.minStars) continue;
1001
+ if (meta.contributors !== void 0 && meta.contributors < gates.minContributors) continue;
981
1002
  qualifyingTotal += 1;
982
1003
  const mergedAt = item.pull_request?.merged_at ?? item.closed_at ?? item.created_at;
983
1004
  const rawDomains = [meta.language ?? "", ...meta.topics].filter(Boolean);
@@ -998,6 +1019,18 @@ async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */
998
1019
  }
999
1020
  return { status: "ok", byDomain: finalDomains, qualifyingTotal, computedAt };
1000
1021
  }
1022
+ async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */ new Map()) {
1023
+ if (!token) return emptyCredential("no-token");
1024
+ const ownedOrgs = await fetchOwnedOrgs(token);
1025
+ return computeAcceptanceFromSearch(login, token, ownedOrgs, cache);
1026
+ }
1027
+ async function computeAcceptanceCredentialPublic(login, token, cache = /* @__PURE__ */ new Map(), opts) {
1028
+ if (!token) return emptyCredential("no-token");
1029
+ const ownedOrgs = await fetchPublicOrgs(login, token);
1030
+ for (const org of opts?.includeOrgs ?? []) ownedOrgs.delete(org.toLowerCase());
1031
+ const gates = opts?.relaxGates ? { minStars: 0, minContributors: 0 } : void 0;
1032
+ return computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates);
1033
+ }
1001
1034
  function acceptanceCountForDomains(cred, domains) {
1002
1035
  if (cred.status !== "ok") return 0;
1003
1036
  let max = 0;
@@ -1016,7 +1049,60 @@ function bestAcceptanceDomain(cred, domains) {
1016
1049
  }
1017
1050
  return best;
1018
1051
  }
1019
- var MIN_STARS, MIN_CONTRIBUTORS, CANDIDATE_PR_PAGE, TRIVIAL_PR_TITLE;
1052
+ function resumeRecencyDecay(lastSeenIso, now) {
1053
+ const ageMs = now - new Date(lastSeenIso).getTime();
1054
+ if (!Number.isFinite(ageMs)) return 0;
1055
+ return Math.pow(0.5, ageMs / RESUME_DECAY_HALF_LIFE_MS);
1056
+ }
1057
+ async function fetchRepoRecency(login, token) {
1058
+ try {
1059
+ const repos = await ghFetch(`/users/${login}/repos?sort=pushed&per_page=100`, token);
1060
+ return repos.filter((r) => !r.fork && !!r.pushed_at).map((r) => ({ pushedAt: r.pushed_at, language: r.language ?? null, topics: r.topics ?? [] }));
1061
+ } catch {
1062
+ return [];
1063
+ }
1064
+ }
1065
+ function deriveResumeTrend(cred, repoRecency, now = Date.now()) {
1066
+ const agg = /* @__PURE__ */ new Map();
1067
+ const bump = (domain, when, count, mergedPRs) => {
1068
+ const e = agg.get(domain);
1069
+ if (!e) {
1070
+ agg.set(domain, { count, last: when, earliest: when, mergedPRs });
1071
+ } else {
1072
+ e.count += count;
1073
+ e.mergedPRs += mergedPRs;
1074
+ if (when > e.last) e.last = when;
1075
+ if (when < e.earliest) e.earliest = when;
1076
+ }
1077
+ };
1078
+ if (cred.status === "ok") {
1079
+ for (const [domain, d] of Object.entries(cred.byDomain)) {
1080
+ bump(domain, d.lastMergedAt, d.mergedPRs, d.mergedPRs);
1081
+ }
1082
+ }
1083
+ for (const r of repoRecency) {
1084
+ for (const domain of new Set(normalize([r.language ?? "", ...r.topics].filter(Boolean)))) {
1085
+ bump(domain, r.pushedAt, 1, 0);
1086
+ }
1087
+ }
1088
+ const oneHalfLifeAgoIso = new Date(now - RESUME_DECAY_HALF_LIFE_MS).toISOString();
1089
+ const scored = [];
1090
+ for (const [domain, e] of agg.entries()) {
1091
+ const recencyScore2 = resumeRecencyDecay(e.last, now);
1092
+ const weight = e.count * recencyScore2;
1093
+ if (weight < RESUME_MIN_SCORE) continue;
1094
+ let direction;
1095
+ if (e.earliest > oneHalfLifeAgoIso) direction = "new";
1096
+ else if (recencyScore2 >= 0.5) direction = "up";
1097
+ else direction = "down";
1098
+ scored.push({
1099
+ t: { domain, direction, recencyScore: Math.round(recencyScore2 * 1e3) / 1e3, mergedPRs: e.mergedPRs },
1100
+ weight
1101
+ });
1102
+ }
1103
+ return scored.sort((a, b) => b.weight - a.weight).slice(0, 12).map((s) => s.t);
1104
+ }
1105
+ var MIN_STARS, MIN_CONTRIBUTORS, CANDIDATE_PR_PAGE, TRIVIAL_PR_TITLE, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
1020
1106
  var init_github = __esm({
1021
1107
  "../../packages/core/src/github.ts"() {
1022
1108
  "use strict";
@@ -1025,6 +1111,8 @@ var init_github = __esm({
1025
1111
  MIN_CONTRIBUTORS = 10;
1026
1112
  CANDIDATE_PR_PAGE = 50;
1027
1113
  TRIVIAL_PR_TITLE = /^\s*(fix\s+typo|typo\b|update\s+readme|readme\b|docs?:|docs?\(|chore:|chore\(|style:|ci:|build:|bump\b|update\s+dependenc)/i;
1114
+ RESUME_DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
1115
+ RESUME_MIN_SCORE = 0.05;
1028
1116
  }
1029
1117
  });
1030
1118
 
@@ -1172,6 +1260,18 @@ var init_matcher = __esm({
1172
1260
  }
1173
1261
  });
1174
1262
 
1263
+ // ../../packages/core/src/feeds/http.ts
1264
+ function fetchWithTimeout(input, init, timeoutMs = FEED_FETCH_TIMEOUT_MS) {
1265
+ return fetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
1266
+ }
1267
+ var FEED_FETCH_TIMEOUT_MS;
1268
+ var init_http = __esm({
1269
+ "../../packages/core/src/feeds/http.ts"() {
1270
+ "use strict";
1271
+ FEED_FETCH_TIMEOUT_MS = 1e4;
1272
+ }
1273
+ });
1274
+
1175
1275
  // ../../packages/core/src/feeds/greenhouse.ts
1176
1276
  function extractTags(job) {
1177
1277
  const body = [
@@ -1190,7 +1290,7 @@ async function fetchSlug(slug) {
1190
1290
  const url = `https://boards-api.greenhouse.io/v1/boards/${slug}/jobs?content=true`;
1191
1291
  let res;
1192
1292
  try {
1193
- res = await fetch(url, { headers: { Accept: "application/json" } });
1293
+ res = await fetchWithTimeout(url, { headers: { Accept: "application/json" } });
1194
1294
  } catch (err) {
1195
1295
  console.warn(`[greenhouse] ${slug}: network error \u2014`, err);
1196
1296
  return [];
@@ -1232,6 +1332,7 @@ var init_greenhouse = __esm({
1232
1332
  "../../packages/core/src/feeds/greenhouse.ts"() {
1233
1333
  "use strict";
1234
1334
  init_vocabulary();
1335
+ init_http();
1235
1336
  FALLBACK_SLUGS = [
1236
1337
  "stripe",
1237
1338
  "linear",
@@ -1302,7 +1403,7 @@ function inferRemote2(job) {
1302
1403
  }
1303
1404
  async function fetchSlug2(slug) {
1304
1405
  const url = `https://api.ashbyhq.com/posting-api/job-board/${slug}`;
1305
- const res = await fetch(url, {
1406
+ const res = await fetchWithTimeout(url, {
1306
1407
  headers: { Accept: "application/json" }
1307
1408
  });
1308
1409
  if (!res.ok) {
@@ -1334,6 +1435,7 @@ var init_ashby = __esm({
1334
1435
  "../../packages/core/src/feeds/ashby.ts"() {
1335
1436
  "use strict";
1336
1437
  init_vocabulary();
1438
+ init_http();
1337
1439
  ashby = {
1338
1440
  source: "ashby",
1339
1441
  async fetch(opts) {
@@ -1384,7 +1486,7 @@ function toIso(ms) {
1384
1486
  }
1385
1487
  async function fetchSlug3(slug) {
1386
1488
  const url = `https://api.lever.co/v0/postings/${slug}?mode=json`;
1387
- const res = await fetch(url, { headers: { Accept: "application/json" } });
1489
+ const res = await fetchWithTimeout(url, { headers: { Accept: "application/json" } });
1388
1490
  if (!res.ok) {
1389
1491
  throw new Error(`Lever ${slug}: HTTP ${res.status}`);
1390
1492
  }
@@ -1415,6 +1517,7 @@ var init_lever = __esm({
1415
1517
  "../../packages/core/src/feeds/lever.ts"() {
1416
1518
  "use strict";
1417
1519
  init_vocabulary();
1520
+ init_http();
1418
1521
  lever = {
1419
1522
  source: "lever",
1420
1523
  async fetch(opts) {
@@ -1456,12 +1559,13 @@ var init_himalayas = __esm({
1456
1559
  "../../packages/core/src/feeds/himalayas.ts"() {
1457
1560
  "use strict";
1458
1561
  init_vocabulary();
1562
+ init_http();
1459
1563
  himalayas = {
1460
1564
  source: "himalayas",
1461
1565
  async fetch(opts) {
1462
1566
  const limit = opts?.limit ?? 100;
1463
1567
  const url = `https://himalayas.app/jobs/api?limit=${limit}`;
1464
- const res = await fetch(url, {
1568
+ const res = await fetchWithTimeout(url, {
1465
1569
  headers: { Accept: "application/json" }
1466
1570
  });
1467
1571
  if (!res.ok) {
@@ -1570,12 +1674,13 @@ var init_wwr = __esm({
1570
1674
  "use strict";
1571
1675
  init_vocabulary();
1572
1676
  init_entities();
1677
+ init_http();
1573
1678
  WWR_RSS_URL = "https://weworkremotely.com/remote-jobs.rss";
1574
1679
  wwr = {
1575
1680
  source: "wwr",
1576
1681
  async fetch(opts) {
1577
1682
  const limit = opts?.limit ?? 200;
1578
- const res = await fetch(WWR_RSS_URL, {
1683
+ const res = await fetchWithTimeout(WWR_RSS_URL, {
1579
1684
  headers: { Accept: "application/rss+xml, application/xml, text/xml" }
1580
1685
  });
1581
1686
  if (!res.ok) {
@@ -1583,6 +1688,11 @@ var init_wwr = __esm({
1583
1688
  }
1584
1689
  const xml = await res.text();
1585
1690
  const items = parseRss(xml).slice(0, limit);
1691
+ function safeIso(s) {
1692
+ if (!s) return void 0;
1693
+ const d = new Date(s);
1694
+ return Number.isNaN(d.getTime()) ? void 0 : d.toISOString();
1695
+ }
1586
1696
  return items.map((item) => ({
1587
1697
  id: extractId(item.link),
1588
1698
  source: "wwr",
@@ -1594,7 +1704,7 @@ var init_wwr = __esm({
1594
1704
  location: "Remote",
1595
1705
  tags: extractTags5(item),
1596
1706
  roleType: inferRoleType(item.category),
1597
- postedAt: item.pubDate ? new Date(item.pubDate).toISOString() : void 0,
1707
+ postedAt: safeIso(item.pubDate),
1598
1708
  applyMode: "direct",
1599
1709
  raw: item
1600
1710
  }));
@@ -1660,13 +1770,14 @@ var init_hn = __esm({
1660
1770
  "use strict";
1661
1771
  init_vocabulary();
1662
1772
  init_entities();
1773
+ init_http();
1663
1774
  ALGOLIA_SEARCH = "https://hn.algolia.com/api/v1/search?query=Ask+HN%3A+Who+is+Hiring%3F&tags=story,ask_hn&hitsPerPage=1";
1664
1775
  ALGOLIA_ITEMS = "https://hn.algolia.com/api/v1/items/";
1665
1776
  hn = {
1666
1777
  source: "hn",
1667
1778
  async fetch(opts) {
1668
1779
  const limit = opts?.limit ?? 150;
1669
- const searchRes = await fetch(ALGOLIA_SEARCH, {
1780
+ const searchRes = await fetchWithTimeout(ALGOLIA_SEARCH, {
1670
1781
  headers: { Accept: "application/json" }
1671
1782
  });
1672
1783
  if (!searchRes.ok) {
@@ -1677,7 +1788,7 @@ var init_hn = __esm({
1677
1788
  if (!story) {
1678
1789
  throw new Error('HN: No "Who is Hiring" story found');
1679
1790
  }
1680
- const itemRes = await fetch(`${ALGOLIA_ITEMS}${story.objectID}`, {
1791
+ const itemRes = await fetchWithTimeout(`${ALGOLIA_ITEMS}${story.objectID}`, {
1681
1792
  headers: { Accept: "application/json" }
1682
1793
  });
1683
1794
  if (!itemRes.ok) {
@@ -1697,18 +1808,25 @@ var init_hn = __esm({
1697
1808
  });
1698
1809
 
1699
1810
  // ../../packages/core/src/feeds/bounty-gate.ts
1811
+ function isDenylistedRepo(fullName) {
1812
+ return DENYLIST_LC.has(fullName.toLowerCase());
1813
+ }
1814
+ function passesAntiFarm(amountUSD, stargazers) {
1815
+ return !(amountUSD > HIGH_VALUE_USD && stargazers < HIGH_VALUE_MIN_STARS);
1816
+ }
1700
1817
  function ageDays(createdAtIso) {
1701
1818
  const created = Date.parse(createdAtIso);
1702
1819
  if (!Number.isFinite(created)) return 0;
1703
1820
  return (Date.now() - created) / (1e3 * 60 * 60 * 24);
1704
1821
  }
1705
1822
  function passesMaturityGate(repo) {
1823
+ if (isDenylistedRepo(repo.fullName)) return false;
1706
1824
  if (repo.archived || repo.disabled) return false;
1707
1825
  if (repo.stargazers < MIN_REPO_STARS) return false;
1708
1826
  if (ageDays(repo.createdAt) < MIN_REPO_AGE_DAYS) return false;
1709
1827
  return true;
1710
1828
  }
1711
- var DEFAULT_BOUNTY_REPOS, MAX_BOUNTIES_PER_REPO, MIN_REPO_STARS, MIN_REPO_AGE_DAYS;
1829
+ var DEFAULT_BOUNTY_REPOS, BOUNTY_REPO_DENYLIST, DENYLIST_LC, MAX_BOUNTIES_PER_REPO, MAX_BOUNTIES_PER_DISCOVERED_REPO, MIN_REPO_STARS, HIGH_VALUE_USD, HIGH_VALUE_MIN_STARS, MIN_REPO_AGE_DAYS;
1712
1830
  var init_bounty_gate = __esm({
1713
1831
  "../../packages/core/src/feeds/bounty-gate.ts"() {
1714
1832
  "use strict";
@@ -1725,8 +1843,13 @@ var init_bounty_gate = __esm({
1725
1843
  "moorcheh-ai/memanto",
1726
1844
  "PrismarineJS/mineflayer"
1727
1845
  ];
1846
+ BOUNTY_REPO_DENYLIST = ["SecureBananaLabs/bug-bounty"];
1847
+ DENYLIST_LC = new Set(BOUNTY_REPO_DENYLIST.map((r) => r.toLowerCase()));
1728
1848
  MAX_BOUNTIES_PER_REPO = 10;
1849
+ MAX_BOUNTIES_PER_DISCOVERED_REPO = 3;
1729
1850
  MIN_REPO_STARS = 5;
1851
+ HIGH_VALUE_USD = 500;
1852
+ HIGH_VALUE_MIN_STARS = 50;
1730
1853
  MIN_REPO_AGE_DAYS = 30;
1731
1854
  }
1732
1855
  });
@@ -1771,7 +1894,7 @@ function isBountyIssue(issue) {
1771
1894
  async function ghJson(path) {
1772
1895
  let res;
1773
1896
  try {
1774
- res = await fetch(`${GITHUB_API}${path}`, { headers: authHeaders() });
1897
+ res = await fetchWithTimeout(`${GITHUB_API}${path}`, { headers: authHeaders() });
1775
1898
  } catch (err) {
1776
1899
  console.warn(`[github-bounties] network error ${path} \u2014`, err);
1777
1900
  return null;
@@ -1853,31 +1976,435 @@ async function fetchRepoBounties(repoFullName) {
1853
1976
  };
1854
1977
  }));
1855
1978
  }
1856
- var GITHUB_API, BOUNTY_LABEL_RE, githubBounties;
1979
+ function repoFullNameFromApiUrl(url) {
1980
+ const m = url.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
1981
+ return m ? `${m[1]}/${m[2]}` : null;
1982
+ }
1983
+ async function searchBountyIssues() {
1984
+ const byUrl = /* @__PURE__ */ new Map();
1985
+ for (const q of SEARCH_QUERIES) {
1986
+ const res = await ghJson(
1987
+ `/search/issues?q=${encodeURIComponent(q)}&sort=created&order=desc&per_page=${SEARCH_PER_PAGE}`
1988
+ );
1989
+ for (const it of res?.items ?? []) {
1990
+ if (it.pull_request) continue;
1991
+ if (!byUrl.has(it.html_url)) byUrl.set(it.html_url, it);
1992
+ }
1993
+ }
1994
+ return [...byUrl.values()].sort(
1995
+ (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
1996
+ );
1997
+ }
1998
+ async function repoMetaCached(fullName) {
1999
+ const hit = repoMetaCache.get(fullName);
2000
+ if (hit !== void 0) return hit;
2001
+ const r = await ghJson(`/repos/${fullName}`) ?? null;
2002
+ repoMetaCache.set(fullName, r);
2003
+ return r;
2004
+ }
2005
+ async function fetchRepoMeta2(fullName) {
2006
+ const repo = await repoMetaCached(fullName);
2007
+ if (!repo) return null;
2008
+ return {
2009
+ fullName: repo.full_name,
2010
+ stargazers: repo.stargazers_count,
2011
+ createdAt: repo.created_at,
2012
+ archived: repo.archived,
2013
+ disabled: repo.disabled
2014
+ };
2015
+ }
2016
+ async function fetchRepoOpenPRRefs(fullName) {
2017
+ const hit = repoOpenPRRefsCache.get(fullName);
2018
+ if (hit !== void 0) return hit;
2019
+ const refs = /* @__PURE__ */ new Map();
2020
+ let scannedAny = false;
2021
+ for (let page = 1; page <= MAX_PR_PAGES; page++) {
2022
+ const prs = await ghJson(
2023
+ `/repos/${fullName}/pulls?state=open&per_page=100&page=${page}`
2024
+ );
2025
+ if (!Array.isArray(prs)) break;
2026
+ scannedAny = true;
2027
+ for (const pr of prs) {
2028
+ const counted = /* @__PURE__ */ new Set();
2029
+ for (const m of `${pr.title ?? ""}
2030
+ ${pr.body ?? ""}`.matchAll(/#(\d+)\b/g)) {
2031
+ const n = Number(m[1]);
2032
+ if (!counted.has(n)) {
2033
+ counted.add(n);
2034
+ refs.set(n, (refs.get(n) ?? 0) + 1);
2035
+ }
2036
+ }
2037
+ }
2038
+ if (prs.length < 100) break;
2039
+ }
2040
+ const result = scannedAny ? refs : null;
2041
+ repoOpenPRRefsCache.set(fullName, result);
2042
+ return result;
2043
+ }
2044
+ async function fetchIssueState(fullName, issueNumber) {
2045
+ const key = `${fullName}#${issueNumber}`;
2046
+ const hit = issueStateCache.get(key);
2047
+ if (hit !== void 0) return hit;
2048
+ const issue = await ghJson(`/repos/${fullName}/issues/${issueNumber}`);
2049
+ const state = issue?.state === "open" ? "open" : issue?.state === "closed" ? "closed" : null;
2050
+ issueStateCache.set(key, state);
2051
+ return state;
2052
+ }
2053
+ async function fetchSearchBounties() {
2054
+ const issues = (await searchBountyIssues()).slice(0, MAX_SEARCH_ISSUES_SCANNED);
2055
+ const distinctRepos = [
2056
+ ...new Set(
2057
+ issues.map((i) => repoFullNameFromApiUrl(i.repository_url)).filter((x) => !!x)
2058
+ )
2059
+ ];
2060
+ for (let i = 0; i < distinctRepos.length; i += REPO_META_CONCURRENCY) {
2061
+ await Promise.all(distinctRepos.slice(i, i + REPO_META_CONCURRENCY).map(repoMetaCached));
2062
+ }
2063
+ const jobs = [];
2064
+ const perRepo = /* @__PURE__ */ new Map();
2065
+ for (const issue of issues) {
2066
+ if (jobs.length >= MAX_SEARCH_BOUNTIES) break;
2067
+ const fullName = repoFullNameFromApiUrl(issue.repository_url);
2068
+ if (!fullName) continue;
2069
+ if ((perRepo.get(fullName) ?? 0) >= MAX_BOUNTIES_PER_REPO) continue;
2070
+ const repo = await repoMetaCached(fullName);
2071
+ if (!repo) continue;
2072
+ const passes = passesMaturityGate({
2073
+ fullName: repo.full_name,
2074
+ stargazers: repo.stargazers_count,
2075
+ createdAt: repo.created_at,
2076
+ archived: repo.archived,
2077
+ disabled: repo.disabled
2078
+ });
2079
+ if (!passes) continue;
2080
+ const title = decodeEntities(issue.title).trim();
2081
+ const body = issue.body ? decodeEntities(issue.body) : "";
2082
+ const labels = labelNames(issue);
2083
+ let amountUSD = parseAmountUSD(title) ?? parseAmountUSD(labels.join(" ")) ?? parseAmountUSD(body);
2084
+ if (amountUSD == null && labels.some((n) => /💎|💰/.test(n))) {
2085
+ amountUSD = await fetchCommentAmount(fullName, issue.number);
2086
+ }
2087
+ if (amountUSD == null) continue;
2088
+ if (!passesAntiFarm(amountUSD, repo.stargazers_count)) continue;
2089
+ const tags = normalize(
2090
+ tokenize2([title, labels.join(" "), body.slice(0, 2e3)].join(" "))
2091
+ );
2092
+ perRepo.set(fullName, (perRepo.get(fullName) ?? 0) + 1);
2093
+ jobs.push({
2094
+ id: `bounty:${fullName}#${issue.number}`,
2095
+ source: "bounty",
2096
+ title,
2097
+ company: repo.owner.login,
2098
+ url: issue.html_url,
2099
+ remote: true,
2100
+ location: "Remote",
2101
+ tags,
2102
+ roleType: "freelance",
2103
+ postedAt: issue.created_at,
2104
+ applyMode: "direct",
2105
+ bounty: {
2106
+ amountUSD,
2107
+ estimatedEffort: effortFromAmount(amountUSD),
2108
+ bountySource: "github",
2109
+ claimUrl: issue.html_url,
2110
+ repoFullName: fullName,
2111
+ repoStars: repo.stargazers_count,
2112
+ issueBody: body.slice(0, 1e3) || void 0
2113
+ },
2114
+ raw: issue
2115
+ });
2116
+ }
2117
+ return jobs;
2118
+ }
2119
+ var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, repoMetaCache, MAX_PR_PAGES, repoOpenPRRefsCache, issueStateCache, githubBounties;
1857
2120
  var init_github_bounties = __esm({
1858
2121
  "../../packages/core/src/feeds/github-bounties.ts"() {
1859
2122
  "use strict";
1860
2123
  init_vocabulary();
1861
2124
  init_entities();
1862
2125
  init_bounty_gate();
2126
+ init_http();
1863
2127
  GITHUB_API = "https://api.github.com";
1864
2128
  BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
2129
+ SEARCH_QUERIES = [
2130
+ 'label:"\u{1F48E} Bounty" type:issue state:open',
2131
+ // Algora-applied — highest signal
2132
+ "label:bounty type:issue state:open",
2133
+ 'label:"\u{1F4B0} Bounty" type:issue state:open'
2134
+ ];
2135
+ SEARCH_PER_PAGE = 100;
2136
+ MAX_SEARCH_BOUNTIES = 150;
2137
+ MAX_SEARCH_ISSUES_SCANNED = 300;
2138
+ REPO_META_CONCURRENCY = 15;
2139
+ repoMetaCache = /* @__PURE__ */ new Map();
2140
+ MAX_PR_PAGES = 3;
2141
+ repoOpenPRRefsCache = /* @__PURE__ */ new Map();
2142
+ issueStateCache = /* @__PURE__ */ new Map();
1865
2143
  githubBounties = {
1866
2144
  source: "bounty",
1867
2145
  async fetch(opts) {
1868
- const repos = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : DEFAULT_BOUNTY_REPOS;
1869
- console.info(`[github-bounties] scanning ${repos.length} repos`);
1870
- const settled = await Promise.allSettled(repos.map(fetchRepoBounties));
2146
+ const allowlist = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : DEFAULT_BOUNTY_REPOS;
2147
+ const [searched, listed] = await Promise.all([
2148
+ fetchSearchBounties().catch((e) => {
2149
+ console.warn("[github-bounties] search discovery failed:", e);
2150
+ return [];
2151
+ }),
2152
+ Promise.allSettled(allowlist.map(fetchRepoBounties)).then(
2153
+ (settled) => settled.flatMap((r) => r.status === "fulfilled" ? r.value : [])
2154
+ )
2155
+ ]);
2156
+ const seen = /* @__PURE__ */ new Set();
2157
+ const out = [];
2158
+ for (const j of [...searched, ...listed]) {
2159
+ if (!seen.has(j.id)) {
2160
+ seen.add(j.id);
2161
+ out.push(j);
2162
+ }
2163
+ }
2164
+ console.info(
2165
+ `[github-bounties] total: ${out.length} bounties (${searched.length} search + ${listed.length} allowlist, deduped)`
2166
+ );
2167
+ return out;
2168
+ }
2169
+ };
2170
+ }
2171
+ });
2172
+
2173
+ // ../../packages/core/src/feeds/opire.ts
2174
+ function tokenize3(text) {
2175
+ return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter((w) => w.length > 1);
2176
+ }
2177
+ function effortFromAmount2(usd) {
2178
+ if (usd == null) return void 0;
2179
+ if (usd < 150) return "small";
2180
+ if (usd < 750) return "medium";
2181
+ return "large";
2182
+ }
2183
+ function priceToUSD(p) {
2184
+ if (!p || typeof p.value !== "number") return void 0;
2185
+ if (p.unit === "USD_CENT") return Math.round(p.value) / 100;
2186
+ if (p.unit === "USD") return p.value;
2187
+ return void 0;
2188
+ }
2189
+ function repoFullNameFromUrl(url) {
2190
+ const m = url?.match(/github\.com\/([^/]+)\/([^/]+)/i);
2191
+ return m ? `${m[1]}/${m[2].replace(/\.git$/, "")}` : void 0;
2192
+ }
2193
+ function issueNumberFromUrl(url) {
2194
+ const m = url?.match(/\/issues\/(\d+)/);
2195
+ return m ? parseInt(m[1], 10) : void 0;
2196
+ }
2197
+ var OPIRE_REWARDS_URL, MIN_USD, MAX_USD, MAX_OPIRE_BOUNTIES, REPO_META_CONCURRENCY2, opire;
2198
+ var init_opire = __esm({
2199
+ "../../packages/core/src/feeds/opire.ts"() {
2200
+ "use strict";
2201
+ init_vocabulary();
2202
+ init_bounty_gate();
2203
+ init_github_bounties();
2204
+ init_http();
2205
+ OPIRE_REWARDS_URL = "https://api.opire.dev/rewards";
2206
+ MIN_USD = 25;
2207
+ MAX_USD = 25e3;
2208
+ MAX_OPIRE_BOUNTIES = 100;
2209
+ REPO_META_CONCURRENCY2 = 15;
2210
+ opire = {
2211
+ source: "bounty",
2212
+ async fetch() {
2213
+ let rewards;
2214
+ try {
2215
+ const res = await fetchWithTimeout(OPIRE_REWARDS_URL, {
2216
+ headers: { Accept: "application/json", "User-Agent": "terminalhire" }
2217
+ });
2218
+ if (!res.ok) {
2219
+ console.warn(`[opire] HTTP ${res.status}`);
2220
+ return [];
2221
+ }
2222
+ const json = await res.json();
2223
+ rewards = Array.isArray(json) ? json : json?.data ?? json?.items ?? [];
2224
+ } catch (err) {
2225
+ console.warn("[opire] fetch failed \u2014", err);
2226
+ return [];
2227
+ }
2228
+ const candidates = [];
2229
+ for (const r of rewards) {
2230
+ if (r.platform !== "GitHub") continue;
2231
+ if (r.project && r.project.isPublic === false) continue;
2232
+ const repoFullName = repoFullNameFromUrl(r.project?.url ?? r.url);
2233
+ if (!repoFullName) continue;
2234
+ const amountUSD = priceToUSD(r.pendingPrice);
2235
+ if (amountUSD == null || amountUSD < MIN_USD || amountUSD > MAX_USD) continue;
2236
+ const title = (r.title ?? "").trim();
2237
+ if (title.length < 4) continue;
2238
+ const tags = normalize([...r.programmingLanguages ?? [], ...tokenize3(title)]);
2239
+ const bounty = {
2240
+ amountUSD,
2241
+ estimatedEffort: effortFromAmount2(amountUSD),
2242
+ bountySource: "opire",
2243
+ claimUrl: r.url,
2244
+ repoFullName
2245
+ };
2246
+ candidates.push({
2247
+ repoFullName,
2248
+ amountUSD,
2249
+ issueNumber: issueNumberFromUrl(r.url),
2250
+ bounty,
2251
+ job: {
2252
+ id: `bounty:opire:${r.id}`,
2253
+ source: "bounty",
2254
+ title,
2255
+ company: r.organization?.name ?? repoFullName.split("/")[0],
2256
+ url: r.url,
2257
+ remote: true,
2258
+ location: "Remote",
2259
+ tags,
2260
+ roleType: "freelance",
2261
+ postedAt: Number.isFinite(r.createdAt) ? new Date(r.createdAt).toISOString() : void 0,
2262
+ applyMode: "direct",
2263
+ bounty,
2264
+ raw: r
2265
+ }
2266
+ });
2267
+ }
2268
+ const distinctRepos = [...new Set(candidates.map((c) => c.repoFullName))];
2269
+ const meta = /* @__PURE__ */ new Map();
2270
+ for (let i = 0; i < distinctRepos.length; i += REPO_META_CONCURRENCY2) {
2271
+ const batch = distinctRepos.slice(i, i + REPO_META_CONCURRENCY2);
2272
+ const metas = await Promise.all(batch.map((name) => fetchRepoMeta2(name)));
2273
+ batch.forEach((name, k) => meta.set(name, metas[k]));
2274
+ }
2275
+ const gated = [];
2276
+ let dropped = 0;
2277
+ let ungated = 0;
2278
+ for (const c of candidates) {
2279
+ const m = meta.get(c.repoFullName);
2280
+ if (m) {
2281
+ if (!passesMaturityGate(m) || !passesAntiFarm(c.amountUSD, m.stargazers)) {
2282
+ dropped++;
2283
+ continue;
2284
+ }
2285
+ c.bounty.repoStars = m.stargazers;
2286
+ } else {
2287
+ ungated++;
2288
+ }
2289
+ gated.push(c);
2290
+ }
2291
+ const issueState = /* @__PURE__ */ new Map();
2292
+ for (let i = 0; i < gated.length; i += REPO_META_CONCURRENCY2) {
2293
+ const batch = gated.slice(i, i + REPO_META_CONCURRENCY2);
2294
+ const states = await Promise.all(
2295
+ batch.map(
2296
+ (c) => c.issueNumber != null ? fetchIssueState(c.repoFullName, c.issueNumber) : Promise.resolve(null)
2297
+ )
2298
+ );
2299
+ batch.forEach((c, k) => issueState.set(c.job.id, states[k]));
2300
+ }
2301
+ const jobs = [];
2302
+ let closed = 0;
2303
+ for (const c of gated) {
2304
+ if (jobs.length >= MAX_OPIRE_BOUNTIES) break;
2305
+ if (issueState.get(c.job.id) === "closed") {
2306
+ closed++;
2307
+ continue;
2308
+ }
2309
+ jobs.push(c.job);
2310
+ }
2311
+ console.info(
2312
+ `[opire] ${jobs.length} bounties (from ${rewards.length} rewards; ${dropped} repo-gated, ${closed} closed-issue, ${ungated} kept ungated)`
2313
+ );
2314
+ return jobs;
2315
+ }
2316
+ };
2317
+ }
2318
+ });
2319
+
2320
+ // ../../packages/core/src/feeds/workable.ts
2321
+ function locationStr(loc) {
2322
+ if (!loc) return "";
2323
+ return [loc.city, loc.country].filter(Boolean).join(", ");
2324
+ }
2325
+ function isRemote(j) {
2326
+ return j.remote === true || (j.workplace ?? "").toLowerCase() === "remote";
2327
+ }
2328
+ function extractTags7(j) {
2329
+ const body = [...j.department ?? [], locationStr(j.location)].filter(Boolean).join(" ");
2330
+ return extractSkillTags(j.title, body);
2331
+ }
2332
+ async function fetchAccount(account) {
2333
+ const url = `https://apply.workable.com/api/v3/accounts/${account}/jobs`;
2334
+ const out = [];
2335
+ let token;
2336
+ for (let page = 0; page < MAX_PAGES; page++) {
2337
+ let res;
2338
+ try {
2339
+ res = await fetchWithTimeout(url, {
2340
+ method: "POST",
2341
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
2342
+ body: JSON.stringify(token ? { token } : {})
2343
+ });
2344
+ } catch (err) {
2345
+ console.warn(`[workable] ${account}: network error \u2014`, err);
2346
+ break;
2347
+ }
2348
+ if (!res.ok) {
2349
+ console.warn(`[workable] ${account}: HTTP ${res.status}`);
2350
+ break;
2351
+ }
2352
+ let data;
2353
+ try {
2354
+ data = await res.json();
2355
+ } catch (err) {
2356
+ console.warn(`[workable] ${account}: JSON parse error \u2014`, err);
2357
+ break;
2358
+ }
2359
+ const results = data.results ?? [];
2360
+ for (const j of results) {
2361
+ if (j.state && j.state !== "published") continue;
2362
+ out.push({
2363
+ id: `workable:${j.id}`,
2364
+ source: "workable",
2365
+ title: j.title,
2366
+ company: account,
2367
+ url: `https://apply.workable.com/${account}/j/${j.shortcode}/`,
2368
+ remote: isRemote(j),
2369
+ location: locationStr(j.location) || void 0,
2370
+ tags: extractTags7(j),
2371
+ roleType: "full_time",
2372
+ postedAt: j.published,
2373
+ applyMode: "direct",
2374
+ raw: j
2375
+ });
2376
+ }
2377
+ token = data.token;
2378
+ if (!token || results.length === 0) break;
2379
+ }
2380
+ if (out.length > 0) console.info(`[workable] ${account}: ${out.length} jobs`);
2381
+ return out;
2382
+ }
2383
+ var FALLBACK_ACCOUNTS, MAX_PAGES, workable;
2384
+ var init_workable = __esm({
2385
+ "../../packages/core/src/feeds/workable.ts"() {
2386
+ "use strict";
2387
+ init_vocabulary();
2388
+ init_http();
2389
+ FALLBACK_ACCOUNTS = ["zego", "workmotion"];
2390
+ MAX_PAGES = 5;
2391
+ workable = {
2392
+ source: "workable",
2393
+ async fetch(opts) {
2394
+ const accounts = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : FALLBACK_ACCOUNTS;
2395
+ console.info(`[workable] fetching ${accounts.length} accounts: ${accounts.join(", ")}`);
2396
+ const results = await Promise.allSettled(accounts.map(fetchAccount));
1871
2397
  const jobs = [];
1872
2398
  let failures = 0;
1873
- for (const r of settled) {
1874
- if (r.status === "fulfilled") jobs.push(...r.value);
1875
- else {
2399
+ for (const r of results) {
2400
+ if (r.status === "fulfilled") {
2401
+ jobs.push(...r.value);
2402
+ } else {
1876
2403
  failures++;
1877
- console.warn("[github-bounties] repo fetch rejected:", r.reason);
2404
+ console.warn("[workable] account fetch rejected:", r.reason);
1878
2405
  }
1879
2406
  }
1880
- console.info(`[github-bounties] total: ${jobs.length} bounties, ${failures} repo failures`);
2407
+ console.info(`[workable] total: ${jobs.length} jobs, ${failures} account failures`);
1881
2408
  return jobs;
1882
2409
  }
1883
2410
  };
@@ -1886,7 +2413,57 @@ var init_github_bounties = __esm({
1886
2413
 
1887
2414
  // ../../packages/core/src/feeds/index.ts
1888
2415
  async function aggregateBounties(opts) {
1889
- return githubBounties.fetch({ slugs: opts?.repos });
2416
+ const [gh, op] = await Promise.all([
2417
+ githubBounties.fetch({ slugs: opts?.repos }),
2418
+ opire.fetch()
2419
+ ]);
2420
+ const allowlist = new Set(
2421
+ (opts?.repos && opts.repos.length > 0 ? opts.repos : DEFAULT_BOUNTY_REPOS).map(
2422
+ (r) => r.toLowerCase()
2423
+ )
2424
+ );
2425
+ const seen = /* @__PURE__ */ new Set();
2426
+ const perRepo = /* @__PURE__ */ new Map();
2427
+ const seenRepoTitles = /* @__PURE__ */ new Set();
2428
+ const out = [];
2429
+ for (const j of [...gh, ...op]) {
2430
+ const key = j.bounty?.claimUrl ?? j.url;
2431
+ if (seen.has(key)) continue;
2432
+ const repo = j.bounty?.repoFullName?.toLowerCase();
2433
+ if (repo) {
2434
+ if (isDenylistedRepo(repo)) continue;
2435
+ const titleKey = `${repo} ${normalizeBountyTitle(j.title)}`;
2436
+ if (seenRepoTitles.has(titleKey)) continue;
2437
+ const cap = allowlist.has(repo) ? MAX_BOUNTIES_PER_REPO : MAX_BOUNTIES_PER_DISCOVERED_REPO;
2438
+ const n = perRepo.get(repo) ?? 0;
2439
+ if (n >= cap) continue;
2440
+ perRepo.set(repo, n + 1);
2441
+ seenRepoTitles.add(titleKey);
2442
+ }
2443
+ seen.add(key);
2444
+ out.push(j);
2445
+ }
2446
+ const repos = [...new Set(out.map((j) => j.bounty?.repoFullName).filter((r) => !!r))];
2447
+ const refsByRepo = /* @__PURE__ */ new Map();
2448
+ const PR_REFS_CONCURRENCY = 15;
2449
+ for (let i = 0; i < repos.length; i += PR_REFS_CONCURRENCY) {
2450
+ const batch = repos.slice(i, i + PR_REFS_CONCURRENCY);
2451
+ const results = await Promise.all(batch.map((r) => fetchRepoOpenPRRefs(r)));
2452
+ batch.forEach((r, k) => refsByRepo.set(r, results[k]));
2453
+ }
2454
+ for (const j of out) {
2455
+ const num = bountyIssueNumber(j.bounty?.claimUrl);
2456
+ const refs = j.bounty?.repoFullName ? refsByRepo.get(j.bounty.repoFullName) : void 0;
2457
+ if (j.bounty && refs && num != null) j.bounty.competingOpenPRs = refs.get(num) ?? 0;
2458
+ }
2459
+ return out;
2460
+ }
2461
+ function bountyIssueNumber(url) {
2462
+ const m = url?.match(/\/issues\/(\d+)/);
2463
+ return m ? Number(m[1]) : void 0;
2464
+ }
2465
+ function normalizeBountyTitle(title) {
2466
+ return title.toLowerCase().replace(/#\d+\s*$/, "").replace(/[^a-z0-9]+/g, " ").trim();
1890
2467
  }
1891
2468
  function flattenTiers(t) {
1892
2469
  return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
@@ -1895,18 +2472,20 @@ async function aggregate(opts) {
1895
2472
  const ghSlugs = opts?.slugs?.["greenhouse"] ?? DEFAULT_GREENHOUSE_SLUGS;
1896
2473
  const ashbySlugs = opts?.slugs?.["ashby"] ?? DEFAULT_ASHBY_SLUGS;
1897
2474
  const leverSlugs = opts?.slugs?.["lever"] ?? DEFAULT_LEVER_SLUGS;
2475
+ const workableSlugs = opts?.slugs?.["workable"] ?? DEFAULT_WORKABLE_SLUGS;
1898
2476
  const limit = opts?.limit ?? 150;
1899
2477
  const settled = await Promise.allSettled([
1900
2478
  greenhouse.fetch({ slugs: ghSlugs, limit }),
1901
2479
  ashby.fetch({ slugs: ashbySlugs, limit }),
1902
2480
  lever.fetch({ slugs: leverSlugs, limit }),
2481
+ workable.fetch({ slugs: workableSlugs, limit }),
1903
2482
  himalayas.fetch({ limit }),
1904
2483
  wwr.fetch({ limit }),
1905
2484
  hn.fetch({ limit })
1906
2485
  ]);
1907
2486
  const seen = /* @__PURE__ */ new Set();
1908
2487
  const jobs = [];
1909
- const sourceNames = ["greenhouse", "ashby", "lever", "himalayas", "wwr", "hn"];
2488
+ const sourceNames = ["greenhouse", "ashby", "lever", "workable", "himalayas", "wwr", "hn"];
1910
2489
  for (let i = 0; i < settled.length; i++) {
1911
2490
  const result = settled[i];
1912
2491
  if (result.status === "rejected") {
@@ -1922,7 +2501,7 @@ async function aggregate(opts) {
1922
2501
  }
1923
2502
  if (opts?.includeBounties !== false) {
1924
2503
  try {
1925
- const bounties = await githubBounties.fetch({ slugs: opts?.slugs?.["bounty"], limit });
2504
+ const bounties = await aggregateBounties({ repos: opts?.slugs?.["bounty"] });
1926
2505
  for (const b of bounties) {
1927
2506
  if (!seen.has(b.id)) {
1928
2507
  seen.add(b.id);
@@ -1935,7 +2514,7 @@ async function aggregate(opts) {
1935
2514
  }
1936
2515
  return jobs;
1937
2516
  }
1938
- var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS;
2517
+ var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS, DEFAULT_WORKABLE_SLUGS;
1939
2518
  var init_feeds = __esm({
1940
2519
  "../../packages/core/src/feeds/index.ts"() {
1941
2520
  "use strict";
@@ -1946,8 +2525,11 @@ var init_feeds = __esm({
1946
2525
  init_wwr();
1947
2526
  init_hn();
1948
2527
  init_github_bounties();
2528
+ init_opire();
2529
+ init_workable();
1949
2530
  init_bounty_gate();
1950
- FEEDS = [greenhouse, ashby, lever, himalayas, wwr, hn];
2531
+ init_bounty_gate();
2532
+ FEEDS = [greenhouse, ashby, lever, workable, himalayas, wwr, hn];
1951
2533
  GREENHOUSE_SLUGS_BY_TIER = {
1952
2534
  bigco: [
1953
2535
  "stripe",
@@ -2053,6 +2635,7 @@ var init_feeds = __esm({
2053
2635
  DEFAULT_GREENHOUSE_SLUGS = flattenTiers(GREENHOUSE_SLUGS_BY_TIER);
2054
2636
  DEFAULT_ASHBY_SLUGS = flattenTiers(ASHBY_SLUGS_BY_TIER);
2055
2637
  DEFAULT_LEVER_SLUGS = flattenTiers(LEVER_SLUGS_BY_TIER);
2638
+ DEFAULT_WORKABLE_SLUGS = ["zego", "workmotion"];
2056
2639
  }
2057
2640
  });
2058
2641
 
@@ -2153,6 +2736,7 @@ __export(src_exports, {
2153
2736
  DEFAULT_BOUNTY_REPOS: () => DEFAULT_BOUNTY_REPOS,
2154
2737
  DEFAULT_GREENHOUSE_SLUGS: () => DEFAULT_GREENHOUSE_SLUGS,
2155
2738
  DEFAULT_LEVER_SLUGS: () => DEFAULT_LEVER_SLUGS,
2739
+ DEFAULT_WORKABLE_SLUGS: () => DEFAULT_WORKABLE_SLUGS,
2156
2740
  EXAMPLE_BUYER: () => EXAMPLE_BUYER,
2157
2741
  FEEDS: () => FEEDS,
2158
2742
  GRAPH: () => GRAPH,
@@ -2171,10 +2755,13 @@ __export(src_exports, {
2171
2755
  buildIndex: () => buildIndex,
2172
2756
  buildReason: () => buildReason,
2173
2757
  computeAcceptanceCredential: () => computeAcceptanceCredential,
2758
+ computeAcceptanceCredentialPublic: () => computeAcceptanceCredentialPublic,
2174
2759
  coreTagsFromTitle: () => coreTagsFromTitle,
2760
+ deriveResumeTrend: () => deriveResumeTrend,
2175
2761
  expandWeighted: () => expandWeighted,
2176
2762
  extractSkillTags: () => extractSkillTags,
2177
2763
  fetchGitHubProfile: () => fetchGitHubProfile,
2764
+ fetchRepoRecency: () => fetchRepoRecency,
2178
2765
  flattenTiers: () => flattenTiers,
2179
2766
  getBuyer: () => getBuyer,
2180
2767
  githubBounties: () => githubBounties,
@@ -2188,9 +2775,11 @@ __export(src_exports, {
2188
2775
  looksLikeEngRole: () => looksLikeEngRole,
2189
2776
  match: () => match,
2190
2777
  normalize: () => normalize,
2778
+ opire: () => opire,
2191
2779
  passesMaturityGate: () => passesMaturityGate,
2192
2780
  tokenize: () => tokenize,
2193
2781
  validateGraph: () => validateGraph,
2782
+ workable: () => workable,
2194
2783
  wwr: () => wwr
2195
2784
  });
2196
2785
  var init_src = __esm({
@@ -2480,11 +3069,11 @@ async function runLogin() {
2480
3069
  if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
2481
3070
  const { createRequire: createRequire2 } = await import("module");
2482
3071
  const { fileURLToPath: fileURLToPath7 } = await import("url");
2483
- const { join: join15, dirname: dirname3 } = await import("path");
3072
+ const { join: join17, dirname: dirname3 } = await import("path");
2484
3073
  const __dirname6 = fileURLToPath7(new URL(".", import.meta.url));
2485
- const fixturePath = join15(__dirname6, "../../fixtures/github-sample.json");
2486
- const { readFileSync: readFileSync14 } = await import("fs");
2487
- ghProfile = JSON.parse(readFileSync14(fixturePath, "utf8"));
3074
+ const fixturePath = join17(__dirname6, "../../fixtures/github-sample.json");
3075
+ const { readFileSync: readFileSync16 } = await import("fs");
3076
+ ghProfile = JSON.parse(readFileSync16(fixturePath, "utf8"));
2488
3077
  } else {
2489
3078
  ghProfile = await fetchGitHubProfile2(login, token);
2490
3079
  }
@@ -2882,9 +3471,11 @@ function printBounty(i, job, score, reason, matchedTags) {
2882
3471
  const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
2883
3472
  const effort = b.estimatedEffort ? ` \xB7 ${EFFORT_LABEL[b.estimatedEffort]}` : "";
2884
3473
  const scoreStr = score > 0 ? ` \xB7 match ${Math.round(score * 100)}%` : "";
3474
+ const prs = b.competingOpenPRs;
3475
+ const contend = prs != null && prs > 0 ? ` \xB7 \u26A0 ${prs} PR${prs === 1 ? "" : "s"} in flight` : "";
2885
3476
  console.log(`
2886
3477
  ${i + 1}. ${linkTitle2(job.title, job.url)}`);
2887
- console.log(` ${formatAmount(b)}${effort} \xB7 ${b.repoFullName ?? job.company}${stars}${scoreStr}`);
3478
+ console.log(` ${formatAmount(b)}${effort} \xB7 ${b.repoFullName ?? job.company}${stars}${scoreStr}${contend}`);
2888
3479
  if (reason) console.log(` ${reason}`);
2889
3480
  if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
2890
3481
  console.log(` id: ${job.id}`);
@@ -2896,6 +3487,7 @@ async function run3() {
2896
3487
  const index = await fetchIndex2();
2897
3488
  let bounties = (index.jobs ?? []).filter((j) => j.source === "bounty");
2898
3489
  if (PRICED_ONLY) bounties = bounties.filter((j) => j.bounty?.amountUSD != null);
3490
+ if (WINNABLE_ONLY) bounties = bounties.filter((j) => (j.bounty?.competingOpenPRs ?? 0) === 0);
2899
3491
  if (bounties.length === 0) {
2900
3492
  console.log("\nNo bounties available right now. Try again later \u2014 supply refreshes through the day.");
2901
3493
  return;
@@ -2915,7 +3507,8 @@ async function run3() {
2915
3507
  }
2916
3508
  const score = (j) => ranked.get(j.id)?.score ?? 0;
2917
3509
  const amt = (j) => j.bounty?.amountUSD ?? -1;
2918
- bounties.sort((a, b) => score(b) - score(a) || amt(b) - amt(a));
3510
+ const contested = (j) => (j.bounty?.competingOpenPRs ?? 0) > 0 ? 1 : 0;
3511
+ bounties.sort((a, b) => contested(a) - contested(b) || score(b) - score(a) || amt(b) - amt(a));
2919
3512
  const shown = SHOW_ALL2 ? bounties : bounties.slice(0, LIMIT2);
2920
3513
  const matchedCount = bounties.filter((j) => score(j) > 0).length;
2921
3514
  console.log(
@@ -2948,7 +3541,7 @@ Open this to claim/work the bounty (you go straight to the source \u2014 we neve
2948
3541
  process.exit(1);
2949
3542
  }
2950
3543
  }
2951
- var TERMINALHIRE_DIR4, INDEX_CACHE_FILE2, INDEX_TTL_MS2, API_URL2, DEFAULT_LIMIT2, args2, limitArg2, LIMIT2, PRICED_ONLY, SHOW_ALL2, EFFORT_LABEL;
3544
+ var TERMINALHIRE_DIR4, INDEX_CACHE_FILE2, INDEX_TTL_MS2, API_URL2, DEFAULT_LIMIT2, args2, limitArg2, LIMIT2, PRICED_ONLY, SHOW_ALL2, WINNABLE_ONLY, EFFORT_LABEL;
2952
3545
  var init_jpi_bounties = __esm({
2953
3546
  "bin/jpi-bounties.js"() {
2954
3547
  "use strict";
@@ -2962,14 +3555,378 @@ var init_jpi_bounties = __esm({
2962
3555
  LIMIT2 = limitArg2 !== -1 ? parseInt(args2[limitArg2 + 1] ?? "15", 10) : DEFAULT_LIMIT2;
2963
3556
  PRICED_ONLY = args2.includes("--priced");
2964
3557
  SHOW_ALL2 = args2.includes("--all");
3558
+ WINNABLE_ONLY = args2.includes("--winnable");
2965
3559
  EFFORT_LABEL = { small: "small (~\xBD day)", medium: "medium (~1 day)", large: "large (multi-day)" };
2966
3560
  }
2967
3561
  });
2968
3562
 
3563
+ // src/claims.ts
3564
+ var claims_exports = {};
3565
+ __export(claims_exports, {
3566
+ acceptedPRRate: () => acceptedPRRate,
3567
+ findClaim: () => findClaim,
3568
+ listClaims: () => listClaims,
3569
+ readClaims: () => readClaims,
3570
+ recordClaim: () => recordClaim,
3571
+ removeClaim: () => removeClaim,
3572
+ updateClaim: () => updateClaim
3573
+ });
3574
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, renameSync, existsSync as existsSync4 } from "fs";
3575
+ import { join as join6 } from "path";
3576
+ import { homedir as homedir5 } from "os";
3577
+ function nowISO() {
3578
+ return (/* @__PURE__ */ new Date()).toISOString();
3579
+ }
3580
+ function readClaims() {
3581
+ try {
3582
+ if (!existsSync4(CLAIMS_FILE)) return [];
3583
+ const data = JSON.parse(readFileSync6(CLAIMS_FILE, "utf8"));
3584
+ return Array.isArray(data?.claims) ? data.claims : [];
3585
+ } catch {
3586
+ return [];
3587
+ }
3588
+ }
3589
+ function writeClaims(claims) {
3590
+ mkdirSync5(TERMINALHIRE_DIR5, { recursive: true });
3591
+ const tmp = `${CLAIMS_FILE}.tmp`;
3592
+ const payload = { claims };
3593
+ writeFileSync5(tmp, JSON.stringify(payload, null, 2), "utf8");
3594
+ renameSync(tmp, CLAIMS_FILE);
3595
+ }
3596
+ function findClaim(id) {
3597
+ return readClaims().find((c) => c.id === id) ?? null;
3598
+ }
3599
+ function listClaims(opts = {}) {
3600
+ const claims = readClaims();
3601
+ if (!opts.active) return claims;
3602
+ return claims.filter((c) => !TERMINAL_STATES.has(c.state));
3603
+ }
3604
+ function recordClaim(rec) {
3605
+ const claims = readClaims();
3606
+ if (claims.some((c) => c.id === rec.id)) {
3607
+ throw new Error(
3608
+ `claim already exists for '${rec.id}' \u2014 run 'terminalhire claim status ${rec.id}' or 'terminalhire claim release ${rec.id}'`
3609
+ );
3610
+ }
3611
+ const ts = nowISO();
3612
+ const claim = {
3613
+ ...rec,
3614
+ state: "claimed",
3615
+ worktreePath: null,
3616
+ branch: null,
3617
+ prUrl: null,
3618
+ review: null,
3619
+ claimedAt: ts,
3620
+ updatedAt: ts
3621
+ };
3622
+ claims.push(claim);
3623
+ writeClaims(claims);
3624
+ return claim;
3625
+ }
3626
+ function updateClaim(id, patch) {
3627
+ const claims = readClaims();
3628
+ const idx = claims.findIndex((c) => c.id === id);
3629
+ if (idx === -1) return null;
3630
+ claims[idx] = { ...claims[idx], ...patch, updatedAt: nowISO() };
3631
+ writeClaims(claims);
3632
+ return claims[idx];
3633
+ }
3634
+ function removeClaim(id) {
3635
+ const claims = readClaims();
3636
+ const next = claims.filter((c) => c.id !== id);
3637
+ if (next.length === claims.length) return false;
3638
+ writeClaims(next);
3639
+ return true;
3640
+ }
3641
+ function acceptedPRRate(claims = readClaims()) {
3642
+ const total = claims.length;
3643
+ const merged = claims.filter((c) => c.state === "merged").length;
3644
+ return { merged, total, rate: total === 0 ? 0 : merged / total };
3645
+ }
3646
+ var TERMINALHIRE_DIR5, CLAIMS_FILE, TERMINAL_STATES;
3647
+ var init_claims = __esm({
3648
+ "src/claims.ts"() {
3649
+ "use strict";
3650
+ TERMINALHIRE_DIR5 = join6(homedir5(), ".terminalhire");
3651
+ CLAIMS_FILE = join6(TERMINALHIRE_DIR5, "claims.json");
3652
+ TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
3653
+ }
3654
+ });
3655
+
3656
+ // bin/jpi-claim.js
3657
+ var jpi_claim_exports = {};
3658
+ __export(jpi_claim_exports, {
3659
+ run: () => run4
3660
+ });
3661
+ import { readFileSync as readFileSync7, existsSync as existsSync5 } from "fs";
3662
+ import { join as join7 } from "path";
3663
+ import { homedir as homedir6 } from "os";
3664
+ function findBountyInCache(bountyId) {
3665
+ try {
3666
+ if (!existsSync5(INDEX_CACHE_FILE3)) return null;
3667
+ const entry = JSON.parse(readFileSync7(INDEX_CACHE_FILE3, "utf8"));
3668
+ const jobs = entry?.index?.jobs ?? [];
3669
+ const job = jobs.find((j) => j.id === bountyId && j.source === "bounty");
3670
+ return job ?? null;
3671
+ } catch {
3672
+ return null;
3673
+ }
3674
+ }
3675
+ function parseGitHubUrl(url) {
3676
+ const m = String(url ?? "").match(/github\.com\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)/);
3677
+ if (!m) return null;
3678
+ return { owner: m[1], repo: m[2], number: parseInt(m[3], 10), repoFullName: `${m[1]}/${m[2]}` };
3679
+ }
3680
+ async function countOpenPRsReferencingIssue(repoFullName, issueNumber) {
3681
+ try {
3682
+ const res = await fetch(`${GH_API}/repos/${repoFullName}/pulls?state=open&per_page=100`, {
3683
+ headers: GH_HEADERS,
3684
+ signal: AbortSignal.timeout(1e4)
3685
+ });
3686
+ if (!res.ok) return null;
3687
+ const prs = await res.json();
3688
+ if (!Array.isArray(prs)) return null;
3689
+ if (prs.length === 100) return null;
3690
+ const needle = new RegExp(`#${issueNumber}\\b`);
3691
+ return prs.filter((p) => needle.test(`${p.title ?? ""}
3692
+ ${p.body ?? ""}`)).length;
3693
+ } catch {
3694
+ return null;
3695
+ }
3696
+ }
3697
+ async function fetchIssueState2(repoFullName, issueNumber) {
3698
+ try {
3699
+ const res = await fetch(`${GH_API}/repos/${repoFullName}/issues/${issueNumber}`, {
3700
+ headers: GH_HEADERS,
3701
+ signal: AbortSignal.timeout(1e4)
3702
+ });
3703
+ if (!res.ok) return null;
3704
+ const issue = await res.json();
3705
+ return issue.state === "open" ? "open" : issue.state === "closed" ? "closed" : null;
3706
+ } catch {
3707
+ return null;
3708
+ }
3709
+ }
3710
+ async function pollPR(prUrl) {
3711
+ const p = parseGitHubUrl(prUrl);
3712
+ if (!p) return null;
3713
+ try {
3714
+ const res = await fetch(`${GH_API}/repos/${p.repoFullName}/pulls/${p.number}`, {
3715
+ headers: GH_HEADERS,
3716
+ signal: AbortSignal.timeout(1e4)
3717
+ });
3718
+ if (!res.ok) return null;
3719
+ const pr = await res.json();
3720
+ return { merged: pr.merged === true, state: pr.state };
3721
+ } catch {
3722
+ return null;
3723
+ }
3724
+ }
3725
+ function fmtAmount(a) {
3726
+ return a != null ? "$" + a.toLocaleString() : "$\u2014";
3727
+ }
3728
+ function printMetric(rate) {
3729
+ const pct = Math.round(rate.rate * 100);
3730
+ console.log(`
3731
+ \u{1F4CA} Accepted-PR rate: ${rate.merged}/${rate.total} claims merged (${pct}%)`);
3732
+ }
3733
+ async function cmdRecord(arg) {
3734
+ const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
3735
+ if (!arg) {
3736
+ console.error("Usage: terminalhire claim record <bountyId|issueUrl>");
3737
+ console.error(" Run `terminalhire bounties` first to populate the local index cache,");
3738
+ console.error(" then pass the id shown in its output \u2014 or pass a GitHub issue URL directly.");
3739
+ process.exit(1);
3740
+ }
3741
+ let bountyId, title, repoFullName, issueUrl, amountUSD;
3742
+ const job = findBountyInCache(arg);
3743
+ if (job) {
3744
+ const b = job.bounty ?? {};
3745
+ bountyId = job.id;
3746
+ title = job.title;
3747
+ repoFullName = b.repoFullName ?? job.company ?? "";
3748
+ issueUrl = b.claimUrl ?? job.url ?? "";
3749
+ amountUSD = b.amountUSD ?? null;
3750
+ } else {
3751
+ const parsed = parseGitHubUrl(arg);
3752
+ if (!parsed) {
3753
+ console.error(`terminalhire claim: '${arg}' is not in the index cache and is not a GitHub issue URL.`);
3754
+ console.error(" Run `terminalhire bounties` to populate the cache, or pass a full issue URL.");
3755
+ process.exit(1);
3756
+ }
3757
+ bountyId = `gh:${parsed.repoFullName}#${parsed.number}`;
3758
+ title = `${parsed.repoFullName}#${parsed.number}`;
3759
+ repoFullName = parsed.repoFullName;
3760
+ issueUrl = arg;
3761
+ amountUSD = null;
3762
+ }
3763
+ const ghIssue = parseGitHubUrl(issueUrl);
3764
+ const [issueState, openPRs] = ghIssue ? await Promise.all([
3765
+ fetchIssueState2(repoFullName, ghIssue.number),
3766
+ countOpenPRsReferencingIssue(repoFullName, ghIssue.number)
3767
+ // Guardrail #5
3768
+ ]) : [null, null];
3769
+ if (issueState === "closed") {
3770
+ console.error(
3771
+ `terminalhire claim: ${repoFullName}#${ghIssue.number} is CLOSED \u2014 not claimable.
3772
+ The bounty index drops closed issues; this one is likely a stale cache entry.
3773
+ Run \`terminalhire bounties\` for the current open pool.`
3774
+ );
3775
+ process.exit(1);
3776
+ }
3777
+ let claim;
3778
+ try {
3779
+ claim = claims.recordClaim({ id: bountyId, bountyId, title, repoFullName, issueUrl, amountUSD, openPRsAtClaim: openPRs });
3780
+ } catch (err) {
3781
+ console.error(`terminalhire claim: ${err.message ?? err}`);
3782
+ process.exit(1);
3783
+ }
3784
+ console.log(`
3785
+ \u2713 Claimed: ${claim.title}`);
3786
+ console.log(` id: ${claim.id}`);
3787
+ console.log(` repo: ${claim.repoFullName}`);
3788
+ console.log(` amount: ${fmtAmount(claim.amountUSD)}`);
3789
+ console.log(` issue: ${claim.issueUrl}`);
3790
+ if (openPRs == null) {
3791
+ console.log(" open PRs: unknown (GitHub read unavailable \u2014 check the issue manually before working)");
3792
+ } else if (openPRs > 0) {
3793
+ console.log(` \u26A0 open PRs referencing this issue: ${openPRs} \u2014 someone may already be on it. Check before working.`);
3794
+ } else {
3795
+ console.log(" open PRs referencing this issue: 0");
3796
+ }
3797
+ console.log("\n Executor constraints (enforce when spawning the background agent):");
3798
+ console.log(" \u2022 work in an ISOLATED git worktree; scrub the subprocess env (no token/profile inheritance)");
3799
+ console.log(" \u2022 MUST NOT `git push` or `gh pr` \u2014 pushing happens only via `terminalhire submit`");
3800
+ console.log(" \u2022 clone + static analysis + patch only; NO test/build execution without explicit approval");
3801
+ console.log(" \u2022 no access to ~/.terminalhire (the executor never needs your profile)");
3802
+ console.log("\n Next: do the work, then `terminalhire claim update " + claim.id + " <state>` as you progress.");
3803
+ }
3804
+ async function cmdList(active) {
3805
+ const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
3806
+ const list = claims.listClaims({ active });
3807
+ if (list.length === 0) {
3808
+ console.log(active ? "No active claims." : "No claims yet. Use `terminalhire claim record <bountyId>`.");
3809
+ return;
3810
+ }
3811
+ console.log(`
3812
+ ${list.length} ${active ? "active " : ""}claim${list.length === 1 ? "" : "s"}:
3813
+ `);
3814
+ for (const c of list) {
3815
+ const pr = c.prUrl ? ` \xB7 ${c.prUrl}` : "";
3816
+ console.log(` [${c.state}] ${fmtAmount(c.amountUSD)} \xB7 ${c.title}`);
3817
+ console.log(` id: ${c.id}${pr}`);
3818
+ }
3819
+ printMetric(claims.acceptedPRRate());
3820
+ }
3821
+ async function cmdStatus(id) {
3822
+ const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
3823
+ const targets = id ? [claims.findClaim(id)].filter(Boolean) : claims.listClaims();
3824
+ if (targets.length === 0) {
3825
+ console.log(id ? `No claim with id '${id}'.` : "No claims to poll.");
3826
+ return;
3827
+ }
3828
+ let polled = 0;
3829
+ for (const c of targets) {
3830
+ if (!c.prUrl) continue;
3831
+ const res = await pollPR(c.prUrl);
3832
+ if (!res) {
3833
+ console.log(` ? ${c.title} \u2014 could not read PR state (${c.prUrl})`);
3834
+ continue;
3835
+ }
3836
+ polled++;
3837
+ let next = c.state;
3838
+ if (res.merged) next = "merged";
3839
+ else if (res.state === "closed") next = "abandoned";
3840
+ else next = "submitted";
3841
+ const ORDER = ["claimed", "working", "in-review", "ready", "submitted", "merged", "abandoned"];
3842
+ if (next !== c.state && ORDER.indexOf(next) > ORDER.indexOf(c.state)) {
3843
+ claims.updateClaim(c.id, { state: next });
3844
+ }
3845
+ const mark = res.merged ? "\u2713 merged" : res.state === "closed" ? "\u2717 closed (unmerged)" : "\u2026 open";
3846
+ console.log(` ${mark} \u2014 ${c.title} (${c.prUrl})`);
3847
+ }
3848
+ if (polled === 0) console.log(" No submitted claims with a PR URL yet. Set one via `claim update <id> submitted` after `submit`.");
3849
+ printMetric(claims.acceptedPRRate());
3850
+ }
3851
+ async function cmdUpdate(id, state, prUrl) {
3852
+ const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
3853
+ const VALID = ["claimed", "working", "in-review", "ready", "submitted", "merged", "abandoned"];
3854
+ if (!id || !state || !VALID.includes(state)) {
3855
+ console.error("Usage: terminalhire claim update <id> <state> [prUrl]");
3856
+ console.error(" state: " + VALID.join(" | "));
3857
+ console.error(" prUrl: attach the source PR URL (so `claim status` can poll its merge state)");
3858
+ process.exit(1);
3859
+ }
3860
+ const patch = { state };
3861
+ if (prUrl) {
3862
+ if (!parseGitHubUrl(prUrl)) {
3863
+ console.error(`terminalhire claim: '${prUrl}' is not a GitHub PR URL.`);
3864
+ process.exit(1);
3865
+ }
3866
+ patch.prUrl = prUrl;
3867
+ }
3868
+ const updated = claims.updateClaim(id, patch);
3869
+ if (!updated) {
3870
+ console.error(`terminalhire claim: no claim with id '${id}'.`);
3871
+ process.exit(1);
3872
+ }
3873
+ console.log(`Updated ${id} \u2192 ${state}${prUrl ? ` (PR: ${prUrl})` : ""}`);
3874
+ }
3875
+ async function cmdRelease(id) {
3876
+ const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
3877
+ if (!id) {
3878
+ console.error("Usage: terminalhire claim release <id>");
3879
+ process.exit(1);
3880
+ }
3881
+ const removed = claims.removeClaim(id);
3882
+ console.log(removed ? `Released claim: ${id}` : `terminalhire claim: no claim with id '${id}'.`);
3883
+ if (!removed) process.exit(1);
3884
+ }
3885
+ async function run4() {
3886
+ const verb = process.argv[2];
3887
+ const rest = process.argv.slice(3).filter((a) => !a.startsWith("--"));
3888
+ const active = process.argv.includes("--active");
3889
+ try {
3890
+ switch (verb) {
3891
+ case "record":
3892
+ await cmdRecord(rest[0]);
3893
+ break;
3894
+ case "list":
3895
+ await cmdList(active);
3896
+ break;
3897
+ case "status":
3898
+ await cmdStatus(rest[0]);
3899
+ break;
3900
+ case "update":
3901
+ await cmdUpdate(rest[0], rest[1], rest[2]);
3902
+ break;
3903
+ case "release":
3904
+ await cmdRelease(rest[0]);
3905
+ break;
3906
+ default:
3907
+ console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: record | list | status | update | release`);
3908
+ process.exit(1);
3909
+ }
3910
+ } catch (err) {
3911
+ console.error("terminalhire claim error:", err.message ?? err);
3912
+ process.exit(1);
3913
+ }
3914
+ }
3915
+ var TERMINALHIRE_DIR6, INDEX_CACHE_FILE3, GH_API, GH_HEADERS;
3916
+ var init_jpi_claim = __esm({
3917
+ "bin/jpi-claim.js"() {
3918
+ "use strict";
3919
+ TERMINALHIRE_DIR6 = join7(homedir6(), ".terminalhire");
3920
+ INDEX_CACHE_FILE3 = join7(TERMINALHIRE_DIR6, "index-cache.json");
3921
+ GH_API = "https://api.github.com";
3922
+ GH_HEADERS = { "User-Agent": "terminalhire-claim", Accept: "application/vnd.github+json" };
3923
+ }
3924
+ });
3925
+
2969
3926
  // bin/jpi-profile.js
2970
3927
  var jpi_profile_exports = {};
2971
3928
  __export(jpi_profile_exports, {
2972
- run: () => run4
3929
+ run: () => run5
2973
3930
  });
2974
3931
  import { createInterface as createInterface3 } from "readline";
2975
3932
  function prompt3(question) {
@@ -2981,7 +3938,7 @@ function prompt3(question) {
2981
3938
  });
2982
3939
  });
2983
3940
  }
2984
- async function run4() {
3941
+ async function run5() {
2985
3942
  const { readProfile: readProfile2, writeProfile: writeProfile2, deleteProfile: deleteProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
2986
3943
  const args3 = process.argv.slice(2);
2987
3944
  if (args3.includes("--show")) {
@@ -3054,9 +4011,9 @@ var signal_exports = {};
3054
4011
  __export(signal_exports, {
3055
4012
  extractFingerprint: () => extractFingerprint
3056
4013
  });
3057
- import { readFileSync as readFileSync6, readdirSync } from "fs";
4014
+ import { readFileSync as readFileSync8, readdirSync } from "fs";
3058
4015
  import { execFileSync } from "child_process";
3059
- import { join as join6 } from "path";
4016
+ import { join as join8 } from "path";
3060
4017
  function safeGit(args3, cwd) {
3061
4018
  try {
3062
4019
  return execFileSync("git", ["-C", cwd, ...args3], {
@@ -3084,20 +4041,20 @@ function isEmployerContext(cwd) {
3084
4041
  }
3085
4042
  function readJsonSafe(path) {
3086
4043
  try {
3087
- return JSON.parse(readFileSync6(path, "utf8"));
4044
+ return JSON.parse(readFileSync8(path, "utf8"));
3088
4045
  } catch {
3089
4046
  return null;
3090
4047
  }
3091
4048
  }
3092
4049
  function readFileSafe(path) {
3093
4050
  try {
3094
- return readFileSync6(path, "utf8");
4051
+ return readFileSync8(path, "utf8");
3095
4052
  } catch {
3096
4053
  return "";
3097
4054
  }
3098
4055
  }
3099
4056
  function tokensFromPackageJson(cwd) {
3100
- const pkg = readJsonSafe(join6(cwd, "package.json"));
4057
+ const pkg = readJsonSafe(join8(cwd, "package.json"));
3101
4058
  if (!pkg || typeof pkg !== "object") return [];
3102
4059
  const p = pkg;
3103
4060
  const deps = {
@@ -3111,9 +4068,9 @@ function workspaceMemberDirs(cwd) {
3111
4068
  const dirs = [cwd];
3112
4069
  for (const group of ["apps", "packages"]) {
3113
4070
  try {
3114
- const groupDir = join6(cwd, group);
4071
+ const groupDir = join8(cwd, group);
3115
4072
  for (const e of readdirSync(groupDir, { withFileTypes: true })) {
3116
- if (e.isDirectory() && !e.isSymbolicLink()) dirs.push(join6(groupDir, e.name));
4073
+ if (e.isDirectory() && !e.isSymbolicLink()) dirs.push(join8(groupDir, e.name));
3117
4074
  }
3118
4075
  } catch {
3119
4076
  }
@@ -3121,18 +4078,18 @@ function workspaceMemberDirs(cwd) {
3121
4078
  return dirs;
3122
4079
  }
3123
4080
  function tokensFromRequirementsTxt(cwd) {
3124
- const content = readFileSafe(join6(cwd, "requirements.txt"));
4081
+ const content = readFileSafe(join8(cwd, "requirements.txt"));
3125
4082
  if (!content) return [];
3126
4083
  return content.split("\n").map((l) => l.trim().split(/[>=<!\[;]/)[0].trim().toLowerCase()).filter(Boolean);
3127
4084
  }
3128
4085
  function tokensFromGoMod(cwd) {
3129
- const content = readFileSafe(join6(cwd, "go.mod"));
4086
+ const content = readFileSafe(join8(cwd, "go.mod"));
3130
4087
  if (!content) return [];
3131
4088
  const requires = Array.from(content.matchAll(/^\s+([^\s]+)\s+v/gm)).map((m) => m[1].split("/").pop() ?? "").filter(Boolean);
3132
4089
  return ["go", ...requires];
3133
4090
  }
3134
4091
  function tokensFromCargoToml(cwd) {
3135
- const content = readFileSafe(join6(cwd, "Cargo.toml"));
4092
+ const content = readFileSafe(join8(cwd, "Cargo.toml"));
3136
4093
  if (!content) return [];
3137
4094
  const deps = [];
3138
4095
  let inDeps = false;
@@ -3153,7 +4110,7 @@ function tokensFromFileExtensions(cwd) {
3153
4110
  const tokens = [];
3154
4111
  const scanDirs = [cwd];
3155
4112
  try {
3156
- const srcDir = join6(cwd, "src");
4113
+ const srcDir = join8(cwd, "src");
3157
4114
  readdirSync(srcDir);
3158
4115
  scanDirs.push(srcDir);
3159
4116
  } catch {
@@ -3314,9 +4271,9 @@ var init_signal = __esm({
3314
4271
  // bin/jpi-learn.js
3315
4272
  var jpi_learn_exports = {};
3316
4273
  __export(jpi_learn_exports, {
3317
- run: () => run5
4274
+ run: () => run6
3318
4275
  });
3319
- async function run5() {
4276
+ async function run6() {
3320
4277
  try {
3321
4278
  const args3 = process.argv.slice(2);
3322
4279
  const cwdIdx = args3.indexOf("--cwd");
@@ -3343,7 +4300,7 @@ var init_jpi_learn = __esm({
3343
4300
  "use strict";
3344
4301
  isMain = process.argv[1]?.endsWith("jpi-learn.js") || process.argv[1]?.endsWith("jpi-learn");
3345
4302
  if (isMain) {
3346
- run5();
4303
+ run6();
3347
4304
  }
3348
4305
  }
3349
4306
  });
@@ -3351,23 +4308,23 @@ var init_jpi_learn = __esm({
3351
4308
  // bin/jpi-config.js
3352
4309
  var jpi_config_exports = {};
3353
4310
  __export(jpi_config_exports, {
3354
- run: () => run6
4311
+ run: () => run7
3355
4312
  });
3356
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync4 } from "fs";
3357
- import { join as join7 } from "path";
3358
- import { homedir as homedir5 } from "os";
4313
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync6 } from "fs";
4314
+ import { join as join9 } from "path";
4315
+ import { homedir as homedir7 } from "os";
3359
4316
  function readConfig() {
3360
4317
  try {
3361
- if (!existsSync4(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
3362
- return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync7(CONFIG_FILE, "utf8")) };
4318
+ if (!existsSync6(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
4319
+ return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync9(CONFIG_FILE, "utf8")) };
3363
4320
  } catch {
3364
4321
  return { ...DEFAULT_CONFIG };
3365
4322
  }
3366
4323
  }
3367
4324
  function writeConfig(patch) {
3368
- mkdirSync5(TERMINALHIRE_DIR5, { recursive: true });
4325
+ mkdirSync6(TERMINALHIRE_DIR7, { recursive: true });
3369
4326
  const merged = { ...readConfig(), ...patch };
3370
- writeFileSync5(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
4327
+ writeFileSync6(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
3371
4328
  }
3372
4329
  function parseNudgeMode(raw) {
3373
4330
  if (raw === "session" || raw === "always") return raw;
@@ -3375,7 +4332,7 @@ function parseNudgeMode(raw) {
3375
4332
  if (m && parseInt(m[1], 10) >= 1) return raw;
3376
4333
  return null;
3377
4334
  }
3378
- async function run6() {
4335
+ async function run7() {
3379
4336
  const args3 = process.argv.slice(2);
3380
4337
  const filtered = args3[0] === "config" ? args3.slice(1) : args3;
3381
4338
  if (filtered.includes("--show") || filtered.length === 0) {
@@ -3418,12 +4375,12 @@ async function run6() {
3418
4375
  console.error(" terminalhire config --show");
3419
4376
  process.exit(1);
3420
4377
  }
3421
- var TERMINALHIRE_DIR5, CONFIG_FILE, DEFAULT_CONFIG;
4378
+ var TERMINALHIRE_DIR7, CONFIG_FILE, DEFAULT_CONFIG;
3422
4379
  var init_jpi_config = __esm({
3423
4380
  "bin/jpi-config.js"() {
3424
4381
  "use strict";
3425
- TERMINALHIRE_DIR5 = join7(homedir5(), ".terminalhire");
3426
- CONFIG_FILE = join7(TERMINALHIRE_DIR5, "config.json");
4382
+ TERMINALHIRE_DIR7 = join9(homedir7(), ".terminalhire");
4383
+ CONFIG_FILE = join9(TERMINALHIRE_DIR7, "config.json");
3427
4384
  DEFAULT_CONFIG = { nudge: "session" };
3428
4385
  }
3429
4386
  });
@@ -3446,26 +4403,26 @@ __export(spinner_exports, {
3446
4403
  readSpinnerConfig: () => readSpinnerConfig
3447
4404
  });
3448
4405
  import {
3449
- readFileSync as readFileSync8,
3450
- writeFileSync as writeFileSync6,
3451
- existsSync as existsSync5,
3452
- mkdirSync as mkdirSync6,
3453
- renameSync
4406
+ readFileSync as readFileSync10,
4407
+ writeFileSync as writeFileSync7,
4408
+ existsSync as existsSync7,
4409
+ mkdirSync as mkdirSync7,
4410
+ renameSync as renameSync2
3454
4411
  } from "fs";
3455
- import { join as join8, dirname } from "path";
3456
- import { homedir as homedir6 } from "os";
4412
+ import { join as join10, dirname } from "path";
4413
+ import { homedir as homedir8 } from "os";
3457
4414
  function readJson(path, fallback) {
3458
4415
  try {
3459
- return existsSync5(path) ? JSON.parse(readFileSync8(path, "utf8")) : fallback;
4416
+ return existsSync7(path) ? JSON.parse(readFileSync10(path, "utf8")) : fallback;
3460
4417
  } catch {
3461
4418
  return fallback;
3462
4419
  }
3463
4420
  }
3464
4421
  function atomicWriteJson(path, obj) {
3465
- mkdirSync6(dirname(path), { recursive: true });
4422
+ mkdirSync7(dirname(path), { recursive: true });
3466
4423
  const tmp = `${path}.tmp-${process.pid}`;
3467
- writeFileSync6(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
3468
- renameSync(tmp, path);
4424
+ writeFileSync7(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
4425
+ renameSync2(tmp, path);
3469
4426
  }
3470
4427
  function titleCase(s) {
3471
4428
  return String(s || "").replace(/\b\w/g, (c) => c.toUpperCase());
@@ -3548,7 +4505,7 @@ function buildContextVerbs(topMatches, sessionTags) {
3548
4505
  }
3549
4506
  const list = Array.isArray(topMatches) ? topMatches : [];
3550
4507
  const hasBounty = list.some((m) => m && m.source === "bounty");
3551
- if (hasBounty) headers.unshift(`\u2726 Roles + \u{1F48E} paid bounties in your stack \u2014 link below`);
4508
+ if (hasBounty) headers.push(`\u2726 Roles + \u{1F48E} paid bounties in your stack \u2014 link below`);
3552
4509
  return headers;
3553
4510
  }
3554
4511
  function buildSpinnerPool(topMatches, max = 6, opts = {}) {
@@ -3640,8 +4597,8 @@ function buildTips(topMatches, baseUrl, max = 8) {
3640
4597
  let bi = 0;
3641
4598
  let ri = 0;
3642
4599
  while (bi < bountyQ.length || ri < roleQ.length) {
3643
- if (bi < bountyQ.length) ordered.push(bountyQ[bi++]);
3644
4600
  if (ri < roleQ.length) ordered.push(roleQ[ri++]);
4601
+ if (bi < bountyQ.length) ordered.push(bountyQ[bi++]);
3645
4602
  }
3646
4603
  for (const m of ordered) {
3647
4604
  if (!m || !m.title || !m.company || !m.id) continue;
@@ -3716,10 +4673,10 @@ var TH_DIR, CLAUDE_SETTINGS, CONFIG_FILE2, SPINNER_STATE_FILE, SPINNER_DEFAULTS,
3716
4673
  var init_spinner = __esm({
3717
4674
  "bin/spinner.js"() {
3718
4675
  "use strict";
3719
- TH_DIR = process.env["TERMINALHIRE_DIR"] || join8(homedir6(), ".terminalhire");
3720
- CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join8(homedir6(), ".claude", "settings.json");
3721
- CONFIG_FILE2 = join8(TH_DIR, "config.json");
3722
- SPINNER_STATE_FILE = join8(TH_DIR, "spinner-state.json");
4676
+ TH_DIR = process.env["TERMINALHIRE_DIR"] || join10(homedir8(), ".terminalhire");
4677
+ CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join10(homedir8(), ".claude", "settings.json");
4678
+ CONFIG_FILE2 = join10(TH_DIR, "config.json");
4679
+ SPINNER_STATE_FILE = join10(TH_DIR, "spinner-state.json");
3723
4680
  SPINNER_DEFAULTS = { enabled: false, mode: "append", max: 6, frequency: "sometimes" };
3724
4681
  VERB_INTROS = ["Matched:", "You\u2019d fit:", "Worth a look:", "On your radar:", "Fits your stack:"];
3725
4682
  }
@@ -3728,32 +4685,32 @@ var init_spinner = __esm({
3728
4685
  // bin/jpi-spinner.js
3729
4686
  var jpi_spinner_exports = {};
3730
4687
  __export(jpi_spinner_exports, {
3731
- run: () => run7
4688
+ run: () => run8
3732
4689
  });
3733
4690
  import {
3734
- readFileSync as readFileSync9,
3735
- writeFileSync as writeFileSync7,
4691
+ readFileSync as readFileSync11,
4692
+ writeFileSync as writeFileSync8,
3736
4693
  copyFileSync,
3737
- existsSync as existsSync6,
3738
- mkdirSync as mkdirSync7
4694
+ existsSync as existsSync8,
4695
+ mkdirSync as mkdirSync8
3739
4696
  } from "fs";
3740
- import { join as join9 } from "path";
3741
- import { homedir as homedir7 } from "os";
4697
+ import { join as join11 } from "path";
4698
+ import { homedir as homedir9 } from "os";
3742
4699
  import { createInterface as createInterface4 } from "readline";
3743
4700
  function readConfig2() {
3744
4701
  try {
3745
- return existsSync6(CONFIG_FILE3) ? JSON.parse(readFileSync9(CONFIG_FILE3, "utf8")) : {};
4702
+ return existsSync8(CONFIG_FILE3) ? JSON.parse(readFileSync11(CONFIG_FILE3, "utf8")) : {};
3746
4703
  } catch {
3747
4704
  return {};
3748
4705
  }
3749
4706
  }
3750
4707
  function writeConfig2(patch) {
3751
- mkdirSync7(TH_DIR2, { recursive: true });
4708
+ mkdirSync8(TH_DIR2, { recursive: true });
3752
4709
  const merged = { ...readConfig2(), ...patch };
3753
- writeFileSync7(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
4710
+ writeFileSync8(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
3754
4711
  }
3755
4712
  function backupSettings() {
3756
- if (!existsSync6(SETTINGS_PATH)) return null;
4713
+ if (!existsSync8(SETTINGS_PATH)) return null;
3757
4714
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3758
4715
  const backupPath = `${SETTINGS_PATH}.terminalhire-backup-${ts}`;
3759
4716
  copyFileSync(SETTINGS_PATH, backupPath);
@@ -3770,13 +4727,13 @@ function ask(question) {
3770
4727
  }
3771
4728
  function readTopMatches() {
3772
4729
  try {
3773
- const c = JSON.parse(readFileSync9(CACHE_FILE, "utf8"));
4730
+ const c = JSON.parse(readFileSync11(CACHE_FILE, "utf8"));
3774
4731
  return Array.isArray(c.topMatches) ? c.topMatches : [];
3775
4732
  } catch {
3776
4733
  return [];
3777
4734
  }
3778
4735
  }
3779
- async function run7() {
4736
+ async function run8() {
3780
4737
  const args3 = process.argv.slice(2).filter((a) => a !== "spinner");
3781
4738
  const has = (f) => args3.includes(f);
3782
4739
  const val = (f) => {
@@ -3913,21 +4870,21 @@ var init_jpi_spinner = __esm({
3913
4870
  "bin/jpi-spinner.js"() {
3914
4871
  "use strict";
3915
4872
  init_spinner();
3916
- TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join9(homedir7(), ".terminalhire");
3917
- CONFIG_FILE3 = join9(TH_DIR2, "config.json");
3918
- SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join9(homedir7(), ".claude", "settings.json");
3919
- CACHE_FILE = join9(TH_DIR2, "index-cache.json");
4873
+ TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join11(homedir9(), ".terminalhire");
4874
+ CONFIG_FILE3 = join11(TH_DIR2, "config.json");
4875
+ SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join11(homedir9(), ".claude", "settings.json");
4876
+ CACHE_FILE = join11(TH_DIR2, "index-cache.json");
3920
4877
  }
3921
4878
  });
3922
4879
 
3923
4880
  // bin/jpi-sync.js
3924
4881
  var jpi_sync_exports = {};
3925
4882
  __export(jpi_sync_exports, {
3926
- run: () => run8
4883
+ run: () => run9
3927
4884
  });
3928
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync7, rmSync as rmSync2 } from "fs";
3929
- import { join as join10 } from "path";
3930
- import { homedir as homedir8, hostname as osHostname } from "os";
4885
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, existsSync as existsSync9, rmSync as rmSync2 } from "fs";
4886
+ import { join as join12 } from "path";
4887
+ import { homedir as homedir10, hostname as osHostname } from "os";
3931
4888
  import { createInterface as createInterface5 } from "readline";
3932
4889
  import { spawn } from "child_process";
3933
4890
  function ask2(question) {
@@ -3941,14 +4898,14 @@ function ask2(question) {
3941
4898
  }
3942
4899
  function readMarker() {
3943
4900
  try {
3944
- return existsSync7(TIER1_MARKER) ? JSON.parse(readFileSync10(TIER1_MARKER, "utf8")) : null;
4901
+ return existsSync9(TIER1_MARKER) ? JSON.parse(readFileSync12(TIER1_MARKER, "utf8")) : null;
3945
4902
  } catch {
3946
4903
  return null;
3947
4904
  }
3948
4905
  }
3949
4906
  function writeMarker(marker) {
3950
- mkdirSync8(TH_DIR3, { recursive: true });
3951
- writeFileSync8(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
4907
+ mkdirSync9(TH_DIR3, { recursive: true });
4908
+ writeFileSync9(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
3952
4909
  }
3953
4910
  function clearMarker() {
3954
4911
  try {
@@ -4257,7 +5214,7 @@ async function runDelete() {
4257
5214
  clearMarker();
4258
5215
  console.log("\n Synced profile deleted and local marker cleared.\n");
4259
5216
  }
4260
- async function run8() {
5217
+ async function run9() {
4261
5218
  const args3 = process.argv.slice(2).filter((a) => a !== "sync");
4262
5219
  const has = (f) => args3.includes(f);
4263
5220
  if (has("--push") || has("--enable")) {
@@ -4287,8 +5244,8 @@ var TH_DIR3, TIER1_MARKER, API_URL3, SYNC_BASE, POLL_INTERVAL_MS, POLL_TIMEOUT_M
4287
5244
  var init_jpi_sync = __esm({
4288
5245
  "bin/jpi-sync.js"() {
4289
5246
  "use strict";
4290
- TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join10(homedir8(), ".terminalhire");
4291
- TIER1_MARKER = join10(TH_DIR3, "tier1.json");
5247
+ TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join12(homedir10(), ".terminalhire");
5248
+ TIER1_MARKER = join12(TH_DIR3, "tier1.json");
4292
5249
  API_URL3 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
4293
5250
  SYNC_BASE = "https://www.terminalhire.com";
4294
5251
  POLL_INTERVAL_MS = 2e3;
@@ -4300,14 +5257,14 @@ var init_jpi_sync = __esm({
4300
5257
  // bin/jpi-init.js
4301
5258
  var jpi_init_exports = {};
4302
5259
  __export(jpi_init_exports, {
4303
- run: () => run9
5260
+ run: () => run10
4304
5261
  });
4305
- import { existsSync as existsSync8 } from "fs";
4306
- import { join as join11, resolve } from "path";
5262
+ import { existsSync as existsSync10 } from "fs";
5263
+ import { join as join13, resolve } from "path";
4307
5264
  import { fileURLToPath as fileURLToPath3 } from "url";
4308
5265
  import { createInterface as createInterface6 } from "readline";
4309
5266
  import { spawnSync, spawn as spawn2 } from "child_process";
4310
- import { homedir as homedir9 } from "os";
5267
+ import { homedir as homedir11 } from "os";
4311
5268
  function ask3(question) {
4312
5269
  const rl = createInterface6({ input: process.stdin, output: process.stdout });
4313
5270
  return new Promise((resolve2) => {
@@ -4318,18 +5275,18 @@ function ask3(question) {
4318
5275
  });
4319
5276
  }
4320
5277
  function resolveScript(name) {
4321
- const distPath = resolve(join11(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
4322
- const legacyPath = resolve(join11(__dirname2, `${name}.js`));
4323
- return existsSync8(distPath) ? distPath : legacyPath;
5278
+ const distPath = resolve(join13(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
5279
+ const legacyPath = resolve(join13(__dirname2, `${name}.js`));
5280
+ return existsSync10(distPath) ? distPath : legacyPath;
4324
5281
  }
4325
5282
  function resolveInstallJs() {
4326
- const fromDist = resolve(join11(__dirname2, "..", "..", "install.js"));
4327
- const fromBin = resolve(join11(__dirname2, "..", "install.js"));
4328
- if (existsSync8(fromDist)) return fromDist;
4329
- if (existsSync8(fromBin)) return fromBin;
5283
+ const fromDist = resolve(join13(__dirname2, "..", "..", "install.js"));
5284
+ const fromBin = resolve(join13(__dirname2, "..", "install.js"));
5285
+ if (existsSync10(fromDist)) return fromDist;
5286
+ if (existsSync10(fromBin)) return fromBin;
4330
5287
  return fromBin;
4331
5288
  }
4332
- async function run9() {
5289
+ async function run10() {
4333
5290
  console.log("");
4334
5291
  console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
4335
5292
  console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
@@ -4432,13 +5389,13 @@ var init_jpi_init = __esm({
4432
5389
  // bin/jpi-refresh.js
4433
5390
  var jpi_refresh_exports = {};
4434
5391
  __export(jpi_refresh_exports, {
4435
- run: () => run10
5392
+ run: () => run11
4436
5393
  });
4437
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync9 } from "fs";
4438
- import { join as join12 } from "path";
4439
- import { homedir as homedir10 } from "os";
5394
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync11, mkdirSync as mkdirSync10 } from "fs";
5395
+ import { join as join14 } from "path";
5396
+ import { homedir as homedir12 } from "os";
4440
5397
  import { fileURLToPath as fileURLToPath4 } from "url";
4441
- async function run10() {
5398
+ async function run11() {
4442
5399
  try {
4443
5400
  let index;
4444
5401
  try {
@@ -4490,14 +5447,14 @@ async function run10() {
4490
5447
  }
4491
5448
  } catch {
4492
5449
  }
4493
- mkdirSync9(TERMINALHIRE_DIR6, { recursive: true });
5450
+ mkdirSync10(TERMINALHIRE_DIR8, { recursive: true });
4494
5451
  const cacheEntry = {
4495
5452
  ts: Date.now(),
4496
5453
  index,
4497
5454
  matchCount,
4498
5455
  topMatches
4499
5456
  };
4500
- writeFileSync9(INDEX_CACHE_FILE3, JSON.stringify(cacheEntry), "utf8");
5457
+ writeFileSync10(INDEX_CACHE_FILE4, JSON.stringify(cacheEntry), "utf8");
4501
5458
  try {
4502
5459
  const {
4503
5460
  readSpinnerConfig: readSpinnerConfig2,
@@ -4541,13 +5498,13 @@ async function run10() {
4541
5498
  process.exit(1);
4542
5499
  }
4543
5500
  }
4544
- var __dirname3, TERMINALHIRE_DIR6, INDEX_CACHE_FILE3, API_URL4;
5501
+ var __dirname3, TERMINALHIRE_DIR8, INDEX_CACHE_FILE4, API_URL4;
4545
5502
  var init_jpi_refresh = __esm({
4546
5503
  "bin/jpi-refresh.js"() {
4547
5504
  "use strict";
4548
5505
  __dirname3 = fileURLToPath4(new URL(".", import.meta.url));
4549
- TERMINALHIRE_DIR6 = join12(homedir10(), ".terminalhire");
4550
- INDEX_CACHE_FILE3 = join12(TERMINALHIRE_DIR6, "index-cache.json");
5506
+ TERMINALHIRE_DIR8 = join14(homedir12(), ".terminalhire");
5507
+ INDEX_CACHE_FILE4 = join14(TERMINALHIRE_DIR8, "index-cache.json");
4551
5508
  API_URL4 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
4552
5509
  }
4553
5510
  });
@@ -4555,16 +5512,16 @@ var init_jpi_refresh = __esm({
4555
5512
  // bin/jpi-save.js
4556
5513
  var jpi_save_exports = {};
4557
5514
  __export(jpi_save_exports, {
4558
- run: () => run11
5515
+ run: () => run12
4559
5516
  });
4560
- import { readFileSync as readFileSync12, existsSync as existsSync10 } from "fs";
4561
- import { join as join13 } from "path";
4562
- import { homedir as homedir11 } from "os";
5517
+ import { readFileSync as readFileSync14, existsSync as existsSync12 } from "fs";
5518
+ import { join as join15 } from "path";
5519
+ import { homedir as homedir13 } from "os";
4563
5520
  import { fileURLToPath as fileURLToPath5 } from "url";
4564
5521
  function findJobInCache(jobId) {
4565
5522
  try {
4566
- if (!existsSync10(INDEX_CACHE_FILE4)) return null;
4567
- const raw = readFileSync12(INDEX_CACHE_FILE4, "utf8");
5523
+ if (!existsSync12(INDEX_CACHE_FILE5)) return null;
5524
+ const raw = readFileSync14(INDEX_CACHE_FILE5, "utf8");
4568
5525
  const entry = JSON.parse(raw);
4569
5526
  const jobs = entry?.index?.jobs ?? [];
4570
5527
  return jobs.find((j) => j.id === jobId) ?? null;
@@ -4633,7 +5590,7 @@ async function cmdUnsave(jobId) {
4633
5590
  process.exit(1);
4634
5591
  }
4635
5592
  }
4636
- async function run11() {
5593
+ async function run12() {
4637
5594
  const verb = process.argv[2];
4638
5595
  const jobId = process.argv[3];
4639
5596
  try {
@@ -4652,31 +5609,31 @@ async function run11() {
4652
5609
  process.exit(1);
4653
5610
  }
4654
5611
  }
4655
- var __dirname4, TERMINALHIRE_DIR7, INDEX_CACHE_FILE4;
5612
+ var __dirname4, TERMINALHIRE_DIR9, INDEX_CACHE_FILE5;
4656
5613
  var init_jpi_save = __esm({
4657
5614
  "bin/jpi-save.js"() {
4658
5615
  "use strict";
4659
5616
  __dirname4 = fileURLToPath5(new URL(".", import.meta.url));
4660
- TERMINALHIRE_DIR7 = join13(homedir11(), ".terminalhire");
4661
- INDEX_CACHE_FILE4 = join13(TERMINALHIRE_DIR7, "index-cache.json");
5617
+ TERMINALHIRE_DIR9 = join15(homedir13(), ".terminalhire");
5618
+ INDEX_CACHE_FILE5 = join15(TERMINALHIRE_DIR9, "index-cache.json");
4662
5619
  }
4663
5620
  });
4664
5621
 
4665
5622
  // bin/jpi-dispatch.js
4666
5623
  import { fileURLToPath as fileURLToPath6 } from "url";
4667
- import { join as join14, dirname as dirname2 } from "path";
4668
- import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
5624
+ import { join as join16, dirname as dirname2 } from "path";
5625
+ import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
4669
5626
  import { createRequire } from "module";
4670
5627
  var __dirname5 = fileURLToPath6(new URL(".", import.meta.url));
4671
5628
  function readPackageVersion() {
4672
5629
  try {
4673
5630
  const candidates = [
4674
- join14(__dirname5, "..", "..", "package.json"),
4675
- join14(__dirname5, "..", "package.json")
5631
+ join16(__dirname5, "..", "..", "package.json"),
5632
+ join16(__dirname5, "..", "package.json")
4676
5633
  ];
4677
5634
  for (const p of candidates) {
4678
- if (existsSync11(p)) {
4679
- const pkg = JSON.parse(readFileSync13(p, "utf8"));
5635
+ if (existsSync13(p)) {
5636
+ const pkg = JSON.parse(readFileSync15(p, "utf8"));
4680
5637
  if (pkg.version) return pkg.version;
4681
5638
  }
4682
5639
  }
@@ -4687,7 +5644,7 @@ function readPackageVersion() {
4687
5644
  var firstArg = process.argv[2];
4688
5645
  if (!firstArg && !process.stdin.isTTY) {
4689
5646
  const { default: childProcess } = await import("child_process");
4690
- const nudgeScript = join14(__dirname5, "jpi.js");
5647
+ const nudgeScript = join16(__dirname5, "jpi.js");
4691
5648
  const child = childProcess.spawnSync(process.execPath, [nudgeScript], {
4692
5649
  stdio: ["inherit", "inherit", "inherit"]
4693
5650
  });
@@ -4706,6 +5663,9 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
4706
5663
  console.log(" terminalhire jobs --remote-only Filter to remote roles only");
4707
5664
  console.log(" terminalhire bounties Day-sized paid tasks you can knock out today");
4708
5665
  console.log(" terminalhire bounties --priced Only bounties with a known $ amount");
5666
+ console.log(" terminalhire claim record <id|issueUrl> Claim a bounty locally + print the executor brief");
5667
+ console.log(" terminalhire claim list [--active] List your claims + accepted-PR rate");
5668
+ console.log(" terminalhire claim status [<id>] Poll source PR merge state (updates the metric)");
4709
5669
  console.log(" terminalhire profile --show Display your encrypted local profile");
4710
5670
  console.log(" terminalhire profile --edit Set displayName, contactEmail, prefs");
4711
5671
  console.log(" terminalhire profile --delete Wipe profile and encryption key from disk");
@@ -4758,6 +5718,12 @@ if (firstArg === "bounties") {
4758
5718
  await mod.run();
4759
5719
  process.exit(0);
4760
5720
  }
5721
+ if (firstArg === "claim") {
5722
+ process.argv.splice(2, 1);
5723
+ const mod = await Promise.resolve().then(() => (init_jpi_claim(), jpi_claim_exports));
5724
+ await mod.run();
5725
+ process.exit(0);
5726
+ }
4761
5727
  if (firstArg === "profile") {
4762
5728
  const mod = await Promise.resolve().then(() => (init_jpi_profile(), jpi_profile_exports));
4763
5729
  await mod.run();