terminalhire 0.19.2 → 0.21.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.
@@ -0,0 +1,409 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
+ }) : x)(function(x) {
10
+ if (typeof require !== "undefined") return require.apply(this, arguments);
11
+ throw Error('Dynamic require of "' + x + '" is not supported');
12
+ });
13
+ var __esm = (fn, res) => function __init() {
14
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
15
+ };
16
+ var __commonJS = (cb, mod) => function __require2() {
17
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, { get: all[name], enumerable: true });
22
+ };
23
+ var __copyProps = (to, from, except, desc) => {
24
+ if (from && typeof from === "object" || typeof from === "function") {
25
+ for (let key of __getOwnPropNames(from))
26
+ if (!__hasOwnProp.call(to, key) && key !== except)
27
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
28
+ }
29
+ return to;
30
+ };
31
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
32
+ // If the importer is in node compatibility mode or this is not an ESM
33
+ // file that has been converted to a CommonJS file using a Babel-
34
+ // compatible transform (i.e. "__esModule" has not been set), then set
35
+ // "default" to the CommonJS "module.exports" for node compatibility.
36
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
37
+ mod
38
+ ));
39
+
40
+ // ../../node_modules/keytar/build/Release/keytar.node
41
+ var keytar_default;
42
+ var init_keytar = __esm({
43
+ "../../node_modules/keytar/build/Release/keytar.node"() {
44
+ keytar_default = "../keytar-KOAAH267.node";
45
+ }
46
+ });
47
+
48
+ // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
49
+ var require_keytar = __commonJS({
50
+ "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
51
+ "use strict";
52
+ init_keytar();
53
+ try {
54
+ module.exports = __require(keytar_default);
55
+ } catch {
56
+ }
57
+ }
58
+ });
59
+
60
+ // ../../node_modules/keytar/lib/keytar.js
61
+ var require_keytar2 = __commonJS({
62
+ "../../node_modules/keytar/lib/keytar.js"(exports, module) {
63
+ "use strict";
64
+ var keytar = require_keytar();
65
+ function checkRequired(val, name) {
66
+ if (!val || val.length <= 0) {
67
+ throw new Error(name + " is required.");
68
+ }
69
+ }
70
+ module.exports = {
71
+ getPassword: function(service, account) {
72
+ checkRequired(service, "Service");
73
+ checkRequired(account, "Account");
74
+ return keytar.getPassword(service, account);
75
+ },
76
+ setPassword: function(service, account, password) {
77
+ checkRequired(service, "Service");
78
+ checkRequired(account, "Account");
79
+ checkRequired(password, "Password");
80
+ return keytar.setPassword(service, account, password);
81
+ },
82
+ deletePassword: function(service, account) {
83
+ checkRequired(service, "Service");
84
+ checkRequired(account, "Account");
85
+ return keytar.deletePassword(service, account);
86
+ },
87
+ findPassword: function(service) {
88
+ checkRequired(service, "Service");
89
+ return keytar.findPassword(service);
90
+ },
91
+ findCredentials: function(service) {
92
+ checkRequired(service, "Service");
93
+ return keytar.findCredentials(service);
94
+ }
95
+ };
96
+ }
97
+ });
98
+
99
+ // src/claims.ts
100
+ var claims_exports = {};
101
+ __export(claims_exports, {
102
+ PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
103
+ acceptedPRRate: () => acceptedPRRate,
104
+ findClaim: () => findClaim,
105
+ listClaims: () => listClaims,
106
+ readClaims: () => readClaims,
107
+ recordClaim: () => recordClaim,
108
+ removeClaim: () => removeClaim,
109
+ toPushedClaim: () => toPushedClaim,
110
+ updateClaim: () => updateClaim
111
+ });
112
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, renameSync, existsSync as existsSync2 } from "fs";
113
+ import { join as join2 } from "path";
114
+ import { homedir as homedir2 } from "os";
115
+ function toPushedClaim(claim) {
116
+ return {
117
+ kind: claim.kind,
118
+ repoFullName: claim.repoFullName,
119
+ state: claim.state,
120
+ prUrl: claim.prUrl,
121
+ merged: claim.state === "merged",
122
+ claimedAt: claim.claimedAt,
123
+ updatedAt: claim.updatedAt
124
+ };
125
+ }
126
+ function nowISO() {
127
+ return (/* @__PURE__ */ new Date()).toISOString();
128
+ }
129
+ function normalizeClaim(c) {
130
+ return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
131
+ }
132
+ function readClaims() {
133
+ try {
134
+ if (!existsSync2(CLAIMS_FILE)) return [];
135
+ const data = JSON.parse(readFileSync2(CLAIMS_FILE, "utf8"));
136
+ const claims = Array.isArray(data?.claims) ? data.claims : [];
137
+ return claims.map(normalizeClaim);
138
+ } catch {
139
+ return [];
140
+ }
141
+ }
142
+ function writeClaims(claims) {
143
+ mkdirSync2(TERMINALHIRE_DIR2, { recursive: true });
144
+ const tmp = `${CLAIMS_FILE}.tmp`;
145
+ const payload = { claims };
146
+ writeFileSync2(tmp, JSON.stringify(payload, null, 2), "utf8");
147
+ renameSync(tmp, CLAIMS_FILE);
148
+ }
149
+ function findClaim(id) {
150
+ return readClaims().find((c) => c.id === id) ?? null;
151
+ }
152
+ function listClaims(opts = {}) {
153
+ const claims = readClaims();
154
+ if (!opts.active) return claims;
155
+ return claims.filter((c) => !TERMINAL_STATES.has(c.state));
156
+ }
157
+ function recordClaim(rec) {
158
+ const claims = readClaims();
159
+ if (claims.some((c) => c.id === rec.id)) {
160
+ throw new Error(
161
+ `claim already exists for '${rec.id}' \u2014 run 'terminalhire claim status ${rec.id}' or 'terminalhire claim release ${rec.id}'`
162
+ );
163
+ }
164
+ const ts = nowISO();
165
+ const claim = {
166
+ ...rec,
167
+ // Defensive default (mirrors normalizeClaim's `kind ?? 'bounty'` pattern):
168
+ // a caller written before `policy` existed, or a plain-JS caller that skips
169
+ // it, still produces a valid record instead of `policy: undefined`.
170
+ policy: rec.policy ?? null,
171
+ state: "claimed",
172
+ worktreePath: null,
173
+ branch: null,
174
+ prUrl: null,
175
+ review: null,
176
+ claimedAt: ts,
177
+ updatedAt: ts
178
+ };
179
+ claims.push(claim);
180
+ writeClaims(claims);
181
+ return claim;
182
+ }
183
+ function updateClaim(id, patch) {
184
+ const claims = readClaims();
185
+ const idx = claims.findIndex((c) => c.id === id);
186
+ if (idx === -1) return null;
187
+ claims[idx] = { ...claims[idx], ...patch, updatedAt: nowISO() };
188
+ writeClaims(claims);
189
+ return claims[idx];
190
+ }
191
+ function removeClaim(id) {
192
+ const claims = readClaims();
193
+ const next = claims.filter((c) => c.id !== id);
194
+ if (next.length === claims.length) return false;
195
+ writeClaims(next);
196
+ return true;
197
+ }
198
+ function acceptedPRRate(claims = readClaims()) {
199
+ const total = claims.length;
200
+ const merged = claims.filter((c) => c.state === "merged").length;
201
+ return { merged, total, rate: total === 0 ? 0 : merged / total };
202
+ }
203
+ var TERMINALHIRE_DIR2, CLAIMS_FILE, PUSHED_CLAIM_FIELDS, TERMINAL_STATES;
204
+ var init_claims = __esm({
205
+ "src/claims.ts"() {
206
+ "use strict";
207
+ TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join2(homedir2(), ".terminalhire");
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
+ ];
218
+ TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
219
+ }
220
+ });
221
+
222
+ // bin/claim-push-bg.js
223
+ import { createHash } from "crypto";
224
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, rmSync as rmSync2 } from "fs";
225
+ import { join as join3 } from "path";
226
+ import { homedir as homedir3 } from "os";
227
+
228
+ // src/github-auth.ts
229
+ import {
230
+ createCipheriv,
231
+ createDecipheriv,
232
+ randomBytes
233
+ } from "crypto";
234
+ import {
235
+ readFileSync,
236
+ writeFileSync,
237
+ mkdirSync,
238
+ existsSync,
239
+ rmSync
240
+ } from "fs";
241
+ import { join } from "path";
242
+ import { homedir } from "os";
243
+ var TERMINALHIRE_DIR = join(homedir(), ".terminalhire");
244
+ var TOKEN_FILE = join(TERMINALHIRE_DIR, "github-token.enc");
245
+ var KEY_FILE = join(TERMINALHIRE_DIR, "key");
246
+ var ALGO = "aes-256-gcm";
247
+ var KEY_BYTES = 32;
248
+ var IV_BYTES = 12;
249
+ async function loadKey() {
250
+ try {
251
+ const kt = await Promise.resolve().then(() => __toESM(require_keytar2(), 1));
252
+ const stored = await kt.getPassword("terminalhire", "profile-key");
253
+ if (stored) return Buffer.from(stored, "hex");
254
+ const key2 = randomBytes(KEY_BYTES);
255
+ await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
256
+ return key2;
257
+ } catch {
258
+ }
259
+ mkdirSync(TERMINALHIRE_DIR, { recursive: true });
260
+ if (existsSync(KEY_FILE)) {
261
+ return Buffer.from(readFileSync(KEY_FILE, "utf8").trim(), "hex");
262
+ }
263
+ const key = randomBytes(KEY_BYTES);
264
+ writeFileSync(KEY_FILE, key.toString("hex"), { mode: 384, encoding: "utf8" });
265
+ return key;
266
+ }
267
+ function encrypt(plaintext, key) {
268
+ const iv = randomBytes(IV_BYTES);
269
+ const cipher = createCipheriv(ALGO, key, iv);
270
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
271
+ const tag = cipher.getAuthTag();
272
+ return { iv: iv.toString("hex"), tag: tag.toString("hex"), ciphertext: ct.toString("hex") };
273
+ }
274
+ function decrypt(blob, key) {
275
+ const decipher = createDecipheriv(ALGO, key, Buffer.from(blob.iv, "hex"));
276
+ decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
277
+ const plain = Buffer.concat([
278
+ decipher.update(Buffer.from(blob.ciphertext, "hex")),
279
+ decipher.final()
280
+ ]);
281
+ return plain.toString("utf8");
282
+ }
283
+
284
+ // bin/claim-push-bg.js
285
+ var TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join3(homedir3(), ".terminalhire");
286
+ var CLAIM_PUSH_AUTO_MARKER = join3(TERMINALHIRE_DIR3, "claim-push-auto.json");
287
+ var CLAIM_PUSH_TOKEN_FILE = join3(TERMINALHIRE_DIR3, "claim-push-token.enc");
288
+ var CLAIM_SYNC_BASE = "https://terminalhire.com";
289
+ var AUTO_CONSENT_VERSION = 2;
290
+ var AUTO_PUSH_THROTTLE_MS = 24 * 60 * 60 * 1e3;
291
+ async function writePushTokenEnc(rawToken) {
292
+ mkdirSync3(TERMINALHIRE_DIR3, { recursive: true });
293
+ const key = await loadKey();
294
+ const blob = encrypt(rawToken, key);
295
+ writeFileSync3(CLAIM_PUSH_TOKEN_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
296
+ }
297
+ async function readPushTokenEnc() {
298
+ if (!existsSync3(CLAIM_PUSH_TOKEN_FILE)) return void 0;
299
+ try {
300
+ const key = await loadKey();
301
+ const blob = JSON.parse(readFileSync3(CLAIM_PUSH_TOKEN_FILE, "utf8"));
302
+ return decrypt(blob, key);
303
+ } catch {
304
+ return void 0;
305
+ }
306
+ }
307
+ function clearPushTokenEnc() {
308
+ try {
309
+ rmSync2(CLAIM_PUSH_TOKEN_FILE);
310
+ } catch {
311
+ }
312
+ }
313
+ function readAutoMarker() {
314
+ try {
315
+ return existsSync3(CLAIM_PUSH_AUTO_MARKER) ? JSON.parse(readFileSync3(CLAIM_PUSH_AUTO_MARKER, "utf8")) : null;
316
+ } catch {
317
+ return null;
318
+ }
319
+ }
320
+ function writeAutoMarker(marker) {
321
+ mkdirSync3(TERMINALHIRE_DIR3, { recursive: true });
322
+ writeFileSync3(CLAIM_PUSH_AUTO_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
323
+ }
324
+ function clearAutoMarker() {
325
+ try {
326
+ rmSync2(CLAIM_PUSH_AUTO_MARKER);
327
+ } catch {
328
+ }
329
+ }
330
+ function computeSnapshotHash(pushed) {
331
+ return createHash("sha256").update(JSON.stringify(pushed)).digest("hex");
332
+ }
333
+ function backgroundPushGate(params) {
334
+ const {
335
+ autoMarkerExists,
336
+ tokenFileExists,
337
+ lastPushedAt,
338
+ now,
339
+ throttleMs,
340
+ currentHash,
341
+ lastSnapshotHash
342
+ } = params;
343
+ if (!autoMarkerExists || !tokenFileExists) {
344
+ return { push: false, reason: "not-opted-in" };
345
+ }
346
+ const last = lastPushedAt ? Date.parse(lastPushedAt) : NaN;
347
+ if (!Number.isNaN(last) && now - last < throttleMs) {
348
+ return { push: false, reason: "throttled" };
349
+ }
350
+ if (lastSnapshotHash && lastSnapshotHash === currentHash) {
351
+ return { push: false, reason: "unchanged" };
352
+ }
353
+ return { push: true, reason: "ok" };
354
+ }
355
+ async function runBackgroundClaimPush({ now = Date.now() } = {}) {
356
+ try {
357
+ if (!existsSync3(CLAIM_PUSH_AUTO_MARKER) || !existsSync3(CLAIM_PUSH_TOKEN_FILE)) return;
358
+ const marker = readAutoMarker();
359
+ if (!marker || !marker.autoConsentedAt) return;
360
+ const { listClaims: listClaims2, toPushedClaim: toPushedClaim2, PUSHED_CLAIM_FIELDS: PUSHED_CLAIM_FIELDS2 } = await Promise.resolve().then(() => (init_claims(), claims_exports));
361
+ const pushed = listClaims2().map((c) => toPushedClaim2(c));
362
+ const currentHash = computeSnapshotHash(pushed);
363
+ const gate = backgroundPushGate({
364
+ autoMarkerExists: true,
365
+ tokenFileExists: true,
366
+ lastPushedAt: marker.lastPushedAt ?? null,
367
+ now,
368
+ throttleMs: AUTO_PUSH_THROTTLE_MS,
369
+ currentHash,
370
+ lastSnapshotHash: marker.lastSnapshotHash ?? null
371
+ });
372
+ if (!gate.push) return;
373
+ const token = await readPushTokenEnc();
374
+ if (!token) return;
375
+ const consentReceipt = {
376
+ consentedAt: marker.autoConsentedAt,
377
+ version: AUTO_CONSENT_VERSION,
378
+ fields: PUSHED_CLAIM_FIELDS2
379
+ };
380
+ const res = await fetch(`${CLAIM_SYNC_BASE}/api/claim-sync`, {
381
+ method: "POST",
382
+ headers: { "Content-Type": "application/json" },
383
+ body: JSON.stringify({ consentToken: consentReceipt, claims: pushed, pushToken: token }),
384
+ signal: AbortSignal.timeout(1e4)
385
+ });
386
+ if (!res.ok) return;
387
+ writeAutoMarker({
388
+ ...marker,
389
+ lastPushedAt: new Date(now).toISOString(),
390
+ lastSnapshotHash: currentHash
391
+ });
392
+ } catch {
393
+ }
394
+ }
395
+ export {
396
+ AUTO_CONSENT_VERSION,
397
+ AUTO_PUSH_THROTTLE_MS,
398
+ CLAIM_PUSH_AUTO_MARKER,
399
+ CLAIM_PUSH_TOKEN_FILE,
400
+ backgroundPushGate,
401
+ clearAutoMarker,
402
+ clearPushTokenEnc,
403
+ computeSnapshotHash,
404
+ readAutoMarker,
405
+ readPushTokenEnc,
406
+ runBackgroundClaimPush,
407
+ writeAutoMarker,
408
+ writePushTokenEnc
409
+ };
@@ -924,7 +924,7 @@ async function repoContributorCount(owner, name, token) {
924
924
  return void 0;
925
925
  }
926
926
  }
927
- async function fetchRepoMeta(owner, name, token, cache) {
927
+ async function fetchRepoMeta(owner, name, token, cache, stats) {
928
928
  const key = `${owner}/${name}`.toLowerCase();
929
929
  const cached = cache.get(key);
930
930
  if (cached !== void 0) return cached;
@@ -941,8 +941,10 @@ async function fetchRepoMeta(owner, name, token, cache) {
941
941
  topics: r.topics ?? [],
942
942
  contributors
943
943
  };
944
- } catch {
944
+ } catch (err) {
945
945
  meta = null;
946
+ const msg = err instanceof Error ? err.message : String(err);
947
+ if (stats && TRANSIENT_META_ERROR.test(msg)) stats.transient += 1;
946
948
  }
947
949
  cache.set(key, meta);
948
950
  return meta;
@@ -981,8 +983,10 @@ async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates
981
983
  return emptyCredential(/HTTP 403|HTTP 429|rate limit/i.test(msg) ? "rate-limited" : "failed");
982
984
  }
983
985
  const byDomain = {};
986
+ const distinctOrgSet = /* @__PURE__ */ new Set();
984
987
  let qualifyingTotal = 0;
985
988
  const qualifyingPRs = [];
989
+ const metaStats = { transient: 0 };
986
990
  for (const item of items) {
987
991
  const repo = parseRepoUrl(item.repository_url);
988
992
  if (!repo) continue;
@@ -990,13 +994,20 @@ async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates
990
994
  if (ownerLc === loginLc) continue;
991
995
  if (ownedOrgs.has(ownerLc)) continue;
992
996
  if (isTrivialPRTitle(item.title)) continue;
993
- const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache);
997
+ const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache, metaStats);
998
+ if (metaStats.transient > 0) {
999
+ console.warn(
1000
+ `[acceptance] ${login}: per-repo metadata transient failure (${metaStats.transient}) \u2014 degrading to 'rate-limited' rather than a fabricated count`
1001
+ );
1002
+ return emptyCredential("rate-limited");
1003
+ }
994
1004
  if (!meta) continue;
995
1005
  if (meta.private) continue;
996
1006
  if (meta.archived || meta.fork) continue;
997
1007
  if (meta.stars < gates.minStars) continue;
998
1008
  if (meta.contributors !== void 0 && meta.contributors < gates.minContributors) continue;
999
1009
  qualifyingTotal += 1;
1010
+ distinctOrgSet.add(ownerLc);
1000
1011
  const mergedAt = item.pull_request?.merged_at ?? item.closed_at ?? item.created_at;
1001
1012
  const rawDomains = [meta.language ?? "", ...meta.topics].filter(Boolean);
1002
1013
  const domainTags = [...new Set(normalize(rawDomains))];
@@ -1022,7 +1033,14 @@ async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates
1022
1033
  lastMergedAt: b.lastMergedAt
1023
1034
  };
1024
1035
  }
1025
- return { status: "ok", byDomain: finalDomains, qualifyingTotal, qualifyingPRs, computedAt };
1036
+ return {
1037
+ status: "ok",
1038
+ byDomain: finalDomains,
1039
+ qualifyingTotal,
1040
+ qualifyingPRs,
1041
+ distinctOrgs: distinctOrgSet.size,
1042
+ computedAt
1043
+ };
1026
1044
  }
1027
1045
  async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */ new Map()) {
1028
1046
  if (!token) return emptyCredential("no-token");
@@ -1036,6 +1054,61 @@ async function computeAcceptanceCredentialPublic(login, token, cache = /* @__PUR
1036
1054
  const gates = opts?.relaxGates ? { minStars: 0, minContributors: 0 } : void 0;
1037
1055
  return computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates);
1038
1056
  }
1057
+ async function fetchOpenExternalPRs(login, token, cache = /* @__PURE__ */ new Map(), gates = {
1058
+ minStars: MIN_STARS,
1059
+ minContributors: MIN_CONTRIBUTORS
1060
+ }) {
1061
+ if (!token) return [];
1062
+ const loginLc = login.toLowerCase();
1063
+ let ownedOrgs;
1064
+ try {
1065
+ ownedOrgs = await fetchPublicOrgs(login, token);
1066
+ } catch {
1067
+ return null;
1068
+ }
1069
+ let items;
1070
+ try {
1071
+ const q = encodeURIComponent(`type:pr is:open is:public author:${login} -user:${login} sort:updated`);
1072
+ const res = await ghFetch(
1073
+ `/search/issues?q=${q}&per_page=${OPEN_PR_PAGE}`,
1074
+ token
1075
+ );
1076
+ items = res.items ?? [];
1077
+ } catch (err) {
1078
+ const msg = err instanceof Error ? err.message : String(err);
1079
+ console.warn("[open-prs] search failed:", msg);
1080
+ return null;
1081
+ }
1082
+ const metaStats = { transient: 0 };
1083
+ const out = [];
1084
+ for (const item of items) {
1085
+ const repo = parseRepoUrl(item.repository_url);
1086
+ if (!repo) continue;
1087
+ const ownerLc = repo.owner.toLowerCase();
1088
+ if (ownerLc === loginLc) continue;
1089
+ if (ownedOrgs.has(ownerLc)) continue;
1090
+ if (isTrivialPRTitle(item.title)) continue;
1091
+ const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache, metaStats);
1092
+ if (metaStats.transient > 0) {
1093
+ console.warn(
1094
+ `[open-prs] ${login}: per-repo metadata transient failure (${metaStats.transient}) \u2014 returning null (keep prior strip)`
1095
+ );
1096
+ return null;
1097
+ }
1098
+ if (!meta) continue;
1099
+ if (meta.private) continue;
1100
+ if (meta.archived || meta.fork) continue;
1101
+ if (meta.stars < gates.minStars) continue;
1102
+ if (meta.contributors !== void 0 && meta.contributors < gates.minContributors) continue;
1103
+ out.push({
1104
+ title: item.title,
1105
+ url: item.html_url,
1106
+ repoFullName: `${repo.owner}/${repo.name}`,
1107
+ openedAt: item.created_at
1108
+ });
1109
+ }
1110
+ return out;
1111
+ }
1039
1112
  function acceptanceCountForDomains(cred, domains) {
1040
1113
  if (cred.status !== "ok") return 0;
1041
1114
  let max = 0;
@@ -1107,7 +1180,7 @@ function deriveResumeTrend(cred, repoRecency, now = Date.now()) {
1107
1180
  }
1108
1181
  return scored.sort((a, b) => b.weight - a.weight).slice(0, 12).map((s) => s.t);
1109
1182
  }
1110
- var TRACTION_TOP_N, CANDIDATE_PR_PAGE, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
1183
+ var TRACTION_TOP_N, CANDIDATE_PR_PAGE, OPEN_PR_PAGE, TRANSIENT_META_ERROR, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
1111
1184
  var init_github = __esm({
1112
1185
  "../../packages/core/src/github.ts"() {
1113
1186
  "use strict";
@@ -1115,6 +1188,8 @@ var init_github = __esm({
1115
1188
  init_contribution_gate();
1116
1189
  TRACTION_TOP_N = 6;
1117
1190
  CANDIDATE_PR_PAGE = 50;
1191
+ OPEN_PR_PAGE = 20;
1192
+ TRANSIENT_META_ERROR = /HTTP 403|HTTP 429|rate limit|HTTP 5\d\d|timeout|network|fetch failed/i;
1118
1193
  RESUME_DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
1119
1194
  RESUME_MIN_SCORE = 0.05;
1120
1195
  }
@@ -2860,6 +2935,10 @@ ${pr.body ?? ""}`.matchAll(/#(\d+)\b/g)) {
2860
2935
  }
2861
2936
  return refs;
2862
2937
  }
2938
+ async function fetchRateLimit(client) {
2939
+ const r = await client.json("/rate_limit");
2940
+ return r?.resources ?? null;
2941
+ }
2863
2942
  async function searchContribIssues(client, queries) {
2864
2943
  const byUrl = /* @__PURE__ */ new Map();
2865
2944
  for (const q of queries) {
@@ -2904,6 +2983,9 @@ async function aggregateContributions(opts = {}) {
2904
2983
  const jobs = [];
2905
2984
  const seen = /* @__PURE__ */ new Set();
2906
2985
  const perRepo = /* @__PURE__ */ new Map();
2986
+ let metaNull = 0;
2987
+ let contribUndefined = 0;
2988
+ let prRefsNull = 0;
2907
2989
  for (const issue of issues) {
2908
2990
  if (jobs.length >= MAX_CONTRIB_ITEMS) break;
2909
2991
  const fullName = repoFullNameFromApiUrl2(issue.repository_url);
@@ -2914,9 +2996,13 @@ async function aggregateContributions(opts = {}) {
2914
2996
  if (isAssigned(issue)) continue;
2915
2997
  if ((perRepo.get(fullName) ?? 0) >= MAX_BOUNTIES_PER_DISCOVERED_REPO) continue;
2916
2998
  const repo = await repoMeta(fullName);
2917
- if (!repo) continue;
2999
+ if (!repo) {
3000
+ metaNull++;
3001
+ continue;
3002
+ }
2918
3003
  const title = decodeEntities(issue.title).trim();
2919
3004
  const contributors = await repoContribCount(fullName);
3005
+ if (contributors === void 0) contribUndefined++;
2920
3006
  if (!passesContributionGate({
2921
3007
  fullName,
2922
3008
  stars: repo.stargazers_count,
@@ -2932,6 +3018,7 @@ async function aggregateContributions(opts = {}) {
2932
3018
  const labels = labelNames2(issue.labels);
2933
3019
  if (looksLikeContentTask({ title, body, labels })) continue;
2934
3020
  const prRefs = await repoPRRefs(fullName);
3021
+ if (prRefs === null) prRefsNull++;
2935
3022
  if (prRefs && prRefs.has(issue.number)) continue;
2936
3023
  const openPRsAtDiscovery = prRefs ? 0 : void 0;
2937
3024
  const tags = normalize(
@@ -2971,9 +3058,18 @@ async function aggregateContributions(opts = {}) {
2971
3058
  raw: issue
2972
3059
  });
2973
3060
  }
3061
+ if (!opts.fetchImpl) {
3062
+ const rl = await fetchRateLimit(client);
3063
+ const core = rl?.core ? `${rl.core.remaining}/${rl.core.limit}` : "n/a";
3064
+ const search = rl?.search ? `${rl.search.remaining}/${rl.search.limit}` : "n/a";
3065
+ const noToken = !(process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"]);
3066
+ console.info(
3067
+ `[contribute] build metrics \u2014 scanned=${issues.length} reposDistinct=${repoCache.size} emitted=${jobs.length} metaNull=${metaNull} contribUndefined=${contribUndefined} prRefsNull=${prRefsNull} core=${core} search=${search}` + (noToken ? " (NO TOKEN \u2192 60/hr)" : "")
3068
+ );
3069
+ }
2974
3070
  return jobs;
2975
3071
  }
2976
- var GITHUB_API2, CONTRIB_SEARCH_QUERIES, SEARCH_PER_PAGE2, MAX_CONTRIB_ITEMS, MAX_CONTRIB_ISSUES_SCANNED;
3072
+ var GITHUB_API2, CONTRIB_LABEL_QUERIES, CONTRIB_LANGUAGE_QUERIES, CONTRIB_SEARCH_QUERIES, SEARCH_PER_PAGE2, MAX_CONTRIB_ITEMS, MAX_CONTRIB_ISSUES_SCANNED;
2977
3073
  var init_contributions = __esm({
2978
3074
  "../../packages/core/src/feeds/contributions.ts"() {
2979
3075
  "use strict";
@@ -2985,15 +3081,25 @@ var init_contributions = __esm({
2985
3081
  init_github_bounties();
2986
3082
  init_http();
2987
3083
  GITHUB_API2 = "https://api.github.com";
2988
- CONTRIB_SEARCH_QUERIES = [
3084
+ CONTRIB_LABEL_QUERIES = [
2989
3085
  'label:"good first issue" type:issue state:open',
2990
- 'label:"help wanted" type:issue state:open',
2991
3086
  'label:"good-first-issue" type:issue state:open',
2992
- 'label:"help-wanted" type:issue state:open'
3087
+ 'label:"help wanted" type:issue state:open',
3088
+ 'label:"help-wanted" type:issue state:open',
3089
+ 'label:"up-for-grabs" type:issue state:open'
3090
+ ];
3091
+ CONTRIB_LANGUAGE_QUERIES = [
3092
+ ...["rust", "go", "python", "c++", "ruby"].map(
3093
+ (lang) => `label:"help wanted" language:${lang} type:issue state:open`
3094
+ ),
3095
+ ...["rust", "go"].map(
3096
+ (lang) => `label:"good first issue" language:${lang} type:issue state:open`
3097
+ )
2993
3098
  ];
3099
+ CONTRIB_SEARCH_QUERIES = [...CONTRIB_LABEL_QUERIES, ...CONTRIB_LANGUAGE_QUERIES];
2994
3100
  SEARCH_PER_PAGE2 = 100;
2995
3101
  MAX_CONTRIB_ITEMS = 150;
2996
- MAX_CONTRIB_ISSUES_SCANNED = 300;
3102
+ MAX_CONTRIB_ISSUES_SCANNED = 600;
2997
3103
  }
2998
3104
  });
2999
3105
 
@@ -6436,13 +6542,15 @@ function deriveLegibleProfile(credential, recency, traction, seniorityBand) {
6436
6542
  daysAgo = Math.max(0, Math.round(ageDays2));
6437
6543
  recencyBadge = { lastMergedAt: mostRecent, state: ageDays2 <= thresholdDays ? "live" : "dormant" };
6438
6544
  }
6439
- const orgCount = Object.values(domains).reduce((m, d) => Math.max(m, d.distinctOrgs), 0);
6545
+ const exactOrgCount = typeof credential.distinctOrgs === "number" && credential.distinctOrgs > 0;
6546
+ const orgCount = exactOrgCount ? credential.distinctOrgs : Object.values(domains).reduce((m, d) => Math.max(m, d.distinctOrgs), 0);
6440
6547
  let proofSentence;
6441
6548
  if (!ok) {
6442
6549
  proofSentence = "Contribution credential unavailable \u2014 could not verify.";
6443
6550
  } else {
6444
6551
  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)`;
6552
+ const orgPhrase = exactOrgCount ? `${orgCount}` : `at least ${orgCount}`;
6553
+ 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
6554
  if (daysAgo !== void 0) s += ` \u2014 most recent ${daysAgo}d ago`;
6447
6555
  proofSentence = `${s}.`;
6448
6556
  }
@@ -6680,6 +6788,7 @@ __export(src_exports, {
6680
6788
  expandWeighted: () => expandWeighted,
6681
6789
  extractSkillTags: () => extractSkillTags,
6682
6790
  fetchGitHubProfile: () => fetchGitHubProfile,
6791
+ fetchOpenExternalPRs: () => fetchOpenExternalPRs,
6683
6792
  fetchOwnedRepoTraction: () => fetchOwnedRepoTraction,
6684
6793
  fetchRepoRecency: () => fetchRepoRecency,
6685
6794
  flattenTiers: () => flattenTiers,
@@ -6759,9 +6868,9 @@ var init_keytar = __esm({
6759
6868
  }
6760
6869
  });
6761
6870
 
6762
- // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/release-0192/node_modules/keytar/build/Release/keytar.node
6871
+ // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
6763
6872
  var require_keytar = __commonJS({
6764
- "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/release-0192/node_modules/keytar/build/Release/keytar.node"(exports, module) {
6873
+ "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
6765
6874
  "use strict";
6766
6875
  init_keytar();
6767
6876
  try {