terminalhire 0.30.2 → 0.33.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/agent-a0bc554500876cd51/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/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node"(exports, module) {
51
51
  "use strict";
52
52
  init_keytar();
53
53
  try {
@@ -2026,7 +2026,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
2026
2026
  reason: buildReason(details)
2027
2027
  };
2028
2028
  });
2029
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
2029
+ return scored.filter((r) => r !== null && r.score >= (opts.minScore ?? MIN_SCORE)).sort((a, b) => {
2030
2030
  const byScore = b.score - a.score;
2031
2031
  if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
2032
2032
  const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
@@ -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 {
@@ -8353,11 +8353,134 @@ var init_profile = __esm({
8353
8353
  }
8354
8354
  });
8355
8355
 
8356
- // bin/jpi-bounties.js
8357
- init_src();
8358
- import { readFileSync as readFileSync4 } from "fs";
8356
+ // src/claims.ts
8357
+ var claims_exports = {};
8358
+ __export(claims_exports, {
8359
+ PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
8360
+ acceptedPRRate: () => acceptedPRRate,
8361
+ findClaim: () => findClaim,
8362
+ listClaims: () => listClaims,
8363
+ readClaims: () => readClaims,
8364
+ recordClaim: () => recordClaim,
8365
+ removeClaim: () => removeClaim,
8366
+ toPushedClaim: () => toPushedClaim,
8367
+ updateClaim: () => updateClaim
8368
+ });
8369
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, renameSync as renameSync2, existsSync as existsSync2 } from "fs";
8359
8370
  import { join as join4 } from "path";
8360
8371
  import { homedir as homedir3 } from "os";
8372
+ function toPushedClaim(claim) {
8373
+ return {
8374
+ kind: claim.kind,
8375
+ repoFullName: claim.repoFullName,
8376
+ state: claim.state,
8377
+ prUrl: claim.prUrl,
8378
+ merged: claim.state === "merged",
8379
+ claimedAt: claim.claimedAt,
8380
+ updatedAt: claim.updatedAt
8381
+ };
8382
+ }
8383
+ function nowISO() {
8384
+ return (/* @__PURE__ */ new Date()).toISOString();
8385
+ }
8386
+ function normalizeClaim(c) {
8387
+ return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
8388
+ }
8389
+ function readClaims() {
8390
+ try {
8391
+ if (!existsSync2(CLAIMS_FILE)) return [];
8392
+ const data = JSON.parse(readFileSync4(CLAIMS_FILE, "utf8"));
8393
+ const claims = Array.isArray(data?.claims) ? data.claims : [];
8394
+ return claims.map(normalizeClaim);
8395
+ } catch {
8396
+ return [];
8397
+ }
8398
+ }
8399
+ function writeClaims(claims) {
8400
+ mkdirSync3(TERMINALHIRE_DIR3, { recursive: true });
8401
+ const tmp = `${CLAIMS_FILE}.tmp`;
8402
+ const payload = { claims };
8403
+ writeFileSync3(tmp, JSON.stringify(payload, null, 2), "utf8");
8404
+ renameSync2(tmp, CLAIMS_FILE);
8405
+ }
8406
+ function findClaim(id) {
8407
+ return readClaims().find((c) => c.id === id) ?? null;
8408
+ }
8409
+ function listClaims(opts = {}) {
8410
+ const claims = readClaims();
8411
+ if (!opts.active) return claims;
8412
+ return claims.filter((c) => !TERMINAL_STATES.has(c.state));
8413
+ }
8414
+ function recordClaim(rec) {
8415
+ const claims = readClaims();
8416
+ if (claims.some((c) => c.id === rec.id)) {
8417
+ throw new Error(
8418
+ `claim already exists for '${rec.id}' \u2014 run 'terminalhire claim status ${rec.id}' or 'terminalhire claim release ${rec.id}'`
8419
+ );
8420
+ }
8421
+ const ts = nowISO();
8422
+ const claim = {
8423
+ ...rec,
8424
+ // Defensive default (mirrors normalizeClaim's `kind ?? 'bounty'` pattern):
8425
+ // a caller written before `policy` existed, or a plain-JS caller that skips
8426
+ // it, still produces a valid record instead of `policy: undefined`.
8427
+ policy: rec.policy ?? null,
8428
+ state: "claimed",
8429
+ worktreePath: null,
8430
+ branch: null,
8431
+ prUrl: null,
8432
+ review: null,
8433
+ claimedAt: ts,
8434
+ updatedAt: ts
8435
+ };
8436
+ claims.push(claim);
8437
+ writeClaims(claims);
8438
+ return claim;
8439
+ }
8440
+ function updateClaim(id, patch) {
8441
+ const claims = readClaims();
8442
+ const idx = claims.findIndex((c) => c.id === id);
8443
+ if (idx === -1) return null;
8444
+ claims[idx] = { ...claims[idx], ...patch, updatedAt: nowISO() };
8445
+ writeClaims(claims);
8446
+ return claims[idx];
8447
+ }
8448
+ function removeClaim(id) {
8449
+ const claims = readClaims();
8450
+ const next = claims.filter((c) => c.id !== id);
8451
+ if (next.length === claims.length) return false;
8452
+ writeClaims(next);
8453
+ return true;
8454
+ }
8455
+ function acceptedPRRate(claims = readClaims()) {
8456
+ const total = claims.length;
8457
+ const merged = claims.filter((c) => c.state === "merged").length;
8458
+ return { merged, total, rate: total === 0 ? 0 : merged / total };
8459
+ }
8460
+ var TERMINALHIRE_DIR3, CLAIMS_FILE, PUSHED_CLAIM_FIELDS, TERMINAL_STATES;
8461
+ var init_claims = __esm({
8462
+ "src/claims.ts"() {
8463
+ "use strict";
8464
+ TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
8465
+ CLAIMS_FILE = join4(TERMINALHIRE_DIR3, "claims.json");
8466
+ PUSHED_CLAIM_FIELDS = [
8467
+ "kind",
8468
+ "repoFullName",
8469
+ "state",
8470
+ "prUrl",
8471
+ "merged",
8472
+ "claimedAt",
8473
+ "updatedAt"
8474
+ ];
8475
+ TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
8476
+ }
8477
+ });
8478
+
8479
+ // bin/jpi-bounties.js
8480
+ init_src();
8481
+ import { readFileSync as readFileSync5 } from "fs";
8482
+ import { join as join5 } from "path";
8483
+ import { homedir as homedir4 } from "os";
8361
8484
  import { createInterface } from "readline";
8362
8485
 
8363
8486
  // bin/cache-store.js
@@ -8422,8 +8545,8 @@ function linkTitle(title, url) {
8422
8545
  }
8423
8546
 
8424
8547
  // bin/jpi-bounties.js
8425
- var TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
8426
- var INDEX_CACHE_FILE2 = join4(TERMINALHIRE_DIR3, "index-cache.json");
8548
+ var TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
8549
+ var INDEX_CACHE_FILE2 = join5(TERMINALHIRE_DIR4, "index-cache.json");
8427
8550
  var INDEX_TTL_MS = 15 * 60 * 1e3;
8428
8551
  var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
8429
8552
  var RANK_MODE = process.env["TERMINALHIRE_BOUNTY_RANK"] ?? "winnability";
@@ -8436,7 +8559,7 @@ var SHOW_ALL = args.includes("--all");
8436
8559
  var WINNABLE_ONLY = args.includes("--winnable");
8437
8560
  function readIndexCache() {
8438
8561
  try {
8439
- const entry = JSON.parse(readFileSync4(INDEX_CACHE_FILE2, "utf8"));
8562
+ const entry = JSON.parse(readFileSync5(INDEX_CACHE_FILE2, "utf8"));
8440
8563
  if (Date.now() - entry.ts < INDEX_TTL_MS) return entry.index;
8441
8564
  return null;
8442
8565
  } catch {
@@ -8468,22 +8591,26 @@ function formatAmount(b) {
8468
8591
  return b.amountUSD != null ? "$" + b.amountUSD.toLocaleString() : "$\u2014";
8469
8592
  }
8470
8593
  var EFFORT_LABEL = { small: "small (~\xBD day)", medium: "medium (~1 day)", large: "large (multi-day)" };
8471
- function printBounty(i, job, score, reason, matchedTags) {
8594
+ function printBounty(i, job, score, reason, matchedTags, claimedIds = /* @__PURE__ */ new Set()) {
8472
8595
  const b = job.bounty ?? {};
8473
8596
  const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
8474
8597
  const effort = b.estimatedEffort ? ` \xB7 ${EFFORT_LABEL[b.estimatedEffort]}` : "";
8475
8598
  const scoreStr = score > 0 ? ` \xB7 match ${Math.round(score * 100)}%` : "";
8476
8599
  const prs = b.competingOpenPRs;
8477
8600
  const contend = prs != null && prs > 0 ? ` \xB7 \u26A0 ${prs} PR${prs === 1 ? "" : "s"} in flight` : "";
8601
+ const claimed = claimedIds.has(job.id);
8602
+ const badge = claimed ? " \xB7 \u25CF claimed by you" : "";
8478
8603
  const ref = opportunityShortToken(job.id);
8479
8604
  console.log(`
8480
8605
  ${i + 1}. ${linkTitle(job.title, job.url)} [${ref}]`);
8481
- console.log(` ${formatAmount(b)}${effort} \xB7 ${sanitizeText(b.repoFullName ?? job.company)}${stars}${scoreStr}${contend}`);
8606
+ console.log(` ${formatAmount(b)}${effort} \xB7 ${sanitizeText(b.repoFullName ?? job.company)}${stars}${scoreStr}${contend}${badge}`);
8482
8607
  if (reason) console.log(` ${reason}`);
8483
8608
  if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
8484
8609
  console.log(` id: ${job.id}`);
8485
8610
  console.log(` Claim: ${sanitizeText(b.claimUrl ?? job.url)}`);
8486
- console.log(` \u2192 terminalhire claim ${ref}`);
8611
+ console.log(
8612
+ claimed ? ` \u2192 claimed by you \u2014 terminalhire claim status ${job.id}` : ` \u2192 terminalhire claim ${ref}`
8613
+ );
8487
8614
  }
8488
8615
  function rankBounties(bounties, { rankMode = "winnability", scoreOf = () => 0 } = {}) {
8489
8616
  const legacy = rankMode === "legacy";
@@ -8561,6 +8688,12 @@ async function run() {
8561
8688
  if (result.status !== "ok") return;
8562
8689
  const { bounties, ranked, matchedCount } = result;
8563
8690
  const shown = SHOW_ALL ? bounties : bounties.slice(0, LIMIT);
8691
+ let claimedIds = /* @__PURE__ */ new Set();
8692
+ try {
8693
+ const { listClaims: listClaims2 } = await Promise.resolve().then(() => (init_claims(), claims_exports));
8694
+ claimedIds = new Set(listClaims2({ active: true }).map((c) => c.id));
8695
+ } catch {
8696
+ }
8564
8697
  console.log(
8565
8698
  `
8566
8699
  \u26A1 ${bounties.length} bount${bounties.length === 1 ? "y" : "ies"} you could knock out` + (matchedCount ? ` \u2014 ${matchedCount} matched to your profile` : "") + ` (local rank \u2014 no data sent)
@@ -8568,7 +8701,7 @@ async function run() {
8568
8701
  );
8569
8702
  for (let i = 0; i < shown.length; i++) {
8570
8703
  const r = ranked.get(shown[i].id);
8571
- printBounty(i, shown[i], r?.score ?? 0, r?.reason, r?.matchedTags);
8704
+ printBounty(i, shown[i], r?.score ?? 0, r?.reason, r?.matchedTags, claimedIds);
8572
8705
  }
8573
8706
  if (!SHOW_ALL && bounties.length > shown.length) {
8574
8707
  console.log(`
@@ -8595,6 +8728,7 @@ export {
8595
8728
  classifyEmptyStatus,
8596
8729
  filterPaidVisibility,
8597
8730
  getBounties,
8731
+ printBounty,
8598
8732
  rankBounties,
8599
8733
  run
8600
8734
  };
@@ -4043,9 +4043,9 @@ var init_keytar = __esm({
4043
4043
  }
4044
4044
  });
4045
4045
 
4046
- // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
4046
+ // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node
4047
4047
  var require_keytar = __commonJS({
4048
- "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
4048
+ "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node"(exports, module) {
4049
4049
  "use strict";
4050
4050
  init_keytar();
4051
4051
  try {
@@ -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 {
@@ -2026,7 +2026,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
2026
2026
  reason: buildReason(details)
2027
2027
  };
2028
2028
  });
2029
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
2029
+ return scored.filter((r) => r !== null && r.score >= (opts.minScore ?? MIN_SCORE)).sort((a, b) => {
2030
2030
  const byScore = b.score - a.score;
2031
2031
  if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
2032
2032
  const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
@@ -8185,9 +8185,9 @@ var init_keytar = __esm({
8185
8185
  }
8186
8186
  });
8187
8187
 
8188
- // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
8188
+ // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node
8189
8189
  var require_keytar = __commonJS({
8190
- "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
8190
+ "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node"(exports, module) {
8191
8191
  "use strict";
8192
8192
  init_keytar();
8193
8193
  try {
@@ -8239,6 +8239,8 @@ var require_keytar2 = __commonJS({
8239
8239
  // src/repo-policy.ts
8240
8240
  var repo_policy_exports = {};
8241
8241
  __export(repo_policy_exports, {
8242
+ POLICY_RULESET_VERSION: () => POLICY_RULESET_VERSION,
8243
+ auditContent: () => auditContent,
8242
8244
  checkRepoPolicy: () => checkRepoPolicy
8243
8245
  });
8244
8246
  async function fetchContentsFile(fetchImpl, repoFullName, path) {
@@ -8260,23 +8262,50 @@ async function fetchContentsFile(fetchImpl, repoFullName, path) {
8260
8262
  return { ok: false, missing: false, content: null };
8261
8263
  }
8262
8264
  }
8263
- function findHits(file, content) {
8264
- const lines = content.split("\n");
8265
+ function classifyLine(line) {
8266
+ if (PROHIBITED_PATTERNS.some((re) => re.test(line))) return "prohibited";
8267
+ if (DISCLOSURE_PATTERNS.some((re) => re.test(line))) return "disclosure-required";
8268
+ if (AI_SIGNAL_PATTERNS.some((p) => p.re.test(line))) return "ai-mentioned";
8269
+ return null;
8270
+ }
8271
+ function sanitizeExcerpt(text) {
8272
+ return text.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, "");
8273
+ }
8274
+ function excerptAround(lines, i) {
8275
+ const start = Math.max(0, i - 2);
8276
+ const end = Math.min(lines.length, i + 3);
8277
+ return sanitizeExcerpt(lines.slice(start, end).join("\n"));
8278
+ }
8279
+ function auditContent(files) {
8265
8280
  const hits = [];
8266
- for (let i = 0; i < lines.length; i++) {
8267
- const line = lines[i];
8268
- if (!AI_SIGNAL_PATTERNS.some((p) => p.re.test(line))) continue;
8269
- const start = Math.max(0, i - 2);
8270
- const end = Math.min(lines.length, i + 3);
8271
- hits.push({ file, excerpt: lines.slice(start, end).join("\n") });
8281
+ const requirements = [];
8282
+ const seenKinds = /* @__PURE__ */ new Set();
8283
+ for (const { file, content } of files) {
8284
+ const lines = content.split("\n");
8285
+ for (let i = 0; i < lines.length; i++) {
8286
+ const rule = classifyLine(lines[i]);
8287
+ if (rule) hits.push({ file, excerpt: excerptAround(lines, i), rule });
8288
+ for (const { kind, re } of REQUIREMENT_PATTERNS) {
8289
+ if (seenKinds.has(kind) || !re.test(lines[i])) continue;
8290
+ seenKinds.add(kind);
8291
+ requirements.push({ kind, file, excerpt: excerptAround(lines, i) });
8292
+ }
8293
+ }
8272
8294
  }
8273
- return hits;
8295
+ let verdict = "clean";
8296
+ for (const h of hits) {
8297
+ if (verdict === "clean" || VERDICT_SEVERITY[h.rule] > VERDICT_SEVERITY[verdict]) {
8298
+ verdict = h.rule;
8299
+ }
8300
+ }
8301
+ const assignment = seenKinds.has("take-bot") ? "take-bot" : seenKinds.has("assignment-required") ? "required" : "none";
8302
+ return { hits, requirements, verdict, assignment };
8274
8303
  }
8275
8304
  async function checkRepoPolicy(repoFullName, opts = {}) {
8276
8305
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
8277
8306
  let requestsUsed = 0;
8278
8307
  let hadError = false;
8279
- const hits = [];
8308
+ const files = [];
8280
8309
  outer: for (const group of CANDIDATE_GROUPS) {
8281
8310
  for (const path of group) {
8282
8311
  if (requestsUsed >= MAX_REQUESTS) break outer;
@@ -8287,21 +8316,27 @@ async function checkRepoPolicy(repoFullName, opts = {}) {
8287
8316
  continue;
8288
8317
  }
8289
8318
  if (outcome.missing) continue;
8290
- if (outcome.content) hits.push(...findHits(path, outcome.content));
8319
+ if (outcome.content) files.push({ file: path, content: outcome.content });
8291
8320
  break;
8292
8321
  }
8293
8322
  }
8294
- if (hits.length > 0) return { status: "flagged", hits };
8295
- if (hadError) return { status: "unavailable", hits: [] };
8296
- return { status: "clean", hits: [] };
8323
+ const { hits, requirements, verdict, assignment } = auditContent(files);
8324
+ if (hits.length > 0) {
8325
+ return { status: "flagged", verdict, hits, requirements, assignment, rulesetVersion: POLICY_RULESET_VERSION };
8326
+ }
8327
+ if (hadError) {
8328
+ return { status: "unavailable", verdict: "unavailable", hits: [], requirements, assignment, rulesetVersion: POLICY_RULESET_VERSION };
8329
+ }
8330
+ return { status: "clean", verdict: "clean", hits: [], requirements, assignment, rulesetVersion: POLICY_RULESET_VERSION };
8297
8331
  }
8298
- var GH_API, GH_HEADERS, MAX_REQUESTS, AI_SIGNAL_PATTERNS, CANDIDATE_GROUPS;
8332
+ 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;
8299
8333
  var init_repo_policy = __esm({
8300
8334
  "src/repo-policy.ts"() {
8301
8335
  "use strict";
8302
8336
  GH_API = "https://api.github.com";
8303
8337
  GH_HEADERS = { "User-Agent": "terminalhire-claim", Accept: "application/vnd.github+json" };
8304
8338
  MAX_REQUESTS = 4;
8339
+ POLICY_RULESET_VERSION = 2;
8305
8340
  AI_SIGNAL_PATTERNS = [
8306
8341
  { label: "AI", re: /\bAI\b/i },
8307
8342
  { label: "artificial intelligence", re: /artificial intelligence/i },
@@ -8313,11 +8348,47 @@ var init_repo_policy = __esm({
8313
8348
  { label: "generative", re: /\bgenerative\b/i },
8314
8349
  { label: "machine-generated", re: /machine[\s-]generated/i }
8315
8350
  ];
8351
+ AI_TERM = "(?:ai|llms?|generative(?:\\s+ai)?|artificial intelligence|language models?|copilot|chatgpt|claude|machine[\\s-]generated)";
8352
+ PROHIBITED_PATTERNS = [
8353
+ new RegExp(`prohibit\\w*[^.\\n]{0,60}\\b${AI_TERM}`, "i"),
8354
+ /did not write the code yourself/i,
8355
+ new RegExp(`(?:not|never|don'?t|won'?t)\\s+accept\\w*[^.\\n]{0,60}\\b${AI_TERM}`, "i"),
8356
+ new RegExp(`\\bno\\s+${AI_TERM}[^.\\n]{0,40}\\b(?:prs?|pull requests?|contributions?|code|patch(?:es)?|commits?|submissions?)\\b`, "i"),
8357
+ new RegExp(
8358
+ `\\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)`,
8359
+ "i"
8360
+ )
8361
+ ];
8362
+ DISCLOSURE_PATTERNS = [
8363
+ new RegExp(`disclos\\w+[^.\\n]{0,60}\\b${AI_TERM}`, "i"),
8364
+ new RegExp(`\\b${AI_TERM}[^.\\n]{0,60}disclos`, "i"),
8365
+ new RegExp(`\\b${AI_TERM}[\\s-]assist\\w*[^.\\n]{0,60}\\b(?:must|should|required?)\\b`, "i"),
8366
+ // PR-template checkbox, e.g. "- [ ] I used AI tools and have reviewed the output"
8367
+ new RegExp(`\\[ \\][^\\n]{0,80}\\b${AI_TERM}`, "i")
8368
+ ];
8369
+ REQUIREMENT_PATTERNS = [
8370
+ // `/take` bot first: its docs usually also say "assign", and the bot is the
8371
+ // more specific expectation (post exactly `/take`, not a prose request).
8372
+ { kind: "take-bot", re: /(?:^|[\s`"'(])\/take\b/m },
8373
+ { kind: "assignment-required", re: /(?:request|ask|wait)[^.\n]{0,40}\bassign/i },
8374
+ { kind: "assignment-required", re: /\bassigned before\b/i },
8375
+ { kind: "assignment-required", re: /\bself[\s-]assign/i },
8376
+ { kind: "assignment-required", re: /do not (?:open|submit)[^.\n]{0,40}\b(?:prs?|pull requests?)\b/i },
8377
+ { kind: "cla-required", re: /\bCLA\b/ },
8378
+ { kind: "cla-required", re: /contributor licen[cs]e agreement/i },
8379
+ { kind: "discussion-first", re: /open an issue (?:first|before)/i },
8380
+ { kind: "discussion-first", re: /discuss\w*[^.\n]{0,40}\bbefore\b/i }
8381
+ ];
8316
8382
  CANDIDATE_GROUPS = [
8317
8383
  ["CONTRIBUTING.md", ".github/CONTRIBUTING.md", "docs/CONTRIBUTING.md"],
8318
- [".github/PULL_REQUEST_TEMPLATE.md", "PULL_REQUEST_TEMPLATE.md"],
8319
- ["AGENTS.md"]
8384
+ ["AGENTS.md", "AGENTS.MD"],
8385
+ [".github/PULL_REQUEST_TEMPLATE.md", "PULL_REQUEST_TEMPLATE.md"]
8320
8386
  ];
8387
+ VERDICT_SEVERITY = {
8388
+ "ai-mentioned": 1,
8389
+ "disclosure-required": 2,
8390
+ prohibited: 3
8391
+ };
8321
8392
  }
8322
8393
  });
8323
8394
 
@@ -9010,17 +9081,38 @@ function buildSubmitBody(issueNumber, opts = {}) {
9010
9081
  return `${closesLine}${main}${AI_DISCLOSURE_NOTE}`;
9011
9082
  }
9012
9083
  function printPolicySection(policy) {
9013
- if (policy.status === "flagged") {
9084
+ const verdict = policy.verdict ?? (policy.status === "flagged" ? "ai-mentioned" : policy.status);
9085
+ if (verdict === "prohibited") {
9086
+ console.log("\n POLICY: \u2717 this repo PROHIBITS AI-generated contributions \u2014 READ BEFORE CLAIMING:");
9087
+ } else if (verdict === "disclosure-required") {
9088
+ console.log("\n POLICY: \u26A0 this repo requires disclosing AI assistance \u2014 READ BEFORE WORKING:");
9089
+ } else if (verdict === "ai-mentioned") {
9014
9090
  console.log("\n POLICY: \u26A0 possible AI-assistance policy language found \u2014 READ BEFORE WORKING:");
9015
- for (const hit of policy.hits) {
9016
- console.log(` [${hit.file}]`);
9017
- for (const line of hit.excerpt.split("\n")) console.log(` ${line}`);
9018
- }
9019
9091
  } else if (policy.status === "unavailable") {
9020
9092
  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.");
9021
9093
  } else {
9022
9094
  console.log(" policy: no AI-assistance policy language detected in CONTRIBUTING/PR-template/AGENTS docs");
9023
9095
  }
9096
+ for (const hit of policy.hits ?? []) {
9097
+ console.log(` [${hit.file}]`);
9098
+ for (const line of hit.excerpt.split("\n")) console.log(` ${line}`);
9099
+ }
9100
+ const requirements = policy.requirements ?? [];
9101
+ if (requirements.length > 0) {
9102
+ console.log("\n REQUIREMENTS: this repo sets contribution expectations \u2014 in its own words:");
9103
+ for (const req of requirements) {
9104
+ console.log(` ${req.kind} [${req.file}]`);
9105
+ for (const line of req.excerpt.split("\n")) console.log(` ${line}`);
9106
+ }
9107
+ }
9108
+ const assignment = policy.assignment ?? "none";
9109
+ if (assignment === "take-bot") {
9110
+ console.log(" assignment: repo self-assigns via a /take comment \u2014 `claim start` will post `/take` on the issue");
9111
+ } else if (assignment === "required") {
9112
+ console.log(" assignment: repo expects you to request assignment \u2014 `claim start` will post a request comment on the issue");
9113
+ } else {
9114
+ console.log(" assignment: no assignment expectation found in repo docs \u2014 `claim start` will not comment (pass --assign to request anyway)");
9115
+ }
9024
9116
  }
9025
9117
  var pExecFile = promisify2(execFile2);
9026
9118
  async function sh(cmd, args, opts = {}) {
@@ -9291,9 +9383,9 @@ async function fetchIssue(repoFullName, issueNumber) {
9291
9383
  }
9292
9384
  var ASSIGNMENT_MARKER = "<!-- terminalhire:assignment-request -->";
9293
9385
  var TAKE_BOT_REPOS = /* @__PURE__ */ new Set(["paradedb/paradedb"]);
9294
- function buildAssignmentComment(repoFullName) {
9295
- const usesTakeBot = TAKE_BOT_REPOS.has(repoFullName.toLowerCase());
9296
- const body = usesTakeBot ? "/take" : `I'd like to work on this \u2014 could I be assigned? Thanks!
9386
+ function buildAssignmentComment(repoFullName, opts = {}) {
9387
+ const usesTakeBot = TAKE_BOT_REPOS.has(repoFullName.toLowerCase()) || Boolean(opts.takeBot);
9388
+ const body = usesTakeBot ? "/take" : `Hi! I'd like to work on this issue. Could you assign it to me? Thanks!
9297
9389
 
9298
9390
  ${ASSIGNMENT_MARKER}`;
9299
9391
  return { usesTakeBot, body };
@@ -9314,6 +9406,11 @@ async function hasPriorAssignmentRequest(repoFullName, issueNumber, login) {
9314
9406
  return false;
9315
9407
  }
9316
9408
  }
9409
+ function shouldRequestAssignment(claim, flags = {}) {
9410
+ const expectation = claim && claim.policy ? claim.policy.assignment : void 0;
9411
+ const post = Boolean(flags.assign) || expectation === void 0 || expectation === "required" || expectation === "take-bot";
9412
+ return { post, takeBot: expectation === "take-bot" };
9413
+ }
9317
9414
  async function requestIssueAssignment(claim, flags = {}, ghUser) {
9318
9415
  if (flags["no-assign"]) {
9319
9416
  console.log(" (--no-assign \u2014 skipping the assignment request; request it manually before working)");
@@ -9322,6 +9419,11 @@ async function requestIssueAssignment(claim, flags = {}, ghUser) {
9322
9419
  const parsed = parseGitHubUrl(claim.issueUrl);
9323
9420
  if (!parsed) return;
9324
9421
  const { repoFullName, number } = parsed;
9422
+ const expectation = shouldRequestAssignment(claim, flags);
9423
+ if (!expectation.post) {
9424
+ console.log(" (no assignment expectation found in repo docs \u2014 pass --assign to request assignment anyway)");
9425
+ return;
9426
+ }
9325
9427
  let login = ghUser;
9326
9428
  if (!login) {
9327
9429
  try {
@@ -9336,11 +9438,21 @@ async function requestIssueAssignment(claim, flags = {}, ghUser) {
9336
9438
  console.log(` \u2713 Already assigned to @${login} on ${repoFullName}#${number}.`);
9337
9439
  return;
9338
9440
  }
9339
- const { usesTakeBot, body } = buildAssignmentComment(repoFullName);
9441
+ const { usesTakeBot, body } = buildAssignmentComment(repoFullName, { takeBot: expectation.takeBot });
9340
9442
  if (!usesTakeBot && await hasPriorAssignmentRequest(repoFullName, number, login)) {
9341
9443
  console.log(` \u2713 Assignment already requested on ${repoFullName}#${number}.`);
9342
9444
  return;
9343
9445
  }
9446
+ if (process.stdin.isTTY) {
9447
+ console.log(`
9448
+ About to comment on ${repoFullName}#${number} as @${login}:`);
9449
+ for (const line of body.split("\n")) console.log(` ${line}`);
9450
+ const go = await confirm(" Post it? (y/N) ");
9451
+ if (!go) {
9452
+ console.log(" (skipped \u2014 request assignment manually before working)");
9453
+ return;
9454
+ }
9455
+ }
9344
9456
  try {
9345
9457
  await sh("gh", ["issue", "comment", String(number), "--repo", repoFullName, "--body", body]);
9346
9458
  console.log(
@@ -9473,7 +9585,7 @@ function fmtContestedWarning(b) {
9473
9585
  async function cmdRecord(arg, flags = {}) {
9474
9586
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
9475
9587
  if (!arg) {
9476
- console.error("Usage: terminalhire claim record <bountyId|issueUrl> [--ack-policy] [--ack-contested]");
9588
+ console.error("Usage: terminalhire claim record <bountyId|issueUrl> [--ack-policy] [--ack-policy-prohibited] [--ack-contested]");
9477
9589
  console.error(" Run `terminalhire bounties` first to populate the local index cache,");
9478
9590
  console.error(" then pass the id shown in its output \u2014 or pass a GitHub issue URL directly.");
9479
9591
  process.exit(1);
@@ -9511,8 +9623,25 @@ ${contestedWarning}`);
9511
9623
  const policy = await checkRepoPolicy2(b.repoFullName);
9512
9624
  printPolicySection(policy);
9513
9625
  let ackedAt = null;
9514
- if (policy.status === "flagged" || policy.status === "unavailable") {
9515
- const reason = policy.status === "flagged" ? "This repo may prohibit AI-generated contributions" : "This repo's contribution policy could not be checked";
9626
+ if (policy.verdict === "prohibited") {
9627
+ let acked = Boolean(flags["ack-policy-prohibited"]);
9628
+ if (!acked && process.stdin.isTTY) {
9629
+ acked = await confirm(
9630
+ "\n This repo PROHIBITS AI-generated contributions \u2014 continuing means everything you submit must be hand-written by you. Continue? (y/N) "
9631
+ );
9632
+ }
9633
+ if (!acked) {
9634
+ console.error(
9635
+ `
9636
+ terminalhire claim: refusing to record \u2014 ${b.repoFullName} prohibits AI-generated contributions.
9637
+ Read the excerpts above. If you will hand-write the work yourself, re-run with
9638
+ --ack-policy-prohibited (or confirm interactively). --ack-policy is not enough here.`
9639
+ );
9640
+ process.exit(1);
9641
+ }
9642
+ ackedAt = (/* @__PURE__ */ new Date()).toISOString();
9643
+ } else if (policy.status === "flagged" || policy.status === "unavailable") {
9644
+ const reason = policy.status === "flagged" ? "This repo has AI-assistance policy language" : "This repo's contribution policy could not be checked";
9516
9645
  let acked = Boolean(flags["ack-policy"]);
9517
9646
  if (!acked && process.stdin.isTTY) {
9518
9647
  acked = await confirm(`
@@ -9542,7 +9671,18 @@ terminalhire claim: refusing to record \u2014 read ${b.repoFullName}'s contribut
9542
9671
  amountUSD: b.amountUSD,
9543
9672
  openPRsAtClaim: b.openPRs,
9544
9673
  kind,
9545
- policy: { status: policy.status, ackedAt }
9674
+ // Persist the full audit verdict (not just the coarse status) so `claim
9675
+ // start` can drive the assignment decision from it and `claim audit` can
9676
+ // show what ruleset the dev acknowledged. Excerpts are NOT persisted —
9677
+ // the record stores the conclusion; the docs stay the source of truth.
9678
+ policy: {
9679
+ status: policy.status,
9680
+ verdict: policy.verdict,
9681
+ assignment: policy.assignment,
9682
+ requirements: (policy.requirements ?? []).map((r) => r.kind),
9683
+ rulesetVersion: policy.rulesetVersion,
9684
+ ackedAt
9685
+ }
9546
9686
  });
9547
9687
  } catch (err) {
9548
9688
  console.error(`terminalhire claim: ${err.message ?? err}`);
@@ -9604,7 +9744,14 @@ async function cmdPreview(arg, { json } = {}) {
9604
9744
  openPRs: b.openPRs,
9605
9745
  assignees: b.assignees,
9606
9746
  contested: isContested(b),
9607
- policy: { status: policy.status, hits: policy.hits }
9747
+ policy: {
9748
+ status: policy.status,
9749
+ verdict: policy.verdict,
9750
+ assignment: policy.assignment,
9751
+ rulesetVersion: policy.rulesetVersion,
9752
+ hits: policy.hits,
9753
+ requirements: policy.requirements
9754
+ }
9608
9755
  }) + "\n"
9609
9756
  );
9610
9757
  return;
@@ -10736,6 +10883,7 @@ export {
10736
10883
  run,
10737
10884
  selectCompetingPrs,
10738
10885
  selectPushRemote,
10886
+ shouldRequestAssignment,
10739
10887
  startBranchFor,
10740
10888
  workDirFor
10741
10889
  };