terminalhire 0.30.2 → 0.32.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.
@@ -346,9 +346,9 @@ var init_keytar = __esm({
346
346
  }
347
347
  });
348
348
 
349
- // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
349
+ // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node
350
350
  var require_keytar = __commonJS({
351
- "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
351
+ "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node"(exports, module) {
352
352
  "use strict";
353
353
  init_keytar();
354
354
  try {
@@ -10182,6 +10182,94 @@ var init_directory2 = __esm({
10182
10182
  }
10183
10183
  });
10184
10184
 
10185
+ // bin/match-slots.js
10186
+ function rotate(list, now, windowMs = ROTATE_WINDOW_MS) {
10187
+ if (!Array.isArray(list) || list.length === 0) return [];
10188
+ const idx = Math.floor(now / windowMs) % list.length;
10189
+ return [...list.slice(idx), ...list.slice(0, idx)];
10190
+ }
10191
+ function budgetSlots(results, opts = {}) {
10192
+ const {
10193
+ thinProfile = false,
10194
+ contributeEnabled = true,
10195
+ totalSlots = TOTAL_SLOTS,
10196
+ now = Date.now(),
10197
+ interestPicks = [],
10198
+ interestJobPicks = [],
10199
+ interestJobSlots = INTEREST_JOB_SLOTS
10200
+ } = opts;
10201
+ const list = Array.isArray(results) ? results : [];
10202
+ const bountyMatches = list.filter(
10203
+ (r) => r && r.job && r.job.source === "bounty" && typeof r.score === "number" && r.score >= BOUNTY_MIN_MATCH
10204
+ );
10205
+ const bountyTop = [...bountyMatches].sort((a, b) => b.score - a.score).slice(0, BOUNTY_SLOTS);
10206
+ const contributeMatches = list.filter((r) => r && r.job && r.job.source === "contribute");
10207
+ const contributeCap = thinProfile && contributeEnabled ? CONTRIBUTE_SLOTS_THIN : CONTRIBUTE_SLOTS;
10208
+ const contributeTop = rotate(contributeMatches, now).slice(0, contributeCap);
10209
+ const reservedContributeIds = new Set(contributeTop.map((r) => r.job.id));
10210
+ const interestTop = (Array.isArray(interestPicks) ? interestPicks : []).filter((r) => r && r.job && !reservedContributeIds.has(r.job.id)).slice(0, INTEREST_CONTRIBUTE_SLOTS).map((r) => ({ ...r, interestLabel: INTEREST_SLOT_LABEL }));
10211
+ const surfacedIds = new Set(list.map((r) => r?.job?.id).filter(Boolean));
10212
+ const interestJobTop = (Array.isArray(interestJobPicks) ? interestJobPicks : []).filter((r) => r && r.job && !surfacedIds.has(r.job.id)).slice(0, interestJobSlots).map((r) => ({ ...r, interestLabel: INTEREST_SLOT_LABEL }));
10213
+ const roleSlots = Math.max(
10214
+ 0,
10215
+ totalSlots - bountyTop.length - contributeTop.length - interestTop.length - interestJobTop.length
10216
+ );
10217
+ const roleMatches = list.filter(
10218
+ (r) => r && r.job && r.job.source !== "bounty" && r.job.source !== "contribute"
10219
+ );
10220
+ const roleStable = Math.min(ROLE_STABLE_MAX, roleSlots);
10221
+ const headPoolSize = Math.max(roleStable, ROLE_HEAD_POOL);
10222
+ const headPool = roleMatches.slice(0, headPoolSize);
10223
+ const headIndex = new Map(headPool.map((r, i) => [r, i]));
10224
+ const stableRoles = rotate(headPool, now, ROLE_HEAD_ROTATE_MS).slice(0, roleStable).sort((a, b) => headIndex.get(a) - headIndex.get(b));
10225
+ const rotableRoles = roleMatches.slice(headPoolSize, headPoolSize + ROLE_ROTATE_WINDOW);
10226
+ const rotatedRoles = rotate(rotableRoles, now);
10227
+ const roleTop = [...stableRoles, ...rotatedRoles].slice(0, roleSlots);
10228
+ return { roleTop, bountyTop, contributeTop, interestTop, interestJobTop };
10229
+ }
10230
+ var TOTAL_SLOTS, BOUNTY_SLOTS, BOUNTY_MIN_MATCH, INTEREST_CONTRIBUTE_SLOTS, INTEREST_JOB_SLOTS, INTEREST_SLOT_LABEL, CONTRIBUTE_SLOTS, CONTRIBUTE_SLOTS_THIN, ROLE_STABLE_MAX, ROLE_ROTATE_WINDOW, ROTATE_WINDOW_MS, ROLE_HEAD_POOL, ROLE_HEAD_ROTATE_MS;
10231
+ var init_match_slots = __esm({
10232
+ "bin/match-slots.js"() {
10233
+ "use strict";
10234
+ TOTAL_SLOTS = 25;
10235
+ BOUNTY_SLOTS = 3;
10236
+ BOUNTY_MIN_MATCH = 0.5;
10237
+ INTEREST_CONTRIBUTE_SLOTS = 1;
10238
+ INTEREST_JOB_SLOTS = 1;
10239
+ INTEREST_SLOT_LABEL = "Stretch";
10240
+ CONTRIBUTE_SLOTS = 5;
10241
+ CONTRIBUTE_SLOTS_THIN = 8;
10242
+ ROLE_STABLE_MAX = 8;
10243
+ ROLE_ROTATE_WINDOW = 60;
10244
+ ROTATE_WINDOW_MS = 5 * 60 * 1e3;
10245
+ ROLE_HEAD_POOL = 12;
10246
+ ROLE_HEAD_ROTATE_MS = 60 * 60 * 1e3;
10247
+ }
10248
+ });
10249
+
10250
+ // bin/directory-select.js
10251
+ function selectDirectoryDevs(results, { limit, now = Date.now() } = {}) {
10252
+ const list = Array.isArray(results) ? results : [];
10253
+ if (list.length === 0) return [];
10254
+ if (limit >= list.length) return list;
10255
+ const head = list.slice(0, Math.min(DEVS_STABLE_HEAD, limit));
10256
+ const tailPool = list.slice(head.length, head.length + DEVS_ROTATE_WINDOW);
10257
+ const tailSelected = rotate(tailPool, now, DEVS_ROTATE_MS).slice(0, limit - head.length);
10258
+ const rankOf = new Map(list.map((item, idx) => [item, idx]));
10259
+ const tail = [...tailSelected].sort((a, b) => rankOf.get(a) - rankOf.get(b));
10260
+ return [...head, ...tail];
10261
+ }
10262
+ var DEVS_STABLE_HEAD, DEVS_ROTATE_WINDOW, DEVS_ROTATE_MS;
10263
+ var init_directory_select = __esm({
10264
+ "bin/directory-select.js"() {
10265
+ "use strict";
10266
+ init_match_slots();
10267
+ DEVS_STABLE_HEAD = 5;
10268
+ DEVS_ROTATE_WINDOW = 15;
10269
+ DEVS_ROTATE_MS = 5 * 60 * 1e3;
10270
+ }
10271
+ });
10272
+
10185
10273
  // bin/jpi-devs.js
10186
10274
  var jpi_devs_exports = {};
10187
10275
  __export(jpi_devs_exports, {
@@ -10241,7 +10329,7 @@ async function getDevs({ quiet = false, offline = false } = {}) {
10241
10329
  }
10242
10330
  const cards = index.cards ?? [];
10243
10331
  if (cards.length === 0) return { status: "no-cards", fp };
10244
- let results = match2(fp, cards, SHOW_ALL2 ? cards.length : LIMIT2);
10332
+ let results = match2(fp, cards, cards.length);
10245
10333
  let ownLogin;
10246
10334
  try {
10247
10335
  const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
@@ -10249,6 +10337,7 @@ async function getDevs({ quiet = false, offline = false } = {}) {
10249
10337
  } catch {
10250
10338
  }
10251
10339
  results = excludeOwnCard(results, ownLogin);
10340
+ if (!SHOW_ALL2) results = selectDirectoryDevs(results, { limit: LIMIT2 });
10252
10341
  if (results.length === 0) return { status: "no-matches", fp };
10253
10342
  return { status: "ok", results, fp };
10254
10343
  }
@@ -10308,6 +10397,7 @@ var init_jpi_devs = __esm({
10308
10397
  "use strict";
10309
10398
  init_directory2();
10310
10399
  init_sanitize();
10400
+ init_directory_select();
10311
10401
  API_URL3 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
10312
10402
  DEFAULT_LIMIT2 = 10;
10313
10403
  args2 = process.argv.slice(2);
@@ -11148,6 +11238,8 @@ var init_claim_push_bg = __esm({
11148
11238
  // src/repo-policy.ts
11149
11239
  var repo_policy_exports = {};
11150
11240
  __export(repo_policy_exports, {
11241
+ POLICY_RULESET_VERSION: () => POLICY_RULESET_VERSION,
11242
+ auditContent: () => auditContent,
11151
11243
  checkRepoPolicy: () => checkRepoPolicy
11152
11244
  });
11153
11245
  async function fetchContentsFile(fetchImpl, repoFullName, path) {
@@ -11169,23 +11261,50 @@ async function fetchContentsFile(fetchImpl, repoFullName, path) {
11169
11261
  return { ok: false, missing: false, content: null };
11170
11262
  }
11171
11263
  }
11172
- function findHits(file, content) {
11173
- const lines = content.split("\n");
11264
+ function classifyLine(line) {
11265
+ if (PROHIBITED_PATTERNS.some((re) => re.test(line))) return "prohibited";
11266
+ if (DISCLOSURE_PATTERNS.some((re) => re.test(line))) return "disclosure-required";
11267
+ if (AI_SIGNAL_PATTERNS.some((p) => p.re.test(line))) return "ai-mentioned";
11268
+ return null;
11269
+ }
11270
+ function sanitizeExcerpt(text) {
11271
+ return text.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, "");
11272
+ }
11273
+ function excerptAround(lines, i) {
11274
+ const start = Math.max(0, i - 2);
11275
+ const end = Math.min(lines.length, i + 3);
11276
+ return sanitizeExcerpt(lines.slice(start, end).join("\n"));
11277
+ }
11278
+ function auditContent(files) {
11174
11279
  const hits = [];
11175
- for (let i = 0; i < lines.length; i++) {
11176
- const line = lines[i];
11177
- if (!AI_SIGNAL_PATTERNS.some((p) => p.re.test(line))) continue;
11178
- const start = Math.max(0, i - 2);
11179
- const end = Math.min(lines.length, i + 3);
11180
- hits.push({ file, excerpt: lines.slice(start, end).join("\n") });
11280
+ const requirements = [];
11281
+ const seenKinds = /* @__PURE__ */ new Set();
11282
+ for (const { file, content } of files) {
11283
+ const lines = content.split("\n");
11284
+ for (let i = 0; i < lines.length; i++) {
11285
+ const rule = classifyLine(lines[i]);
11286
+ if (rule) hits.push({ file, excerpt: excerptAround(lines, i), rule });
11287
+ for (const { kind, re } of REQUIREMENT_PATTERNS) {
11288
+ if (seenKinds.has(kind) || !re.test(lines[i])) continue;
11289
+ seenKinds.add(kind);
11290
+ requirements.push({ kind, file, excerpt: excerptAround(lines, i) });
11291
+ }
11292
+ }
11181
11293
  }
11182
- return hits;
11294
+ let verdict = "clean";
11295
+ for (const h of hits) {
11296
+ if (verdict === "clean" || VERDICT_SEVERITY[h.rule] > VERDICT_SEVERITY[verdict]) {
11297
+ verdict = h.rule;
11298
+ }
11299
+ }
11300
+ const assignment = seenKinds.has("take-bot") ? "take-bot" : seenKinds.has("assignment-required") ? "required" : "none";
11301
+ return { hits, requirements, verdict, assignment };
11183
11302
  }
11184
11303
  async function checkRepoPolicy(repoFullName, opts = {}) {
11185
11304
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
11186
11305
  let requestsUsed = 0;
11187
11306
  let hadError = false;
11188
- const hits = [];
11307
+ const files = [];
11189
11308
  outer: for (const group of CANDIDATE_GROUPS) {
11190
11309
  for (const path of group) {
11191
11310
  if (requestsUsed >= MAX_REQUESTS) break outer;
@@ -11196,21 +11315,27 @@ async function checkRepoPolicy(repoFullName, opts = {}) {
11196
11315
  continue;
11197
11316
  }
11198
11317
  if (outcome.missing) continue;
11199
- if (outcome.content) hits.push(...findHits(path, outcome.content));
11318
+ if (outcome.content) files.push({ file: path, content: outcome.content });
11200
11319
  break;
11201
11320
  }
11202
11321
  }
11203
- if (hits.length > 0) return { status: "flagged", hits };
11204
- if (hadError) return { status: "unavailable", hits: [] };
11205
- return { status: "clean", hits: [] };
11322
+ const { hits, requirements, verdict, assignment } = auditContent(files);
11323
+ if (hits.length > 0) {
11324
+ return { status: "flagged", verdict, hits, requirements, assignment, rulesetVersion: POLICY_RULESET_VERSION };
11325
+ }
11326
+ if (hadError) {
11327
+ return { status: "unavailable", verdict: "unavailable", hits: [], requirements, assignment, rulesetVersion: POLICY_RULESET_VERSION };
11328
+ }
11329
+ return { status: "clean", verdict: "clean", hits: [], requirements, assignment, rulesetVersion: POLICY_RULESET_VERSION };
11206
11330
  }
11207
- var GH_API, GH_HEADERS, MAX_REQUESTS, AI_SIGNAL_PATTERNS, CANDIDATE_GROUPS;
11331
+ var GH_API, GH_HEADERS, MAX_REQUESTS, POLICY_RULESET_VERSION, AI_SIGNAL_PATTERNS, AI_TERM, PROHIBITED_PATTERNS, DISCLOSURE_PATTERNS, REQUIREMENT_PATTERNS, CANDIDATE_GROUPS, VERDICT_SEVERITY;
11208
11332
  var init_repo_policy = __esm({
11209
11333
  "src/repo-policy.ts"() {
11210
11334
  "use strict";
11211
11335
  GH_API = "https://api.github.com";
11212
11336
  GH_HEADERS = { "User-Agent": "terminalhire-claim", Accept: "application/vnd.github+json" };
11213
11337
  MAX_REQUESTS = 4;
11338
+ POLICY_RULESET_VERSION = 2;
11214
11339
  AI_SIGNAL_PATTERNS = [
11215
11340
  { label: "AI", re: /\bAI\b/i },
11216
11341
  { label: "artificial intelligence", re: /artificial intelligence/i },
@@ -11222,11 +11347,47 @@ var init_repo_policy = __esm({
11222
11347
  { label: "generative", re: /\bgenerative\b/i },
11223
11348
  { label: "machine-generated", re: /machine[\s-]generated/i }
11224
11349
  ];
11350
+ AI_TERM = "(?:ai|llms?|generative(?:\\s+ai)?|artificial intelligence|language models?|copilot|chatgpt|claude|machine[\\s-]generated)";
11351
+ PROHIBITED_PATTERNS = [
11352
+ new RegExp(`prohibit\\w*[^.\\n]{0,60}\\b${AI_TERM}`, "i"),
11353
+ /did not write the code yourself/i,
11354
+ new RegExp(`(?:not|never|don'?t|won'?t)\\s+accept\\w*[^.\\n]{0,60}\\b${AI_TERM}`, "i"),
11355
+ new RegExp(`\\bno\\s+${AI_TERM}[^.\\n]{0,40}\\b(?:prs?|pull requests?|contributions?|code|patch(?:es)?|commits?|submissions?)\\b`, "i"),
11356
+ new RegExp(
11357
+ `\\b${AI_TERM}[^.\\n]{0,60}\\b(?:is|are|will be)\\s+(?:\\w+\\s+)?(?:not accepted|not allowed|banned|rejected|removed|closed|reverted|prohibited|forbidden)`,
11358
+ "i"
11359
+ )
11360
+ ];
11361
+ DISCLOSURE_PATTERNS = [
11362
+ new RegExp(`disclos\\w+[^.\\n]{0,60}\\b${AI_TERM}`, "i"),
11363
+ new RegExp(`\\b${AI_TERM}[^.\\n]{0,60}disclos`, "i"),
11364
+ new RegExp(`\\b${AI_TERM}[\\s-]assist\\w*[^.\\n]{0,60}\\b(?:must|should|required?)\\b`, "i"),
11365
+ // PR-template checkbox, e.g. "- [ ] I used AI tools and have reviewed the output"
11366
+ new RegExp(`\\[ \\][^\\n]{0,80}\\b${AI_TERM}`, "i")
11367
+ ];
11368
+ REQUIREMENT_PATTERNS = [
11369
+ // `/take` bot first: its docs usually also say "assign", and the bot is the
11370
+ // more specific expectation (post exactly `/take`, not a prose request).
11371
+ { kind: "take-bot", re: /(?:^|[\s`"'(])\/take\b/m },
11372
+ { kind: "assignment-required", re: /(?:request|ask|wait)[^.\n]{0,40}\bassign/i },
11373
+ { kind: "assignment-required", re: /\bassigned before\b/i },
11374
+ { kind: "assignment-required", re: /\bself[\s-]assign/i },
11375
+ { kind: "assignment-required", re: /do not (?:open|submit)[^.\n]{0,40}\b(?:prs?|pull requests?)\b/i },
11376
+ { kind: "cla-required", re: /\bCLA\b/ },
11377
+ { kind: "cla-required", re: /contributor licen[cs]e agreement/i },
11378
+ { kind: "discussion-first", re: /open an issue (?:first|before)/i },
11379
+ { kind: "discussion-first", re: /discuss\w*[^.\n]{0,40}\bbefore\b/i }
11380
+ ];
11225
11381
  CANDIDATE_GROUPS = [
11226
11382
  ["CONTRIBUTING.md", ".github/CONTRIBUTING.md", "docs/CONTRIBUTING.md"],
11227
- [".github/PULL_REQUEST_TEMPLATE.md", "PULL_REQUEST_TEMPLATE.md"],
11228
- ["AGENTS.md"]
11383
+ ["AGENTS.md", "AGENTS.MD"],
11384
+ [".github/PULL_REQUEST_TEMPLATE.md", "PULL_REQUEST_TEMPLATE.md"]
11229
11385
  ];
11386
+ VERDICT_SEVERITY = {
11387
+ "ai-mentioned": 1,
11388
+ "disclosure-required": 2,
11389
+ prohibited: 3
11390
+ };
11230
11391
  }
11231
11392
  });
11232
11393
 
@@ -11399,6 +11560,7 @@ __export(jpi_claim_exports, {
11399
11560
  run: () => run7,
11400
11561
  selectCompetingPrs: () => selectCompetingPrs,
11401
11562
  selectPushRemote: () => selectPushRemote,
11563
+ shouldRequestAssignment: () => shouldRequestAssignment,
11402
11564
  startBranchFor: () => startBranchFor,
11403
11565
  workDirFor: () => workDirFor
11404
11566
  });
@@ -11423,17 +11585,38 @@ function buildSubmitBody(issueNumber, opts = {}) {
11423
11585
  return `${closesLine}${main}${AI_DISCLOSURE_NOTE}`;
11424
11586
  }
11425
11587
  function printPolicySection(policy) {
11426
- if (policy.status === "flagged") {
11588
+ const verdict = policy.verdict ?? (policy.status === "flagged" ? "ai-mentioned" : policy.status);
11589
+ if (verdict === "prohibited") {
11590
+ console.log("\n POLICY: \u2717 this repo PROHIBITS AI-generated contributions \u2014 READ BEFORE CLAIMING:");
11591
+ } else if (verdict === "disclosure-required") {
11592
+ console.log("\n POLICY: \u26A0 this repo requires disclosing AI assistance \u2014 READ BEFORE WORKING:");
11593
+ } else if (verdict === "ai-mentioned") {
11427
11594
  console.log("\n POLICY: \u26A0 possible AI-assistance policy language found \u2014 READ BEFORE WORKING:");
11428
- for (const hit of policy.hits) {
11429
- console.log(` [${hit.file}]`);
11430
- for (const line of hit.excerpt.split("\n")) console.log(` ${line}`);
11431
- }
11432
11595
  } else if (policy.status === "unavailable") {
11433
11596
  console.log("\n POLICY: could not read this repo's CONTRIBUTING/PR-template/AGENTS docs (rate-limited or unreachable) \u2014 read them yourself before working.");
11434
11597
  } else {
11435
11598
  console.log(" policy: no AI-assistance policy language detected in CONTRIBUTING/PR-template/AGENTS docs");
11436
11599
  }
11600
+ for (const hit of policy.hits ?? []) {
11601
+ console.log(` [${hit.file}]`);
11602
+ for (const line of hit.excerpt.split("\n")) console.log(` ${line}`);
11603
+ }
11604
+ const requirements = policy.requirements ?? [];
11605
+ if (requirements.length > 0) {
11606
+ console.log("\n REQUIREMENTS: this repo sets contribution expectations \u2014 in its own words:");
11607
+ for (const req of requirements) {
11608
+ console.log(` ${req.kind} [${req.file}]`);
11609
+ for (const line of req.excerpt.split("\n")) console.log(` ${line}`);
11610
+ }
11611
+ }
11612
+ const assignment = policy.assignment ?? "none";
11613
+ if (assignment === "take-bot") {
11614
+ console.log(" assignment: repo self-assigns via a /take comment \u2014 `claim start` will post `/take` on the issue");
11615
+ } else if (assignment === "required") {
11616
+ console.log(" assignment: repo expects you to request assignment \u2014 `claim start` will post a request comment on the issue");
11617
+ } else {
11618
+ console.log(" assignment: no assignment expectation found in repo docs \u2014 `claim start` will not comment (pass --assign to request anyway)");
11619
+ }
11437
11620
  }
11438
11621
  async function sh(cmd, args5, opts = {}) {
11439
11622
  const { stdout } = await pExecFile(cmd, args5, { ...opts, shell: false, maxBuffer: 16 * 1024 * 1024 });
@@ -11700,9 +11883,9 @@ async function fetchIssue(repoFullName, issueNumber) {
11700
11883
  return null;
11701
11884
  }
11702
11885
  }
11703
- function buildAssignmentComment(repoFullName) {
11704
- const usesTakeBot = TAKE_BOT_REPOS.has(repoFullName.toLowerCase());
11705
- const body = usesTakeBot ? "/take" : `I'd like to work on this \u2014 could I be assigned? Thanks!
11886
+ function buildAssignmentComment(repoFullName, opts = {}) {
11887
+ const usesTakeBot = TAKE_BOT_REPOS.has(repoFullName.toLowerCase()) || Boolean(opts.takeBot);
11888
+ const body = usesTakeBot ? "/take" : `Hi! I'd like to work on this issue. Could you assign it to me? Thanks!
11706
11889
 
11707
11890
  ${ASSIGNMENT_MARKER}`;
11708
11891
  return { usesTakeBot, body };
@@ -11723,6 +11906,11 @@ async function hasPriorAssignmentRequest(repoFullName, issueNumber, login) {
11723
11906
  return false;
11724
11907
  }
11725
11908
  }
11909
+ function shouldRequestAssignment(claim, flags = {}) {
11910
+ const expectation = claim && claim.policy ? claim.policy.assignment : void 0;
11911
+ const post = Boolean(flags.assign) || expectation === void 0 || expectation === "required" || expectation === "take-bot";
11912
+ return { post, takeBot: expectation === "take-bot" };
11913
+ }
11726
11914
  async function requestIssueAssignment(claim, flags = {}, ghUser) {
11727
11915
  if (flags["no-assign"]) {
11728
11916
  console.log(" (--no-assign \u2014 skipping the assignment request; request it manually before working)");
@@ -11731,6 +11919,11 @@ async function requestIssueAssignment(claim, flags = {}, ghUser) {
11731
11919
  const parsed = parseGitHubUrl(claim.issueUrl);
11732
11920
  if (!parsed) return;
11733
11921
  const { repoFullName, number: number3 } = parsed;
11922
+ const expectation = shouldRequestAssignment(claim, flags);
11923
+ if (!expectation.post) {
11924
+ console.log(" (no assignment expectation found in repo docs \u2014 pass --assign to request assignment anyway)");
11925
+ return;
11926
+ }
11734
11927
  let login = ghUser;
11735
11928
  if (!login) {
11736
11929
  try {
@@ -11745,11 +11938,21 @@ async function requestIssueAssignment(claim, flags = {}, ghUser) {
11745
11938
  console.log(` \u2713 Already assigned to @${login} on ${repoFullName}#${number3}.`);
11746
11939
  return;
11747
11940
  }
11748
- const { usesTakeBot, body } = buildAssignmentComment(repoFullName);
11941
+ const { usesTakeBot, body } = buildAssignmentComment(repoFullName, { takeBot: expectation.takeBot });
11749
11942
  if (!usesTakeBot && await hasPriorAssignmentRequest(repoFullName, number3, login)) {
11750
11943
  console.log(` \u2713 Assignment already requested on ${repoFullName}#${number3}.`);
11751
11944
  return;
11752
11945
  }
11946
+ if (process.stdin.isTTY) {
11947
+ console.log(`
11948
+ About to comment on ${repoFullName}#${number3} as @${login}:`);
11949
+ for (const line of body.split("\n")) console.log(` ${line}`);
11950
+ const go = await confirm(" Post it? (y/N) ");
11951
+ if (!go) {
11952
+ console.log(" (skipped \u2014 request assignment manually before working)");
11953
+ return;
11954
+ }
11955
+ }
11753
11956
  try {
11754
11957
  await sh("gh", ["issue", "comment", String(number3), "--repo", repoFullName, "--body", body]);
11755
11958
  console.log(
@@ -11882,7 +12085,7 @@ function fmtContestedWarning(b) {
11882
12085
  async function cmdRecord(arg, flags = {}) {
11883
12086
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
11884
12087
  if (!arg) {
11885
- console.error("Usage: terminalhire claim record <bountyId|issueUrl> [--ack-policy] [--ack-contested]");
12088
+ console.error("Usage: terminalhire claim record <bountyId|issueUrl> [--ack-policy] [--ack-policy-prohibited] [--ack-contested]");
11886
12089
  console.error(" Run `terminalhire bounties` first to populate the local index cache,");
11887
12090
  console.error(" then pass the id shown in its output \u2014 or pass a GitHub issue URL directly.");
11888
12091
  process.exit(1);
@@ -11920,8 +12123,25 @@ ${contestedWarning}`);
11920
12123
  const policy = await checkRepoPolicy2(b.repoFullName);
11921
12124
  printPolicySection(policy);
11922
12125
  let ackedAt = null;
11923
- if (policy.status === "flagged" || policy.status === "unavailable") {
11924
- const reason = policy.status === "flagged" ? "This repo may prohibit AI-generated contributions" : "This repo's contribution policy could not be checked";
12126
+ if (policy.verdict === "prohibited") {
12127
+ let acked = Boolean(flags["ack-policy-prohibited"]);
12128
+ if (!acked && process.stdin.isTTY) {
12129
+ acked = await confirm(
12130
+ "\n This repo PROHIBITS AI-generated contributions \u2014 continuing means everything you submit must be hand-written by you. Continue? (y/N) "
12131
+ );
12132
+ }
12133
+ if (!acked) {
12134
+ console.error(
12135
+ `
12136
+ terminalhire claim: refusing to record \u2014 ${b.repoFullName} prohibits AI-generated contributions.
12137
+ Read the excerpts above. If you will hand-write the work yourself, re-run with
12138
+ --ack-policy-prohibited (or confirm interactively). --ack-policy is not enough here.`
12139
+ );
12140
+ process.exit(1);
12141
+ }
12142
+ ackedAt = (/* @__PURE__ */ new Date()).toISOString();
12143
+ } else if (policy.status === "flagged" || policy.status === "unavailable") {
12144
+ const reason = policy.status === "flagged" ? "This repo has AI-assistance policy language" : "This repo's contribution policy could not be checked";
11925
12145
  let acked = Boolean(flags["ack-policy"]);
11926
12146
  if (!acked && process.stdin.isTTY) {
11927
12147
  acked = await confirm(`
@@ -11951,7 +12171,18 @@ terminalhire claim: refusing to record \u2014 read ${b.repoFullName}'s contribut
11951
12171
  amountUSD: b.amountUSD,
11952
12172
  openPRsAtClaim: b.openPRs,
11953
12173
  kind,
11954
- policy: { status: policy.status, ackedAt }
12174
+ // Persist the full audit verdict (not just the coarse status) so `claim
12175
+ // start` can drive the assignment decision from it and `claim audit` can
12176
+ // show what ruleset the dev acknowledged. Excerpts are NOT persisted —
12177
+ // the record stores the conclusion; the docs stay the source of truth.
12178
+ policy: {
12179
+ status: policy.status,
12180
+ verdict: policy.verdict,
12181
+ assignment: policy.assignment,
12182
+ requirements: (policy.requirements ?? []).map((r) => r.kind),
12183
+ rulesetVersion: policy.rulesetVersion,
12184
+ ackedAt
12185
+ }
11955
12186
  });
11956
12187
  } catch (err) {
11957
12188
  console.error(`terminalhire claim: ${err.message ?? err}`);
@@ -12013,7 +12244,14 @@ async function cmdPreview(arg, { json } = {}) {
12013
12244
  openPRs: b.openPRs,
12014
12245
  assignees: b.assignees,
12015
12246
  contested: isContested(b),
12016
- policy: { status: policy.status, hits: policy.hits }
12247
+ policy: {
12248
+ status: policy.status,
12249
+ verdict: policy.verdict,
12250
+ assignment: policy.assignment,
12251
+ rulesetVersion: policy.rulesetVersion,
12252
+ hits: policy.hits,
12253
+ requirements: policy.requirements
12254
+ }
12017
12255
  }) + "\n"
12018
12256
  );
12019
12257
  return;
@@ -42681,66 +42919,6 @@ var init_jpi_init = __esm({
42681
42919
  }
42682
42920
  });
42683
42921
 
42684
- // bin/match-slots.js
42685
- function rotate(list, now) {
42686
- if (!Array.isArray(list) || list.length === 0) return [];
42687
- const idx = Math.floor(now / ROTATE_WINDOW_MS) % list.length;
42688
- return [...list.slice(idx), ...list.slice(0, idx)];
42689
- }
42690
- function budgetSlots(results, opts = {}) {
42691
- const {
42692
- thinProfile = false,
42693
- contributeEnabled = true,
42694
- totalSlots = TOTAL_SLOTS,
42695
- now = Date.now(),
42696
- interestPicks = [],
42697
- interestJobPicks = [],
42698
- interestJobSlots = INTEREST_JOB_SLOTS
42699
- } = opts;
42700
- const list = Array.isArray(results) ? results : [];
42701
- const bountyMatches = list.filter(
42702
- (r) => r && r.job && r.job.source === "bounty" && typeof r.score === "number" && r.score >= BOUNTY_MIN_MATCH
42703
- );
42704
- const bountyTop = [...bountyMatches].sort((a, b) => b.score - a.score).slice(0, BOUNTY_SLOTS);
42705
- const contributeMatches = list.filter((r) => r && r.job && r.job.source === "contribute");
42706
- const contributeCap = thinProfile && contributeEnabled ? CONTRIBUTE_SLOTS_THIN : CONTRIBUTE_SLOTS;
42707
- const contributeTop = rotate(contributeMatches, now).slice(0, contributeCap);
42708
- const reservedContributeIds = new Set(contributeTop.map((r) => r.job.id));
42709
- const interestTop = (Array.isArray(interestPicks) ? interestPicks : []).filter((r) => r && r.job && !reservedContributeIds.has(r.job.id)).slice(0, INTEREST_CONTRIBUTE_SLOTS).map((r) => ({ ...r, interestLabel: INTEREST_SLOT_LABEL }));
42710
- const surfacedIds = new Set(list.map((r) => r?.job?.id).filter(Boolean));
42711
- const interestJobTop = (Array.isArray(interestJobPicks) ? interestJobPicks : []).filter((r) => r && r.job && !surfacedIds.has(r.job.id)).slice(0, interestJobSlots).map((r) => ({ ...r, interestLabel: INTEREST_SLOT_LABEL }));
42712
- const roleSlots = Math.max(
42713
- 0,
42714
- totalSlots - bountyTop.length - contributeTop.length - interestTop.length - interestJobTop.length
42715
- );
42716
- const roleMatches = list.filter(
42717
- (r) => r && r.job && r.job.source !== "bounty" && r.job.source !== "contribute"
42718
- );
42719
- const roleStable = Math.min(ROLE_STABLE_MAX, roleSlots);
42720
- const stableRoles = roleMatches.slice(0, roleStable);
42721
- const rotableRoles = roleMatches.slice(roleStable, roleStable + ROLE_ROTATE_WINDOW);
42722
- const rotatedRoles = rotate(rotableRoles, now);
42723
- const roleTop = [...stableRoles, ...rotatedRoles].slice(0, roleSlots);
42724
- return { roleTop, bountyTop, contributeTop, interestTop, interestJobTop };
42725
- }
42726
- var TOTAL_SLOTS, BOUNTY_SLOTS, BOUNTY_MIN_MATCH, INTEREST_CONTRIBUTE_SLOTS, INTEREST_JOB_SLOTS, INTEREST_SLOT_LABEL, CONTRIBUTE_SLOTS, CONTRIBUTE_SLOTS_THIN, ROLE_STABLE_MAX, ROLE_ROTATE_WINDOW, ROTATE_WINDOW_MS;
42727
- var init_match_slots = __esm({
42728
- "bin/match-slots.js"() {
42729
- "use strict";
42730
- TOTAL_SLOTS = 25;
42731
- BOUNTY_SLOTS = 3;
42732
- BOUNTY_MIN_MATCH = 0.5;
42733
- INTEREST_CONTRIBUTE_SLOTS = 1;
42734
- INTEREST_JOB_SLOTS = 1;
42735
- INTEREST_SLOT_LABEL = "Stretch";
42736
- CONTRIBUTE_SLOTS = 5;
42737
- CONTRIBUTE_SLOTS_THIN = 8;
42738
- ROLE_STABLE_MAX = 8;
42739
- ROLE_ROTATE_WINDOW = 60;
42740
- ROTATE_WINDOW_MS = 5 * 60 * 1e3;
42741
- }
42742
- });
42743
-
42744
42922
  // bin/interest-picks.js
42745
42923
  function selectInterestPicks(contributeSupply, declaredTags, fpSkillTags, cap = INTEREST_PICKS_CAP) {
42746
42924
  if (!Array.isArray(contributeSupply) || contributeSupply.length === 0) return [];
@@ -8433,9 +8433,9 @@ var init_keytar = __esm({
8433
8433
  }
8434
8434
  });
8435
8435
 
8436
- // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
8436
+ // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node
8437
8437
  var require_keytar = __commonJS({
8438
- "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
8438
+ "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node"(exports, module) {
8439
8439
  "use strict";
8440
8440
  init_keytar();
8441
8441
  try {
@@ -9747,6 +9747,31 @@ function excludeOwnCard(results, ownLogin) {
9747
9747
  });
9748
9748
  }
9749
9749
 
9750
+ // bin/match-slots.js
9751
+ var ROTATE_WINDOW_MS = 5 * 60 * 1e3;
9752
+ var ROLE_HEAD_ROTATE_MS = 60 * 60 * 1e3;
9753
+ function rotate(list, now, windowMs = ROTATE_WINDOW_MS) {
9754
+ if (!Array.isArray(list) || list.length === 0) return [];
9755
+ const idx = Math.floor(now / windowMs) % list.length;
9756
+ return [...list.slice(idx), ...list.slice(0, idx)];
9757
+ }
9758
+
9759
+ // bin/directory-select.js
9760
+ var DEVS_STABLE_HEAD = 5;
9761
+ var DEVS_ROTATE_WINDOW = 15;
9762
+ var DEVS_ROTATE_MS = 5 * 60 * 1e3;
9763
+ function selectDirectoryDevs(results, { limit, now = Date.now() } = {}) {
9764
+ const list = Array.isArray(results) ? results : [];
9765
+ if (list.length === 0) return [];
9766
+ if (limit >= list.length) return list;
9767
+ const head = list.slice(0, Math.min(DEVS_STABLE_HEAD, limit));
9768
+ const tailPool = list.slice(head.length, head.length + DEVS_ROTATE_WINDOW);
9769
+ const tailSelected = rotate(tailPool, now, DEVS_ROTATE_MS).slice(0, limit - head.length);
9770
+ const rankOf = new Map(list.map((item, idx) => [item, idx]));
9771
+ const tail = [...tailSelected].sort((a, b) => rankOf.get(a) - rankOf.get(b));
9772
+ return [...head, ...tail];
9773
+ }
9774
+
9750
9775
  // bin/jpi-devs.js
9751
9776
  var API_URL4 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
9752
9777
  var DEFAULT_LIMIT3 = 10;
@@ -9780,7 +9805,7 @@ async function getDevs({ quiet = false, offline = false } = {}) {
9780
9805
  }
9781
9806
  const cards = index.cards ?? [];
9782
9807
  if (cards.length === 0) return { status: "no-cards", fp };
9783
- let results = match2(fp, cards, SHOW_ALL3 ? cards.length : LIMIT3);
9808
+ let results = match2(fp, cards, cards.length);
9784
9809
  let ownLogin;
9785
9810
  try {
9786
9811
  const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
@@ -9788,6 +9813,7 @@ async function getDevs({ quiet = false, offline = false } = {}) {
9788
9813
  } catch {
9789
9814
  }
9790
9815
  results = excludeOwnCard(results, ownLogin);
9816
+ if (!SHOW_ALL3) results = selectDirectoryDevs(results, { limit: LIMIT3 });
9791
9817
  if (results.length === 0) return { status: "no-matches", fp };
9792
9818
  return { status: "ok", results, fp };
9793
9819
  }
@@ -4071,9 +4071,9 @@ var init_keytar = __esm({
4071
4071
  }
4072
4072
  });
4073
4073
 
4074
- // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
4074
+ // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node
4075
4075
  var require_keytar = __commonJS({
4076
- "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
4076
+ "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node"(exports, module) {
4077
4077
  "use strict";
4078
4078
  init_keytar();
4079
4079
  try {
@@ -1089,9 +1089,9 @@ var init_keytar = __esm({
1089
1089
  }
1090
1090
  });
1091
1091
 
1092
- // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
1092
+ // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node
1093
1093
  var require_keytar = __commonJS({
1094
- "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
1094
+ "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node"(exports, module) {
1095
1095
  "use strict";
1096
1096
  init_keytar();
1097
1097
  try {
@@ -8062,9 +8062,9 @@ var init_keytar = __esm({
8062
8062
  }
8063
8063
  });
8064
8064
 
8065
- // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
8065
+ // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node
8066
8066
  var require_keytar = __commonJS({
8067
- "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
8067
+ "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node"(exports, module) {
8068
8068
  "use strict";
8069
8069
  init_keytar();
8070
8070
  try {
@@ -1301,9 +1301,9 @@ var init_keytar = __esm({
1301
1301
  }
1302
1302
  });
1303
1303
 
1304
- // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
1304
+ // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node
1305
1305
  var require_keytar = __commonJS({
1306
- "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
1306
+ "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node"(exports, module) {
1307
1307
  "use strict";
1308
1308
  init_keytar();
1309
1309
  try {
@@ -75,9 +75,9 @@ var init_keytar = __esm({
75
75
  }
76
76
  });
77
77
 
78
- // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
78
+ // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node
79
79
  var require_keytar = __commonJS({
80
- "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
80
+ "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node"(exports, module) {
81
81
  "use strict";
82
82
  init_keytar();
83
83
  try {