terminalhire 0.20.0 → 0.22.0

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.
@@ -45,9 +45,9 @@ var init_keytar = __esm({
45
45
  }
46
46
  });
47
47
 
48
- // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
48
+ // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/claim-frictionless/node_modules/keytar/build/Release/keytar.node
49
49
  var require_keytar = __commonJS({
50
- "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
50
+ "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/claim-frictionless/node_modules/keytar/build/Release/keytar.node"(exports, module) {
51
51
  "use strict";
52
52
  init_keytar();
53
53
  try {
@@ -99,6 +99,7 @@ var require_keytar2 = __commonJS({
99
99
  // src/claims.ts
100
100
  var claims_exports = {};
101
101
  __export(claims_exports, {
102
+ PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
102
103
  acceptedPRRate: () => acceptedPRRate,
103
104
  findClaim: () => findClaim,
104
105
  listClaims: () => listClaims,
@@ -199,12 +200,21 @@ function acceptedPRRate(claims = readClaims()) {
199
200
  const merged = claims.filter((c) => c.state === "merged").length;
200
201
  return { merged, total, rate: total === 0 ? 0 : merged / total };
201
202
  }
202
- var TERMINALHIRE_DIR2, CLAIMS_FILE, TERMINAL_STATES;
203
+ var TERMINALHIRE_DIR2, CLAIMS_FILE, PUSHED_CLAIM_FIELDS, TERMINAL_STATES;
203
204
  var init_claims = __esm({
204
205
  "src/claims.ts"() {
205
206
  "use strict";
206
- TERMINALHIRE_DIR2 = join2(homedir2(), ".terminalhire");
207
+ TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join2(homedir2(), ".terminalhire");
207
208
  CLAIMS_FILE = join2(TERMINALHIRE_DIR2, "claims.json");
209
+ PUSHED_CLAIM_FIELDS = [
210
+ "kind",
211
+ "repoFullName",
212
+ "state",
213
+ "prUrl",
214
+ "merged",
215
+ "claimedAt",
216
+ "updatedAt"
217
+ ];
208
218
  TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
209
219
  }
210
220
  });
@@ -278,7 +288,6 @@ var CLAIM_PUSH_TOKEN_FILE = join3(TERMINALHIRE_DIR3, "claim-push-token.enc");
278
288
  var CLAIM_SYNC_BASE = "https://terminalhire.com";
279
289
  var AUTO_CONSENT_VERSION = 2;
280
290
  var AUTO_PUSH_THROTTLE_MS = 24 * 60 * 60 * 1e3;
281
- var CLAIM_PUSH_FIELDS = ["kind", "repoFullName", "state", "prUrl", "merged", "claimedAt", "updatedAt"];
282
291
  async function writePushTokenEnc(rawToken) {
283
292
  mkdirSync3(TERMINALHIRE_DIR3, { recursive: true });
284
293
  const key = await loadKey();
@@ -348,7 +357,7 @@ async function runBackgroundClaimPush({ now = Date.now() } = {}) {
348
357
  if (!existsSync3(CLAIM_PUSH_AUTO_MARKER) || !existsSync3(CLAIM_PUSH_TOKEN_FILE)) return;
349
358
  const marker = readAutoMarker();
350
359
  if (!marker || !marker.autoConsentedAt) return;
351
- const { listClaims: listClaims2, toPushedClaim: toPushedClaim2 } = await Promise.resolve().then(() => (init_claims(), claims_exports));
360
+ const { listClaims: listClaims2, toPushedClaim: toPushedClaim2, PUSHED_CLAIM_FIELDS: PUSHED_CLAIM_FIELDS2 } = await Promise.resolve().then(() => (init_claims(), claims_exports));
352
361
  const pushed = listClaims2().map((c) => toPushedClaim2(c));
353
362
  const currentHash = computeSnapshotHash(pushed);
354
363
  const gate = backgroundPushGate({
@@ -363,15 +372,15 @@ async function runBackgroundClaimPush({ now = Date.now() } = {}) {
363
372
  if (!gate.push) return;
364
373
  const token = await readPushTokenEnc();
365
374
  if (!token) return;
366
- const consentToken = {
375
+ const consentReceipt = {
367
376
  consentedAt: marker.autoConsentedAt,
368
377
  version: AUTO_CONSENT_VERSION,
369
- fields: CLAIM_PUSH_FIELDS
378
+ fields: PUSHED_CLAIM_FIELDS2
370
379
  };
371
380
  const res = await fetch(`${CLAIM_SYNC_BASE}/api/claim-sync`, {
372
381
  method: "POST",
373
382
  headers: { "Content-Type": "application/json" },
374
- body: JSON.stringify({ consentToken, claims: pushed, pushToken: token }),
383
+ body: JSON.stringify({ consentToken: consentReceipt, claims: pushed, pushToken: token }),
375
384
  signal: AbortSignal.timeout(1e4)
376
385
  });
377
386
  if (!res.ok) return;
@@ -761,9 +761,9 @@ function ghHeaders(token) {
761
761
  if (token) headers["Authorization"] = `Bearer ${token}`;
762
762
  return headers;
763
763
  }
764
- async function ghFetch(path, token) {
764
+ async function ghFetch(path, token, signal) {
765
765
  const url = `https://api.github.com${path}`;
766
- const res = await fetch(url, { headers: ghHeaders(token) });
766
+ const res = await fetch(url, { headers: ghHeaders(token), signal });
767
767
  if (!res.ok) {
768
768
  throw new Error(`GitHub API ${path}: HTTP ${res.status} ${res.statusText}`);
769
769
  }
@@ -812,6 +812,7 @@ async function fetchGitHubProfile(login, token) {
812
812
  } catch {
813
813
  }
814
814
  return {
815
+ id: user.id,
815
816
  login: user.login,
816
817
  name: user.name ?? void 0,
817
818
  publicEmail: user.email ?? void 0,
@@ -888,8 +889,8 @@ async function fetchOwnedRepoTraction(login, token) {
888
889
  computedAt
889
890
  };
890
891
  }
891
- async function ghFetchRaw(path, token) {
892
- return fetch(`https://api.github.com${path}`, { headers: ghHeaders(token) });
892
+ async function ghFetchRaw(path, token, signal) {
893
+ return fetch(`https://api.github.com${path}`, { headers: ghHeaders(token), signal });
893
894
  }
894
895
  function parseRepoUrl(repoUrl) {
895
896
  const m = repoUrl.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
@@ -908,11 +909,12 @@ async function fetchOwnedOrgs(token) {
908
909
  return /* @__PURE__ */ new Set();
909
910
  }
910
911
  }
911
- async function repoContributorCount(owner, name, token) {
912
+ async function repoContributorCount(owner, name, token, signal) {
912
913
  try {
913
914
  const res = await ghFetchRaw(
914
915
  `/repos/${owner}/${name}/contributors?per_page=1&anon=false`,
915
- token
916
+ token,
917
+ signal
916
918
  );
917
919
  if (!res.ok) return void 0;
918
920
  const link = res.headers.get("link");
@@ -924,7 +926,7 @@ async function repoContributorCount(owner, name, token) {
924
926
  return void 0;
925
927
  }
926
928
  }
927
- async function fetchRepoMeta(owner, name, token, cache) {
929
+ async function fetchRepoMeta(owner, name, token, cache, stats) {
928
930
  const key = `${owner}/${name}`.toLowerCase();
929
931
  const cached = cache.get(key);
930
932
  if (cached !== void 0) return cached;
@@ -941,8 +943,10 @@ async function fetchRepoMeta(owner, name, token, cache) {
941
943
  topics: r.topics ?? [],
942
944
  contributors
943
945
  };
944
- } catch {
946
+ } catch (err) {
945
947
  meta = null;
948
+ const msg = err instanceof Error ? err.message : String(err);
949
+ if (stats && TRANSIENT_META_ERROR.test(msg)) stats.transient += 1;
946
950
  }
947
951
  cache.set(key, meta);
948
952
  return meta;
@@ -981,8 +985,10 @@ async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates
981
985
  return emptyCredential(/HTTP 403|HTTP 429|rate limit/i.test(msg) ? "rate-limited" : "failed");
982
986
  }
983
987
  const byDomain = {};
988
+ const distinctOrgSet = /* @__PURE__ */ new Set();
984
989
  let qualifyingTotal = 0;
985
990
  const qualifyingPRs = [];
991
+ const metaStats = { transient: 0 };
986
992
  for (const item of items) {
987
993
  const repo = parseRepoUrl(item.repository_url);
988
994
  if (!repo) continue;
@@ -990,13 +996,20 @@ async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates
990
996
  if (ownerLc === loginLc) continue;
991
997
  if (ownedOrgs.has(ownerLc)) continue;
992
998
  if (isTrivialPRTitle(item.title)) continue;
993
- const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache);
999
+ const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache, metaStats);
1000
+ if (metaStats.transient > 0) {
1001
+ console.warn(
1002
+ `[acceptance] ${login}: per-repo metadata transient failure (${metaStats.transient}) \u2014 degrading to 'rate-limited' rather than a fabricated count`
1003
+ );
1004
+ return emptyCredential("rate-limited");
1005
+ }
994
1006
  if (!meta) continue;
995
1007
  if (meta.private) continue;
996
1008
  if (meta.archived || meta.fork) continue;
997
1009
  if (meta.stars < gates.minStars) continue;
998
1010
  if (meta.contributors !== void 0 && meta.contributors < gates.minContributors) continue;
999
1011
  qualifyingTotal += 1;
1012
+ distinctOrgSet.add(ownerLc);
1000
1013
  const mergedAt = item.pull_request?.merged_at ?? item.closed_at ?? item.created_at;
1001
1014
  const rawDomains = [meta.language ?? "", ...meta.topics].filter(Boolean);
1002
1015
  const domainTags = [...new Set(normalize(rawDomains))];
@@ -1022,7 +1035,14 @@ async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates
1022
1035
  lastMergedAt: b.lastMergedAt
1023
1036
  };
1024
1037
  }
1025
- return { status: "ok", byDomain: finalDomains, qualifyingTotal, qualifyingPRs, computedAt };
1038
+ return {
1039
+ status: "ok",
1040
+ byDomain: finalDomains,
1041
+ qualifyingTotal,
1042
+ qualifyingPRs,
1043
+ distinctOrgs: distinctOrgSet.size,
1044
+ computedAt
1045
+ };
1026
1046
  }
1027
1047
  async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */ new Map()) {
1028
1048
  if (!token) return emptyCredential("no-token");
@@ -1036,6 +1056,61 @@ async function computeAcceptanceCredentialPublic(login, token, cache = /* @__PUR
1036
1056
  const gates = opts?.relaxGates ? { minStars: 0, minContributors: 0 } : void 0;
1037
1057
  return computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates);
1038
1058
  }
1059
+ async function fetchOpenExternalPRs(login, token, cache = /* @__PURE__ */ new Map(), gates = {
1060
+ minStars: MIN_STARS,
1061
+ minContributors: MIN_CONTRIBUTORS
1062
+ }) {
1063
+ if (!token) return [];
1064
+ const loginLc = login.toLowerCase();
1065
+ let ownedOrgs;
1066
+ try {
1067
+ ownedOrgs = await fetchPublicOrgs(login, token);
1068
+ } catch {
1069
+ return null;
1070
+ }
1071
+ let items;
1072
+ try {
1073
+ const q = encodeURIComponent(`type:pr is:open is:public author:${login} -user:${login} sort:updated`);
1074
+ const res = await ghFetch(
1075
+ `/search/issues?q=${q}&per_page=${OPEN_PR_PAGE}`,
1076
+ token
1077
+ );
1078
+ items = res.items ?? [];
1079
+ } catch (err) {
1080
+ const msg = err instanceof Error ? err.message : String(err);
1081
+ console.warn("[open-prs] search failed:", msg);
1082
+ return null;
1083
+ }
1084
+ const metaStats = { transient: 0 };
1085
+ const out = [];
1086
+ for (const item of items) {
1087
+ const repo = parseRepoUrl(item.repository_url);
1088
+ if (!repo) continue;
1089
+ const ownerLc = repo.owner.toLowerCase();
1090
+ if (ownerLc === loginLc) continue;
1091
+ if (ownedOrgs.has(ownerLc)) continue;
1092
+ if (isTrivialPRTitle(item.title)) continue;
1093
+ const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache, metaStats);
1094
+ if (metaStats.transient > 0) {
1095
+ console.warn(
1096
+ `[open-prs] ${login}: per-repo metadata transient failure (${metaStats.transient}) \u2014 returning null (keep prior strip)`
1097
+ );
1098
+ return null;
1099
+ }
1100
+ if (!meta) continue;
1101
+ if (meta.private) continue;
1102
+ if (meta.archived || meta.fork) continue;
1103
+ if (meta.stars < gates.minStars) continue;
1104
+ if (meta.contributors !== void 0 && meta.contributors < gates.minContributors) continue;
1105
+ out.push({
1106
+ title: item.title,
1107
+ url: item.html_url,
1108
+ repoFullName: `${repo.owner}/${repo.name}`,
1109
+ openedAt: item.created_at
1110
+ });
1111
+ }
1112
+ return out;
1113
+ }
1039
1114
  function acceptanceCountForDomains(cred, domains) {
1040
1115
  if (cred.status !== "ok") return 0;
1041
1116
  let max = 0;
@@ -1107,7 +1182,78 @@ function deriveResumeTrend(cred, repoRecency, now = Date.now()) {
1107
1182
  }
1108
1183
  return scored.sort((a, b) => b.weight - a.weight).slice(0, 12).map((s) => s.t);
1109
1184
  }
1110
- var TRACTION_TOP_N, CANDIDATE_PR_PAGE, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
1185
+ function parseGitHubRef(url) {
1186
+ const m = String(url ?? "").match(/github\.com\/([^/]+)\/([^/]+)\/(issues|pull)\/(\d+)/);
1187
+ if (!m) return null;
1188
+ return { owner: m[1], repo: m[2], number: parseInt(m[4], 10), kind: m[3] === "pull" ? "pull" : "issue" };
1189
+ }
1190
+ async function ghGraphQL(query, variables, token, signal) {
1191
+ const res = await fetch("https://api.github.com/graphql", {
1192
+ method: "POST",
1193
+ headers: { ...ghHeaders(token), "Content-Type": "application/json" },
1194
+ body: JSON.stringify({ query, variables }),
1195
+ signal
1196
+ });
1197
+ if (!res.ok) throw new Error(`GitHub GraphQL: HTTP ${res.status}`);
1198
+ const json = await res.json();
1199
+ if (json.errors?.length) throw new Error("GitHub GraphQL errors: " + JSON.stringify(json.errors));
1200
+ return json;
1201
+ }
1202
+ async function resolveClosingIssues(owner, name, number, body, token, signal) {
1203
+ if (token) {
1204
+ try {
1205
+ const q = `query($o:String!,$n:String!,$p:Int!){repository(owner:$o,name:$n){pullRequest(number:$p){closingIssuesReferences(first:20){nodes{number}}}}}`;
1206
+ const r = await ghGraphQL(q, { o: owner, n: name, p: number }, token, signal);
1207
+ const nodes = r.data?.repository?.pullRequest?.closingIssuesReferences?.nodes ?? [];
1208
+ return { closesIssues: nodes.map((x) => x.number), linkageSource: "graphql" };
1209
+ } catch {
1210
+ }
1211
+ }
1212
+ const nums = /* @__PURE__ */ new Set();
1213
+ const re = /\b(?:clos(?:e|es|ed)|fix(?:es|ed)?|resolv(?:e|es|ed))\s+#(\d+)/gi;
1214
+ let m;
1215
+ while ((m = re.exec(body)) !== null) nums.add(parseInt(m[1], 10));
1216
+ return { closesIssues: [...nums], linkageSource: nums.size ? "body-keyword" : "none" };
1217
+ }
1218
+ async function fetchPRScoringFacts(prUrl, token, signal) {
1219
+ const ref = parseGitHubRef(prUrl);
1220
+ if (!ref || ref.kind !== "pull") return null;
1221
+ const { owner, repo, number } = ref;
1222
+ const sig = signal ?? AbortSignal.timeout(1e4);
1223
+ let pr;
1224
+ try {
1225
+ pr = await ghFetch(`/repos/${owner}/${repo}/pulls/${number}`, token, sig);
1226
+ } catch {
1227
+ return null;
1228
+ }
1229
+ let repoMeta = null;
1230
+ try {
1231
+ repoMeta = await ghFetch(`/repos/${owner}/${repo}`, token, sig);
1232
+ } catch {
1233
+ }
1234
+ const contributors = await repoContributorCount(owner, repo, token, sig);
1235
+ const { closesIssues, linkageSource } = await resolveClosingIssues(owner, repo, number, pr.body ?? "", token, sig);
1236
+ return {
1237
+ repo: `${owner}/${repo}`,
1238
+ prNumber: number,
1239
+ prUrl: pr.html_url,
1240
+ merged: pr.merged === true,
1241
+ mergedAt: pr.merged_at ?? null,
1242
+ authorId: pr.user?.id ?? null,
1243
+ authorLogin: pr.user?.login ?? null,
1244
+ mergedById: pr.merged_by?.id ?? null,
1245
+ mergedByLogin: pr.merged_by?.login ?? null,
1246
+ closesIssues,
1247
+ linkageSource,
1248
+ repoStars: repoMeta?.stargazers_count ?? null,
1249
+ repoContributors: contributors ?? null,
1250
+ repoArchived: !!repoMeta?.archived,
1251
+ repoFork: !!repoMeta?.fork,
1252
+ repoPrivate: !!repoMeta?.private,
1253
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
1254
+ };
1255
+ }
1256
+ var TRACTION_TOP_N, CANDIDATE_PR_PAGE, OPEN_PR_PAGE, TRANSIENT_META_ERROR, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
1111
1257
  var init_github = __esm({
1112
1258
  "../../packages/core/src/github.ts"() {
1113
1259
  "use strict";
@@ -1115,6 +1261,8 @@ var init_github = __esm({
1115
1261
  init_contribution_gate();
1116
1262
  TRACTION_TOP_N = 6;
1117
1263
  CANDIDATE_PR_PAGE = 50;
1264
+ OPEN_PR_PAGE = 20;
1265
+ TRANSIENT_META_ERROR = /HTTP 403|HTTP 429|rate limit|HTTP 5\d\d|timeout|network|fetch failed/i;
1118
1266
  RESUME_DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
1119
1267
  RESUME_MIN_SCORE = 0.05;
1120
1268
  }
@@ -2792,6 +2940,20 @@ var init_contribution_classify = __esm({
2792
2940
  });
2793
2941
 
2794
2942
  // ../../packages/core/src/feeds/contributions.ts
2943
+ function readReqGapMs() {
2944
+ const raw = process.env["CONTRIB_REQ_GAP_MS"];
2945
+ if (raw == null) return DEFAULT_REQ_GAP_MS;
2946
+ const n = Number.parseInt(raw, 10);
2947
+ if (Number.isNaN(n)) return DEFAULT_REQ_GAP_MS;
2948
+ return Math.min(Math.max(n, 0), 1e3);
2949
+ }
2950
+ function readBuildBudgetMs() {
2951
+ const raw = process.env["CONTRIB_BUILD_BUDGET_MS"];
2952
+ if (raw == null) return DEFAULT_BUILD_BUDGET_MS;
2953
+ const n = Number.parseInt(raw, 10);
2954
+ if (Number.isNaN(n)) return DEFAULT_BUILD_BUDGET_MS;
2955
+ return Math.min(Math.max(n, MIN_BUILD_BUDGET_MS), MAX_BUILD_BUDGET_MS);
2956
+ }
2795
2957
  function authHeaders2() {
2796
2958
  const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
2797
2959
  const h = {
@@ -2812,10 +2974,55 @@ function repoFullNameFromApiUrl2(url) {
2812
2974
  const m = url.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
2813
2975
  return m ? `${m[1]}/${m[2]}` : null;
2814
2976
  }
2815
- function makeClient(fetchImpl) {
2977
+ function makeClient(fetchImpl, cfg) {
2978
+ const startedAt = cfg.now();
2979
+ let lastRequestAt = 0;
2980
+ let pacedMs = 0;
2981
+ let secondaryHits = 0;
2982
+ let aborted = false;
2983
+ let secondaryAborted = false;
2984
+ let budgetAborted = false;
2985
+ let coreHealthyAtStart = false;
2986
+ async function noteAndMaybeBackOff(res) {
2987
+ if (res.status !== 403) return;
2988
+ const remaining = res.headers.get("x-ratelimit-remaining");
2989
+ const retryAfter = res.headers.get("retry-after");
2990
+ const positiveSecondary = retryAfter != null || remaining != null && remaining !== "0";
2991
+ const isSecondary = positiveSecondary || coreHealthyAtStart;
2992
+ if (!isSecondary) return;
2993
+ secondaryHits++;
2994
+ if (secondaryHits >= 2) {
2995
+ aborted = true;
2996
+ secondaryAborted = true;
2997
+ return;
2998
+ }
2999
+ if (cfg.paceEnabled) {
3000
+ const trimmed = (retryAfter ?? "").trim();
3001
+ const parsed = trimmed.length === 0 ? Number.NaN : Number(trimmed);
3002
+ const sec = Number.isNaN(parsed) ? SECONDARY_BACKOFF_CAP_S : Math.min(Math.max(parsed, 0), SECONDARY_BACKOFF_CAP_S);
3003
+ const remaining2 = Math.max(0, cfg.budgetMs - (cfg.now() - startedAt));
3004
+ await cfg.sleep(Math.min(sec * 1e3, remaining2));
3005
+ }
3006
+ }
2816
3007
  async function raw(path) {
3008
+ if (aborted) return null;
3009
+ if (cfg.now() - startedAt > cfg.budgetMs) {
3010
+ aborted = true;
3011
+ budgetAborted = true;
3012
+ return null;
3013
+ }
3014
+ if (cfg.paceEnabled && cfg.gapMs > 0) {
3015
+ const wait = cfg.gapMs - (cfg.now() - lastRequestAt);
3016
+ if (wait > 0) {
3017
+ await cfg.sleep(wait);
3018
+ pacedMs += wait;
3019
+ }
3020
+ lastRequestAt = cfg.now();
3021
+ }
2817
3022
  try {
2818
- return await fetchImpl(`${GITHUB_API2}${path}`, { headers: authHeaders2() });
3023
+ const res = await fetchImpl(`${GITHUB_API2}${path}`, { headers: authHeaders2() });
3024
+ await noteAndMaybeBackOff(res);
3025
+ return res;
2819
3026
  } catch {
2820
3027
  return null;
2821
3028
  }
@@ -2831,7 +3038,42 @@ function makeClient(fetchImpl) {
2831
3038
  return null;
2832
3039
  }
2833
3040
  }
2834
- return { raw, json };
3041
+ async function probe(path) {
3042
+ const bound = cfg.probeTimeoutMs;
3043
+ let timer;
3044
+ const fetchP = fetchImpl(`${GITHUB_API2}${path}`, {
3045
+ headers: authHeaders2()
3046
+ }).then(
3047
+ (r) => r,
3048
+ () => null
3049
+ );
3050
+ try {
3051
+ const res = bound == null ? await fetchP : await Promise.race([
3052
+ fetchP,
3053
+ new Promise((resolve) => {
3054
+ timer = setTimeout(() => resolve(null), bound);
3055
+ })
3056
+ ]);
3057
+ if (!res || !res.ok) return null;
3058
+ return await res.json();
3059
+ } catch {
3060
+ return null;
3061
+ } finally {
3062
+ if (timer) clearTimeout(timer);
3063
+ }
3064
+ }
3065
+ function setSecondaryHint(coreHealthy) {
3066
+ coreHealthyAtStart = coreHealthy;
3067
+ }
3068
+ function getStats() {
3069
+ return {
3070
+ pacedMs,
3071
+ secondaryAborted: secondaryAborted ? 1 : 0,
3072
+ budgetAborted: budgetAborted ? 1 : 0,
3073
+ elapsedMs: cfg.now() - startedAt
3074
+ };
3075
+ }
3076
+ return { raw, json, probe, setSecondaryHint, getStats };
2835
3077
  }
2836
3078
  async function contributorCount(client, fullName) {
2837
3079
  const res = await client.raw(`/repos/${fullName}/contributors?per_page=1&anon=false`);
@@ -2860,6 +3102,10 @@ ${pr.body ?? ""}`.matchAll(/#(\d+)\b/g)) {
2860
3102
  }
2861
3103
  return refs;
2862
3104
  }
3105
+ async function fetchRateLimit(client) {
3106
+ const r = await client.probe("/rate_limit");
3107
+ return r?.resources ?? null;
3108
+ }
2863
3109
  async function searchContribIssues(client, queries) {
2864
3110
  const byUrl = /* @__PURE__ */ new Map();
2865
3111
  for (const q of queries) {
@@ -2876,8 +3122,21 @@ async function searchContribIssues(client, queries) {
2876
3122
  );
2877
3123
  }
2878
3124
  async function aggregateContributions(opts = {}) {
2879
- const client = makeClient(opts.fetchImpl ?? fetchWithTimeout);
3125
+ const paceEnabled = opts.paceEnabled ?? !opts.fetchImpl;
3126
+ const client = makeClient(opts.fetchImpl ?? fetchWithTimeout, {
3127
+ paceEnabled,
3128
+ gapMs: readReqGapMs(),
3129
+ budgetMs: readBuildBudgetMs(),
3130
+ sleep: opts.sleepImpl ?? realSleep,
3131
+ now: opts.nowImpl ?? Date.now,
3132
+ // Bound the unguarded probe on the REAL network only; injected-fetch tests get
3133
+ // null so the probe stays deterministic (and its shared spy sleeper untouched).
3134
+ probeTimeoutMs: opts.fetchImpl ? null : PROBE_TIMEOUT_MS
3135
+ });
2880
3136
  const queries = opts.queries ?? CONTRIB_SEARCH_QUERIES;
3137
+ const startRl = await fetchRateLimit(client);
3138
+ const coreHealthyAtStart = (startRl?.core?.remaining ?? 0) >= 500;
3139
+ client.setSecondaryHint(coreHealthyAtStart);
2881
3140
  const issues = (await searchContribIssues(client, queries)).slice(0, MAX_CONTRIB_ISSUES_SCANNED);
2882
3141
  const repoCache = /* @__PURE__ */ new Map();
2883
3142
  const contribCache = /* @__PURE__ */ new Map();
@@ -2904,6 +3163,9 @@ async function aggregateContributions(opts = {}) {
2904
3163
  const jobs = [];
2905
3164
  const seen = /* @__PURE__ */ new Set();
2906
3165
  const perRepo = /* @__PURE__ */ new Map();
3166
+ let metaNull = 0;
3167
+ let contribUndefined = 0;
3168
+ let prRefsNull = 0;
2907
3169
  for (const issue of issues) {
2908
3170
  if (jobs.length >= MAX_CONTRIB_ITEMS) break;
2909
3171
  const fullName = repoFullNameFromApiUrl2(issue.repository_url);
@@ -2914,9 +3176,13 @@ async function aggregateContributions(opts = {}) {
2914
3176
  if (isAssigned(issue)) continue;
2915
3177
  if ((perRepo.get(fullName) ?? 0) >= MAX_BOUNTIES_PER_DISCOVERED_REPO) continue;
2916
3178
  const repo = await repoMeta(fullName);
2917
- if (!repo) continue;
3179
+ if (!repo) {
3180
+ metaNull++;
3181
+ continue;
3182
+ }
2918
3183
  const title = decodeEntities(issue.title).trim();
2919
3184
  const contributors = await repoContribCount(fullName);
3185
+ if (contributors === void 0) contribUndefined++;
2920
3186
  if (!passesContributionGate({
2921
3187
  fullName,
2922
3188
  stars: repo.stargazers_count,
@@ -2932,6 +3198,7 @@ async function aggregateContributions(opts = {}) {
2932
3198
  const labels = labelNames2(issue.labels);
2933
3199
  if (looksLikeContentTask({ title, body, labels })) continue;
2934
3200
  const prRefs = await repoPRRefs(fullName);
3201
+ if (prRefs === null) prRefsNull++;
2935
3202
  if (prRefs && prRefs.has(issue.number)) continue;
2936
3203
  const openPRsAtDiscovery = prRefs ? 0 : void 0;
2937
3204
  const tags = normalize(
@@ -2971,9 +3238,19 @@ async function aggregateContributions(opts = {}) {
2971
3238
  raw: issue
2972
3239
  });
2973
3240
  }
3241
+ if (!opts.fetchImpl) {
3242
+ const rl = await fetchRateLimit(client);
3243
+ const core = rl?.core ? `${rl.core.remaining}/${rl.core.limit}` : "n/a";
3244
+ const search = rl?.search ? `${rl.search.remaining}/${rl.search.limit}` : "n/a";
3245
+ const noToken = !(process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"]);
3246
+ const { pacedMs, secondaryAborted, budgetAborted, elapsedMs } = client.getStats();
3247
+ console.info(
3248
+ `[contribute] build metrics \u2014 scanned=${issues.length} reposDistinct=${repoCache.size} emitted=${jobs.length} metaNull=${metaNull} contribUndefined=${contribUndefined} prRefsNull=${prRefsNull} paced=${pacedMs} secondaryAborted=${secondaryAborted} budgetAborted=${budgetAborted} core=${core} search=${search} elapsed=${elapsedMs}` + (noToken ? " (NO TOKEN \u2192 60/hr)" : "")
3249
+ );
3250
+ }
2974
3251
  return jobs;
2975
3252
  }
2976
- var GITHUB_API2, CONTRIB_SEARCH_QUERIES, SEARCH_PER_PAGE2, MAX_CONTRIB_ITEMS, MAX_CONTRIB_ISSUES_SCANNED;
3253
+ var GITHUB_API2, DEFAULT_REQ_GAP_MS, SECONDARY_BACKOFF_CAP_S, DEFAULT_BUILD_BUDGET_MS, MIN_BUILD_BUDGET_MS, MAX_BUILD_BUDGET_MS, PROBE_TIMEOUT_MS, realSleep, CONTRIB_LABEL_QUERIES, CONTRIB_LANGUAGE_QUERIES, CONTRIB_SEARCH_QUERIES, SEARCH_PER_PAGE2, MAX_CONTRIB_ITEMS, MAX_CONTRIB_ISSUES_SCANNED;
2977
3254
  var init_contributions = __esm({
2978
3255
  "../../packages/core/src/feeds/contributions.ts"() {
2979
3256
  "use strict";
@@ -2985,15 +3262,32 @@ var init_contributions = __esm({
2985
3262
  init_github_bounties();
2986
3263
  init_http();
2987
3264
  GITHUB_API2 = "https://api.github.com";
2988
- CONTRIB_SEARCH_QUERIES = [
3265
+ DEFAULT_REQ_GAP_MS = 75;
3266
+ SECONDARY_BACKOFF_CAP_S = 30;
3267
+ DEFAULT_BUILD_BUDGET_MS = 9e4;
3268
+ MIN_BUILD_BUDGET_MS = 1e4;
3269
+ MAX_BUILD_BUDGET_MS = 9e4;
3270
+ PROBE_TIMEOUT_MS = 3e3;
3271
+ realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
3272
+ CONTRIB_LABEL_QUERIES = [
2989
3273
  'label:"good first issue" type:issue state:open',
2990
- 'label:"help wanted" type:issue state:open',
2991
3274
  'label:"good-first-issue" type:issue state:open',
2992
- 'label:"help-wanted" type:issue state:open'
3275
+ 'label:"help wanted" type:issue state:open',
3276
+ 'label:"help-wanted" type:issue state:open',
3277
+ 'label:"up-for-grabs" type:issue state:open'
3278
+ ];
3279
+ CONTRIB_LANGUAGE_QUERIES = [
3280
+ ...["rust", "go", "python", "c++", "ruby"].map(
3281
+ (lang) => `label:"help wanted" language:${lang} type:issue state:open`
3282
+ ),
3283
+ ...["rust", "go"].map(
3284
+ (lang) => `label:"good first issue" language:${lang} type:issue state:open`
3285
+ )
2993
3286
  ];
3287
+ CONTRIB_SEARCH_QUERIES = [...CONTRIB_LABEL_QUERIES, ...CONTRIB_LANGUAGE_QUERIES];
2994
3288
  SEARCH_PER_PAGE2 = 100;
2995
3289
  MAX_CONTRIB_ITEMS = 150;
2996
- MAX_CONTRIB_ISSUES_SCANNED = 300;
3290
+ MAX_CONTRIB_ISSUES_SCANNED = 600;
2997
3291
  }
2998
3292
  });
2999
3293
 
@@ -3091,6 +3385,36 @@ var init_indexer = __esm({
3091
3385
  }
3092
3386
  });
3093
3387
 
3388
+ // ../../packages/core/src/credit.ts
3389
+ function verifyClaimCredit(claim, facts) {
3390
+ const reasons = [];
3391
+ const fail = (code, message) => reasons.push({ code, message });
3392
+ const norm = (r) => r.trim().toLowerCase();
3393
+ if (norm(facts.repo) !== norm(claim.repo))
3394
+ fail("repo-mismatch", `PR is in ${facts.repo}, claim is against ${claim.repo}`);
3395
+ if (!facts.merged) fail("not-merged", `PR #${facts.prNumber} is not merged`);
3396
+ if (facts.authorId == null || facts.authorId !== claim.claimantId)
3397
+ fail("author-mismatch", `PR author id ${facts.authorId} !== claimant id ${claim.claimantId}`);
3398
+ if (facts.merged && facts.mergedById != null && facts.authorId != null && facts.mergedById === facts.authorId)
3399
+ fail("self-merged", `PR was merged by its own author (id ${facts.authorId})`);
3400
+ if (claim.claimedIssueNumber != null) {
3401
+ if (facts.closesIssues.length === 0)
3402
+ fail("issue-linkage-missing", `PR closes no issue; claim names #${claim.claimedIssueNumber}`);
3403
+ else if (!facts.closesIssues.includes(claim.claimedIssueNumber))
3404
+ fail(
3405
+ "issue-linkage-mismatch",
3406
+ `PR closes ${facts.closesIssues.map((n) => "#" + n).join(", ")}; claim names #${claim.claimedIssueNumber}`
3407
+ );
3408
+ }
3409
+ if (reasons.length === 0) reasons.push({ code: "ok", message: "all credit predicates hold" });
3410
+ return { ok: reasons.every((r) => r.code === "ok"), reasons };
3411
+ }
3412
+ var init_credit = __esm({
3413
+ "../../packages/core/src/credit.ts"() {
3414
+ "use strict";
3415
+ }
3416
+ });
3417
+
3094
3418
  // ../../packages/core/src/intro.ts
3095
3419
  function buildIntroPayload(input) {
3096
3420
  const payload = {
@@ -6436,13 +6760,15 @@ function deriveLegibleProfile(credential, recency, traction, seniorityBand) {
6436
6760
  daysAgo = Math.max(0, Math.round(ageDays2));
6437
6761
  recencyBadge = { lastMergedAt: mostRecent, state: ageDays2 <= thresholdDays ? "live" : "dormant" };
6438
6762
  }
6439
- const orgCount = Object.values(domains).reduce((m, d) => Math.max(m, d.distinctOrgs), 0);
6763
+ const exactOrgCount = typeof credential.distinctOrgs === "number" && credential.distinctOrgs > 0;
6764
+ const orgCount = exactOrgCount ? credential.distinctOrgs : Object.values(domains).reduce((m, d) => Math.max(m, d.distinctOrgs), 0);
6440
6765
  let proofSentence;
6441
6766
  if (!ok) {
6442
6767
  proofSentence = "Contribution credential unavailable \u2014 could not verify.";
6443
6768
  } else {
6444
6769
  const prs = credential.qualifyingTotal;
6445
- let s = `${prs} substantive PR${prs === 1 ? "" : "s"} merged into at least ${orgCount} external org${orgCount === 1 ? "" : "s"} (\u2265${MIN_STARS}\u2605, \u2265${MIN_CONTRIBUTORS} contributors)`;
6770
+ const orgPhrase = exactOrgCount ? `${orgCount}` : `at least ${orgCount}`;
6771
+ let s = `${prs} substantive PR${prs === 1 ? "" : "s"} merged into ${orgPhrase} external org${orgCount === 1 ? "" : "s"} (\u2265${MIN_STARS}\u2605, \u2265${MIN_CONTRIBUTORS} contributors)`;
6446
6772
  if (daysAgo !== void 0) s += ` \u2014 most recent ${daysAgo}d ago`;
6447
6773
  proofSentence = `${s}.`;
6448
6774
  }
@@ -6680,7 +7006,9 @@ __export(src_exports, {
6680
7006
  expandWeighted: () => expandWeighted,
6681
7007
  extractSkillTags: () => extractSkillTags,
6682
7008
  fetchGitHubProfile: () => fetchGitHubProfile,
7009
+ fetchOpenExternalPRs: () => fetchOpenExternalPRs,
6683
7010
  fetchOwnedRepoTraction: () => fetchOwnedRepoTraction,
7011
+ fetchPRScoringFacts: () => fetchPRScoringFacts,
6684
7012
  fetchRepoRecency: () => fetchRepoRecency,
6685
7013
  flattenTiers: () => flattenTiers,
6686
7014
  funnelCounts: () => funnelCounts,
@@ -6711,6 +7039,7 @@ __export(src_exports, {
6711
7039
  opire: () => opire,
6712
7040
  opportunityShortToken: () => opportunityShortToken,
6713
7041
  pageMatches: () => pageMatches,
7042
+ parseGitHubRef: () => parseGitHubRef,
6714
7043
  passesContributionGate: () => passesContributionGate,
6715
7044
  passesMaturityGate: () => passesMaturityGate,
6716
7045
  personCardToJob: () => personCardToJob,
@@ -6727,6 +7056,7 @@ __export(src_exports, {
6727
7056
  validateGraph: () => validateGraph,
6728
7057
  validateIntroPayload: () => validateIntroPayload,
6729
7058
  validateTargetContact: () => validateTargetContact,
7059
+ verifyClaimCredit: () => verifyClaimCredit,
6730
7060
  workable: () => workable,
6731
7061
  wwr: () => wwr
6732
7062
  });
@@ -6741,6 +7071,7 @@ var init_src = __esm({
6741
7071
  init_indexer();
6742
7072
  init_partners();
6743
7073
  init_github();
7074
+ init_credit();
6744
7075
  init_intro();
6745
7076
  init_directoryThreshold();
6746
7077
  init_chatCrypto();
@@ -6759,9 +7090,9 @@ var init_keytar = __esm({
6759
7090
  }
6760
7091
  });
6761
7092
 
6762
- // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
7093
+ // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/claim-frictionless/node_modules/keytar/build/Release/keytar.node
6763
7094
  var require_keytar = __commonJS({
6764
- "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
7095
+ "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/claim-frictionless/node_modules/keytar/build/Release/keytar.node"(exports, module) {
6765
7096
  "use strict";
6766
7097
  init_keytar();
6767
7098
  try {