terminalhire 0.27.0 → 0.28.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.
@@ -8882,6 +8882,10 @@ ${p.body ?? ""}`)) {
8882
8882
  }
8883
8883
  return matched;
8884
8884
  }
8885
+ function selectCompetingPrs(prs, selfLogin) {
8886
+ if (!Array.isArray(prs)) return [];
8887
+ return prs.filter((pr) => pr && pr.author !== selfLogin);
8888
+ }
8885
8889
  async function listOpenPRsReferencingIssue(repoFullName, issueNumber) {
8886
8890
  try {
8887
8891
  const res = await fetch(`${GH_API2}/repos/${repoFullName}/pulls?state=open&per_page=100`, {
@@ -8947,6 +8951,67 @@ async function fetchIssue(repoFullName, issueNumber) {
8947
8951
  return null;
8948
8952
  }
8949
8953
  }
8954
+ var ASSIGNMENT_MARKER = "<!-- terminalhire:assignment-request -->";
8955
+ var TAKE_BOT_REPOS = /* @__PURE__ */ new Set(["paradedb/paradedb"]);
8956
+ function buildAssignmentComment(repoFullName) {
8957
+ const usesTakeBot = TAKE_BOT_REPOS.has(repoFullName.toLowerCase());
8958
+ const body = usesTakeBot ? "/take" : `I'd like to work on this \u2014 could I be assigned? Thanks!
8959
+
8960
+ ${ASSIGNMENT_MARKER}`;
8961
+ return { usesTakeBot, body };
8962
+ }
8963
+ async function hasPriorAssignmentRequest(repoFullName, issueNumber, login) {
8964
+ try {
8965
+ const res = await fetch(
8966
+ `${GH_API2}/repos/${repoFullName}/issues/${issueNumber}/comments?per_page=100&sort=created&direction=desc`,
8967
+ { headers: GH_HEADERS2, signal: AbortSignal.timeout(1e4) }
8968
+ );
8969
+ if (!res.ok) return false;
8970
+ const comments = await res.json();
8971
+ if (!Array.isArray(comments)) return false;
8972
+ return comments.some(
8973
+ (c) => c && c.user && c.user.login === login && typeof c.body === "string" && c.body.includes(ASSIGNMENT_MARKER)
8974
+ );
8975
+ } catch {
8976
+ return false;
8977
+ }
8978
+ }
8979
+ async function requestIssueAssignment(claim, flags = {}, ghUser) {
8980
+ if (flags["no-assign"]) {
8981
+ console.log(" (--no-assign \u2014 skipping the assignment request; request it manually before working)");
8982
+ return;
8983
+ }
8984
+ const parsed = parseGitHubUrl(claim.issueUrl);
8985
+ if (!parsed) return;
8986
+ const { repoFullName, number } = parsed;
8987
+ let login = ghUser;
8988
+ if (!login) {
8989
+ try {
8990
+ login = await sh("gh", ["api", "user", "-q", ".login"]);
8991
+ } catch {
8992
+ console.log(" (assignment not requested: 'gh' not authenticated \u2014 comment on the issue manually before working)");
8993
+ return;
8994
+ }
8995
+ }
8996
+ const issue = await fetchIssue(repoFullName, number);
8997
+ if (issue && issue.assignees.includes(login)) {
8998
+ console.log(` \u2713 Already assigned to @${login} on ${repoFullName}#${number}.`);
8999
+ return;
9000
+ }
9001
+ const { usesTakeBot, body } = buildAssignmentComment(repoFullName);
9002
+ if (!usesTakeBot && await hasPriorAssignmentRequest(repoFullName, number, login)) {
9003
+ console.log(` \u2713 Assignment already requested on ${repoFullName}#${number}.`);
9004
+ return;
9005
+ }
9006
+ try {
9007
+ await sh("gh", ["issue", "comment", String(number), "--repo", repoFullName, "--body", body]);
9008
+ console.log(
9009
+ usesTakeBot ? ` \u2713 Requested assignment via /take on ${repoFullName}#${number}.` : ` \u2713 Requested assignment on ${repoFullName}#${number}.`
9010
+ );
9011
+ } catch (err) {
9012
+ console.log(` (could not post the assignment request: ${err.stderr || err.message || err} \u2014 do it manually before working)`);
9013
+ }
9014
+ }
8950
9015
  async function pollPR(prUrl) {
8951
9016
  const p = parseGitHubUrl(prUrl);
8952
9017
  if (!p) return null;
@@ -9436,7 +9501,7 @@ When it's done: terminalhire claim submit ${id}`);
9436
9501
  }
9437
9502
  }
9438
9503
  if (flags.here) {
9439
- await cmdStartHere(claims, claim);
9504
+ await cmdStartHere(claims, claim, flags);
9440
9505
  return;
9441
9506
  }
9442
9507
  let ghUser;
@@ -9498,6 +9563,7 @@ terminalhire claim: not started \u2014 starting forks ${claim.repoFullName} to y
9498
9563
  await sh("git", ["-C", destDir, "checkout", "-b", branch]);
9499
9564
  const toplevel = await sh("git", ["-C", destDir, "rev-parse", "--show-toplevel"]);
9500
9565
  claims.updateClaim(id, { worktreePath: toplevel, branch, state: "working" });
9566
+ await requestIssueAssignment(claim, flags, ghUser);
9501
9567
  console.log(`
9502
9568
  \u2713 Started: ${claim.title}`);
9503
9569
  console.log(` fork: ${forkFullName}`);
@@ -9507,7 +9573,7 @@ terminalhire claim: not started \u2014 starting forks ${claim.repoFullName} to y
9507
9573
  console.log("\n When the work is done (the only step that pushes + opens the PR):");
9508
9574
  console.log(` terminalhire claim submit ${id}`);
9509
9575
  }
9510
- async function cmdStartHere(claims, claim) {
9576
+ async function cmdStartHere(claims, claim, flags = {}) {
9511
9577
  let toplevel;
9512
9578
  try {
9513
9579
  toplevel = await sh("git", ["-C", process.cwd(), "rev-parse", "--show-toplevel"]);
@@ -9537,6 +9603,7 @@ async function cmdStartHere(claims, claim) {
9537
9603
  const branch = startBranchFor(claim.repoFullName, issueNumber);
9538
9604
  await sh("git", ["-C", toplevel, "checkout", "-b", branch]);
9539
9605
  claims.updateClaim(claim.id, { worktreePath: toplevel, branch, state: "working" });
9606
+ await requestIssueAssignment(claim, flags);
9540
9607
  console.log(`
9541
9608
  \u2713 Started here: ${claim.title}`);
9542
9609
  console.log(` worktree: ${toplevel}`);
@@ -9768,10 +9835,22 @@ async function cmdSubmit(id, flags = {}) {
9768
9835
  const issueNo = (parseGitHubUrl(claim.issueUrl) || {}).number;
9769
9836
  const contention = issueNo ? await listOpenPRsReferencingIssue(claim.repoFullName, issueNo) : null;
9770
9837
  if (contention && contention.total > 0) {
9771
- console.log(` \u26A0 contention: ${contention.total} open PR(s) already reference this issue:`);
9772
9838
  const nowMs = Date.now();
9839
+ console.log(` \u26A0 contention: ${contention.total} open PR(s) already reference this issue:`);
9773
9840
  for (const pr of contention.prs) console.log(fmtContentionPr(pr, nowMs, false));
9774
9841
  console.log(CONTENTION_HINT);
9842
+ const competing = selectCompetingPrs(contention.prs, ghUser);
9843
+ if (competing.length > 0 && !flags["force-competing"]) {
9844
+ console.error(
9845
+ `
9846
+ terminalhire claim: refusing to submit \u2014 ${competing.length} open PR(s) by someone else already address ${claim.repoFullName}#${issueNo}:`
9847
+ );
9848
+ for (const pr of competing) console.error(fmtContentionPr(pr, nowMs, false));
9849
+ console.error(
9850
+ "\n Opening a competing PR reads as low-effort duplication and burns maintainer goodwill.\n Stand down, or add value on the existing PR instead (a review, a test, a comment).\n If you are certain yours should still go up, re-run with --force-competing."
9851
+ );
9852
+ process.exit(1);
9853
+ }
9775
9854
  }
9776
9855
  }
9777
9856
  let ok;
@@ -10187,6 +10266,7 @@ export {
10187
10266
  AI_DISCLOSURE_NOTE,
10188
10267
  CLAIM_CONSENT_VERSION,
10189
10268
  backgroundEnableFailed,
10269
+ buildAssignmentComment,
10190
10270
  buildSubmitBody,
10191
10271
  cmdRecord,
10192
10272
  countOpenPRsReferencingIssue,
@@ -10207,6 +10287,7 @@ export {
10207
10287
  resolveSubmitWorktree,
10208
10288
  revokeFailureAction,
10209
10289
  run,
10290
+ selectCompetingPrs,
10210
10291
  selectPushRemote,
10211
10292
  startBranchFor,
10212
10293
  workDirFor
@@ -11035,6 +11035,7 @@ __export(jpi_claim_exports, {
11035
11035
  AI_DISCLOSURE_NOTE: () => AI_DISCLOSURE_NOTE,
11036
11036
  CLAIM_CONSENT_VERSION: () => CLAIM_CONSENT_VERSION,
11037
11037
  backgroundEnableFailed: () => backgroundEnableFailed,
11038
+ buildAssignmentComment: () => buildAssignmentComment,
11038
11039
  buildSubmitBody: () => buildSubmitBody,
11039
11040
  cmdRecord: () => cmdRecord,
11040
11041
  countOpenPRsReferencingIssue: () => countOpenPRsReferencingIssue,
@@ -11055,6 +11056,7 @@ __export(jpi_claim_exports, {
11055
11056
  resolveSubmitWorktree: () => resolveSubmitWorktree,
11056
11057
  revokeFailureAction: () => revokeFailureAction,
11057
11058
  run: () => run7,
11059
+ selectCompetingPrs: () => selectCompetingPrs,
11058
11060
  selectPushRemote: () => selectPushRemote,
11059
11061
  startBranchFor: () => startBranchFor,
11060
11062
  workDirFor: () => workDirFor
@@ -11288,6 +11290,10 @@ ${p.body ?? ""}`)) {
11288
11290
  }
11289
11291
  return matched;
11290
11292
  }
11293
+ function selectCompetingPrs(prs, selfLogin) {
11294
+ if (!Array.isArray(prs)) return [];
11295
+ return prs.filter((pr) => pr && pr.author !== selfLogin);
11296
+ }
11291
11297
  async function listOpenPRsReferencingIssue(repoFullName, issueNumber) {
11292
11298
  try {
11293
11299
  const res = await fetch(`${GH_API2}/repos/${repoFullName}/pulls?state=open&per_page=100`, {
@@ -11353,6 +11359,65 @@ async function fetchIssue(repoFullName, issueNumber) {
11353
11359
  return null;
11354
11360
  }
11355
11361
  }
11362
+ function buildAssignmentComment(repoFullName) {
11363
+ const usesTakeBot = TAKE_BOT_REPOS.has(repoFullName.toLowerCase());
11364
+ const body = usesTakeBot ? "/take" : `I'd like to work on this \u2014 could I be assigned? Thanks!
11365
+
11366
+ ${ASSIGNMENT_MARKER}`;
11367
+ return { usesTakeBot, body };
11368
+ }
11369
+ async function hasPriorAssignmentRequest(repoFullName, issueNumber, login) {
11370
+ try {
11371
+ const res = await fetch(
11372
+ `${GH_API2}/repos/${repoFullName}/issues/${issueNumber}/comments?per_page=100&sort=created&direction=desc`,
11373
+ { headers: GH_HEADERS2, signal: AbortSignal.timeout(1e4) }
11374
+ );
11375
+ if (!res.ok) return false;
11376
+ const comments = await res.json();
11377
+ if (!Array.isArray(comments)) return false;
11378
+ return comments.some(
11379
+ (c) => c && c.user && c.user.login === login && typeof c.body === "string" && c.body.includes(ASSIGNMENT_MARKER)
11380
+ );
11381
+ } catch {
11382
+ return false;
11383
+ }
11384
+ }
11385
+ async function requestIssueAssignment(claim, flags = {}, ghUser) {
11386
+ if (flags["no-assign"]) {
11387
+ console.log(" (--no-assign \u2014 skipping the assignment request; request it manually before working)");
11388
+ return;
11389
+ }
11390
+ const parsed = parseGitHubUrl(claim.issueUrl);
11391
+ if (!parsed) return;
11392
+ const { repoFullName, number: number3 } = parsed;
11393
+ let login = ghUser;
11394
+ if (!login) {
11395
+ try {
11396
+ login = await sh("gh", ["api", "user", "-q", ".login"]);
11397
+ } catch {
11398
+ console.log(" (assignment not requested: 'gh' not authenticated \u2014 comment on the issue manually before working)");
11399
+ return;
11400
+ }
11401
+ }
11402
+ const issue2 = await fetchIssue(repoFullName, number3);
11403
+ if (issue2 && issue2.assignees.includes(login)) {
11404
+ console.log(` \u2713 Already assigned to @${login} on ${repoFullName}#${number3}.`);
11405
+ return;
11406
+ }
11407
+ const { usesTakeBot, body } = buildAssignmentComment(repoFullName);
11408
+ if (!usesTakeBot && await hasPriorAssignmentRequest(repoFullName, number3, login)) {
11409
+ console.log(` \u2713 Assignment already requested on ${repoFullName}#${number3}.`);
11410
+ return;
11411
+ }
11412
+ try {
11413
+ await sh("gh", ["issue", "comment", String(number3), "--repo", repoFullName, "--body", body]);
11414
+ console.log(
11415
+ usesTakeBot ? ` \u2713 Requested assignment via /take on ${repoFullName}#${number3}.` : ` \u2713 Requested assignment on ${repoFullName}#${number3}.`
11416
+ );
11417
+ } catch (err) {
11418
+ console.log(` (could not post the assignment request: ${err.stderr || err.message || err} \u2014 do it manually before working)`);
11419
+ }
11420
+ }
11356
11421
  async function pollPR(prUrl) {
11357
11422
  const p = parseGitHubUrl(prUrl);
11358
11423
  if (!p) return null;
@@ -11842,7 +11907,7 @@ When it's done: terminalhire claim submit ${id}`);
11842
11907
  }
11843
11908
  }
11844
11909
  if (flags.here) {
11845
- await cmdStartHere(claims, claim);
11910
+ await cmdStartHere(claims, claim, flags);
11846
11911
  return;
11847
11912
  }
11848
11913
  let ghUser;
@@ -11904,6 +11969,7 @@ terminalhire claim: not started \u2014 starting forks ${claim.repoFullName} to y
11904
11969
  await sh("git", ["-C", destDir, "checkout", "-b", branch]);
11905
11970
  const toplevel = await sh("git", ["-C", destDir, "rev-parse", "--show-toplevel"]);
11906
11971
  claims.updateClaim(id, { worktreePath: toplevel, branch, state: "working" });
11972
+ await requestIssueAssignment(claim, flags, ghUser);
11907
11973
  console.log(`
11908
11974
  \u2713 Started: ${claim.title}`);
11909
11975
  console.log(` fork: ${forkFullName}`);
@@ -11913,7 +11979,7 @@ terminalhire claim: not started \u2014 starting forks ${claim.repoFullName} to y
11913
11979
  console.log("\n When the work is done (the only step that pushes + opens the PR):");
11914
11980
  console.log(` terminalhire claim submit ${id}`);
11915
11981
  }
11916
- async function cmdStartHere(claims, claim) {
11982
+ async function cmdStartHere(claims, claim, flags = {}) {
11917
11983
  let toplevel;
11918
11984
  try {
11919
11985
  toplevel = await sh("git", ["-C", process.cwd(), "rev-parse", "--show-toplevel"]);
@@ -11943,6 +12009,7 @@ async function cmdStartHere(claims, claim) {
11943
12009
  const branch = startBranchFor(claim.repoFullName, issueNumber);
11944
12010
  await sh("git", ["-C", toplevel, "checkout", "-b", branch]);
11945
12011
  claims.updateClaim(claim.id, { worktreePath: toplevel, branch, state: "working" });
12012
+ await requestIssueAssignment(claim, flags);
11946
12013
  console.log(`
11947
12014
  \u2713 Started here: ${claim.title}`);
11948
12015
  console.log(` worktree: ${toplevel}`);
@@ -12174,10 +12241,22 @@ async function cmdSubmit(id, flags = {}) {
12174
12241
  const issueNo = (parseGitHubUrl(claim.issueUrl) || {}).number;
12175
12242
  const contention = issueNo ? await listOpenPRsReferencingIssue(claim.repoFullName, issueNo) : null;
12176
12243
  if (contention && contention.total > 0) {
12177
- console.log(` \u26A0 contention: ${contention.total} open PR(s) already reference this issue:`);
12178
12244
  const nowMs = Date.now();
12245
+ console.log(` \u26A0 contention: ${contention.total} open PR(s) already reference this issue:`);
12179
12246
  for (const pr of contention.prs) console.log(fmtContentionPr(pr, nowMs, false));
12180
12247
  console.log(CONTENTION_HINT);
12248
+ const competing = selectCompetingPrs(contention.prs, ghUser);
12249
+ if (competing.length > 0 && !flags["force-competing"]) {
12250
+ console.error(
12251
+ `
12252
+ terminalhire claim: refusing to submit \u2014 ${competing.length} open PR(s) by someone else already address ${claim.repoFullName}#${issueNo}:`
12253
+ );
12254
+ for (const pr of competing) console.error(fmtContentionPr(pr, nowMs, false));
12255
+ console.error(
12256
+ "\n Opening a competing PR reads as low-effort duplication and burns maintainer goodwill.\n Stand down, or add value on the existing PR instead (a review, a test, a comment).\n If you are certain yours should still go up, re-run with --force-competing."
12257
+ );
12258
+ process.exit(1);
12259
+ }
12181
12260
  }
12182
12261
  }
12183
12262
  let ok;
@@ -12589,7 +12668,7 @@ async function run7() {
12589
12668
  process.exit(1);
12590
12669
  }
12591
12670
  }
12592
- var TERMINALHIRE_DIR13, INDEX_CACHE_FILE5, CLAIM_PUSH_MARKER, API_URL6, CLAIM_SYNC_BASE2, CLAIM_CONSENT_VERSION, CLAIM_POLL_INTERVAL_MS, CLAIM_POLL_TIMEOUT_MS, GH_API2, GH_HEADERS2, CONTENTION_HINT, AI_DISCLOSURE_NOTE, pExecFile, VALUE_FLAGS;
12671
+ var TERMINALHIRE_DIR13, INDEX_CACHE_FILE5, CLAIM_PUSH_MARKER, API_URL6, CLAIM_SYNC_BASE2, CLAIM_CONSENT_VERSION, CLAIM_POLL_INTERVAL_MS, CLAIM_POLL_TIMEOUT_MS, GH_API2, GH_HEADERS2, CONTENTION_HINT, AI_DISCLOSURE_NOTE, pExecFile, VALUE_FLAGS, ASSIGNMENT_MARKER, TAKE_BOT_REPOS;
12593
12672
  var init_jpi_claim = __esm({
12594
12673
  "bin/jpi-claim.js"() {
12595
12674
  "use strict";
@@ -12611,6 +12690,8 @@ var init_jpi_claim = __esm({
12611
12690
  AI_DISCLOSURE_NOTE = "---\nThis contribution was developed with AI assistance via [terminalhire](https://terminalhire.com). The author has reviewed the change and takes responsibility for its content.";
12612
12691
  pExecFile = promisify(execFile);
12613
12692
  VALUE_FLAGS = /* @__PURE__ */ new Set(["worktree", "branch", "body-file", "title"]);
12693
+ ASSIGNMENT_MARKER = "<!-- terminalhire:assignment-request -->";
12694
+ TAKE_BOT_REPOS = /* @__PURE__ */ new Set(["paradedb/paradedb"]);
12614
12695
  }
12615
12696
  });
12616
12697
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Matching runs on your machine; your profile never leaves it.",
5
5
  "repository": {
6
6
  "type": "git",