terminalhire 0.41.0 → 0.41.2

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.
@@ -166,8 +166,24 @@ function nextPolledState(from, observed) {
166
166
  function nowISO() {
167
167
  return (/* @__PURE__ */ new Date()).toISOString();
168
168
  }
169
+ function defangText(s) {
170
+ return typeof s === "string" ? s.replace(CONTROL_CHARS, "") : s;
171
+ }
172
+ function finiteAmount(a) {
173
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
174
+ if (typeof a !== "string" || a.trim() === "") return null;
175
+ const n = Number(a);
176
+ return Number.isFinite(n) ? n : null;
177
+ }
169
178
  function normalizeClaim(c) {
170
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
179
+ return {
180
+ ...c,
181
+ kind: c.kind ?? "bounty",
182
+ policy: c.policy ?? null,
183
+ title: defangText(c.title),
184
+ repoFullName: defangText(c.repoFullName),
185
+ amountUSD: finiteAmount(c.amountUSD)
186
+ };
171
187
  }
172
188
  function readClaims() {
173
189
  try {
@@ -289,7 +305,7 @@ function acceptedPRRate(claims = readClaims()) {
289
305
  const merged = claims.filter((c) => c.state === "merged").length;
290
306
  return { merged, total, rate: total === 0 ? 0 : merged / total };
291
307
  }
292
- var TERMINALHIRE_DIR3, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
308
+ var TERMINALHIRE_DIR3, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS;
293
309
  var init_claims = __esm({
294
310
  "src/claims.ts"() {
295
311
  "use strict";
@@ -345,6 +361,7 @@ var init_claims = __esm({
345
361
  ]),
346
362
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
347
363
  };
364
+ CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
348
365
  }
349
366
  });
350
367
 
@@ -11338,8 +11338,24 @@ function nextPolledState(from, observed) {
11338
11338
  function nowISO2() {
11339
11339
  return (/* @__PURE__ */ new Date()).toISOString();
11340
11340
  }
11341
+ function defangText(s) {
11342
+ return typeof s === "string" ? s.replace(CONTROL_CHARS2, "") : s;
11343
+ }
11344
+ function finiteAmount(a) {
11345
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
11346
+ if (typeof a !== "string" || a.trim() === "") return null;
11347
+ const n = Number(a);
11348
+ return Number.isFinite(n) ? n : null;
11349
+ }
11341
11350
  function normalizeClaim(c) {
11342
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
11351
+ return {
11352
+ ...c,
11353
+ kind: c.kind ?? "bounty",
11354
+ policy: c.policy ?? null,
11355
+ title: defangText(c.title),
11356
+ repoFullName: defangText(c.repoFullName),
11357
+ amountUSD: finiteAmount(c.amountUSD)
11358
+ };
11343
11359
  }
11344
11360
  function readClaims() {
11345
11361
  try {
@@ -11461,7 +11477,7 @@ function acceptedPRRate(claims = readClaims()) {
11461
11477
  const merged = claims.filter((c) => c.state === "merged").length;
11462
11478
  return { merged, total, rate: total === 0 ? 0 : merged / total };
11463
11479
  }
11464
- var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
11480
+ var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS2;
11465
11481
  var init_claims = __esm({
11466
11482
  "src/claims.ts"() {
11467
11483
  "use strict";
@@ -11517,6 +11533,7 @@ var init_claims = __esm({
11517
11533
  ]),
11518
11534
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
11519
11535
  };
11536
+ CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
11520
11537
  }
11521
11538
  });
11522
11539
 
@@ -10638,8 +10638,24 @@ function nextPolledState(from, observed) {
10638
10638
  function nowISO() {
10639
10639
  return (/* @__PURE__ */ new Date()).toISOString();
10640
10640
  }
10641
+ function defangText(s) {
10642
+ return typeof s === "string" ? s.replace(CONTROL_CHARS2, "") : s;
10643
+ }
10644
+ function finiteAmount(a) {
10645
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
10646
+ if (typeof a !== "string" || a.trim() === "") return null;
10647
+ const n = Number(a);
10648
+ return Number.isFinite(n) ? n : null;
10649
+ }
10641
10650
  function normalizeClaim(c) {
10642
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
10651
+ return {
10652
+ ...c,
10653
+ kind: c.kind ?? "bounty",
10654
+ policy: c.policy ?? null,
10655
+ title: defangText(c.title),
10656
+ repoFullName: defangText(c.repoFullName),
10657
+ amountUSD: finiteAmount(c.amountUSD)
10658
+ };
10643
10659
  }
10644
10660
  function readClaims() {
10645
10661
  try {
@@ -10761,7 +10777,7 @@ function acceptedPRRate(claims = readClaims()) {
10761
10777
  const merged = claims.filter((c) => c.state === "merged").length;
10762
10778
  return { merged, total, rate: total === 0 ? 0 : merged / total };
10763
10779
  }
10764
- var TERMINALHIRE_DIR2, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
10780
+ var TERMINALHIRE_DIR2, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS2;
10765
10781
  var init_claims = __esm({
10766
10782
  "src/claims.ts"() {
10767
10783
  "use strict";
@@ -10817,6 +10833,7 @@ var init_claims = __esm({
10817
10833
  ]),
10818
10834
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
10819
10835
  };
10836
+ CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
10820
10837
  }
10821
10838
  });
10822
10839
 
@@ -26056,6 +26073,13 @@ function openInBrowser(url) {
26056
26073
  }
26057
26074
  }
26058
26075
 
26076
+ // bin/sanitize.js
26077
+ var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
26078
+ function sanitizeText(s) {
26079
+ if (s == null) return "";
26080
+ return String(s).replace(CONTROL_CHARS, "");
26081
+ }
26082
+
26059
26083
  // src/policy-acks.ts
26060
26084
  init_state_dir();
26061
26085
  import { lstatSync, readFileSync as readFileSync2, writeFileSync } from "fs";
@@ -26979,7 +27003,10 @@ async function pollPR(prUrl) {
26979
27003
  }
26980
27004
  }
26981
27005
  function fmtAmount(a) {
26982
- return a != null ? "$" + a.toLocaleString() : "$\u2014";
27006
+ if (a == null) return "$\u2014";
27007
+ if (typeof a === "string" && a.trim() === "") return "$\u2014";
27008
+ const n = typeof a === "number" ? a : Number(a);
27009
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
26983
27010
  }
26984
27011
  function fmtClaimAmount(c) {
26985
27012
  return c.kind === "contribution" ? "contribution" : fmtAmount(c.amountUSD);
@@ -28307,35 +28334,62 @@ async function ensureForkExists(repoFullName, ghUser) {
28307
28334
  if (!isFork) throw new Error(`fork ${forkFullName} created but could not be verified as a fork`);
28308
28335
  return forkFullName;
28309
28336
  }
28337
+ function shouldStatePending(claim, approvalsChecked) {
28338
+ return Boolean(claim?.approval) && claim.approval.state === "pending" && Boolean(approvalsChecked);
28339
+ }
28340
+ function startableRow(c) {
28341
+ const bits = [fmtClaimAmount(c), sanitizeText(c.title)];
28342
+ if (c.repoFullName) bits.push(sanitizeText(c.repoFullName));
28343
+ return bits.join(" \xB7 ");
28344
+ }
28345
+ async function pickStartableClaim(claims, { prompt = ask, isTTY = process.stdin.isTTY } = {}) {
28346
+ const active = claims.listClaims({ active: true });
28347
+ const { approvalsChecked, approvalsUnavailable } = await syncFounderApprovals(claims, active);
28348
+ const startable = claims.listClaims({ active: true }).filter((c) => c.state === "claimed");
28349
+ if (startable.length === 0) {
28350
+ console.log("No claims ready to start. Claim one first: terminalhire claim <ref>");
28351
+ return null;
28352
+ }
28353
+ const interactive = Boolean(isTTY);
28354
+ console.log(`
28355
+ ${startable.length} claim${startable.length === 1 ? "" : "s"} ready to start:
28356
+ `);
28357
+ const width = String(startable.length).length;
28358
+ startable.forEach((c, i) => {
28359
+ console.log(` ${String(i + 1).padStart(width)}) ${startableRow(c)}`);
28360
+ if (!interactive) console.log(` terminalhire claim start ${c.id}`);
28361
+ });
28362
+ if (!interactive) {
28363
+ console.log("\nRun the command under the one you want to start.");
28364
+ return null;
28365
+ }
28366
+ const answer = await prompt(`
28367
+ Which one? (1-${startable.length}, or q to quit) `);
28368
+ if (answer === "" || /^q(uit)?$/i.test(answer)) return null;
28369
+ const n = Number.parseInt(answer, 10);
28370
+ if (!/^\d+$/.test(answer) || !Number.isInteger(n) || n < 1 || n > startable.length) {
28371
+ console.log(`
28372
+ Nothing started \u2014 '${answer}' is not one of 1-${startable.length}.`);
28373
+ return null;
28374
+ }
28375
+ return { id: startable[n - 1].id, approvalsChecked, approvalsUnavailable };
28376
+ }
28310
28377
  async function cmdStart(id, flags = {}) {
28311
28378
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
28379
+ let approvalsChecked = false;
28380
+ let approvalsUnavailable = false;
28312
28381
  if (!id) {
28313
- let list = claims.listClaims({ active: true });
28314
- await syncFounderApprovals(claims, list);
28315
- const startable = claims.listClaims({ active: true }).filter((c) => c.state === "claimed");
28316
- if (startable.length === 0) {
28317
- console.log("No claims ready to start. Claim one first: terminalhire claim <ref>");
28318
- return;
28319
- }
28320
- console.log(
28321
- `
28322
- ${startable.length} claim${startable.length === 1 ? "" : "s"} ready to start:
28323
- `
28324
- );
28325
- for (const c of startable) {
28326
- console.log(` ${c.title}`);
28327
- console.log(` terminalhire claim start ${c.id}`);
28328
- }
28329
- console.log("\nRun the command under the one you want to start.");
28330
- return;
28382
+ const picked = await pickStartableClaim(claims);
28383
+ if (!picked) return;
28384
+ ({ id, approvalsChecked, approvalsUnavailable } = picked);
28331
28385
  }
28332
28386
  let claim = claims.findClaim(id);
28333
28387
  if (!claim) {
28334
28388
  console.error(`terminalhire claim: no claim with id '${id}'.`);
28335
28389
  process.exit(1);
28336
28390
  }
28337
- if (claim.approval?.state === "pending") {
28338
- await syncFounderApprovals(claims, [claim]);
28391
+ if (claim.approval?.state === "pending" && !approvalsChecked) {
28392
+ ({ approvalsChecked, approvalsUnavailable } = await syncFounderApprovals(claims, [claim]));
28339
28393
  claim = claims.findClaim(id);
28340
28394
  }
28341
28395
  if (claim.worktreePath) {
@@ -28356,19 +28410,18 @@ When it's done: terminalhire claim submit ${id}`);
28356
28410
  }
28357
28411
  if (claim.approval) {
28358
28412
  console.log(`
28359
- ${claim.title}`);
28360
- if (claim.approval.state === "pending") {
28413
+ ${sanitizeText(claim.title)}`);
28414
+ console.log("\n No fork was attempted \u2014 founder postings are never forked or cloned. Your");
28415
+ console.log(" work slice is delivered through terminalhire, and your patch goes back the");
28416
+ console.log(" same way.");
28417
+ if (shouldStatePending(claim, approvalsChecked)) {
28418
+ console.log("\n Access is pending \u2014 the founder has not approved your claim yet.");
28419
+ } else if (approvalsUnavailable) {
28361
28420
  console.log(
28362
- "\n Access is pending \u2014 this approval-only posting needs the founder to approve"
28421
+ "\n Terminalhire could not check approvals right now; local state was preserved.\n The next step below still works \u2014 it asks the server directly."
28363
28422
  );
28364
- console.log(" your claim before any work can be delivered. No fork was attempted: founder");
28365
- console.log(" postings are never forked or cloned; once approved, your work slice is");
28366
- console.log(" delivered through terminalhire.");
28367
- } else {
28368
- console.log("\n No fork was attempted \u2014 founder postings are never forked or cloned. Your");
28369
- console.log(" work slice is delivered through terminalhire, and your patch goes back the");
28370
- console.log(" same way.");
28371
28423
  }
28424
+ printNextSteps([claim]);
28372
28425
  return;
28373
28426
  }
28374
28427
  if (flags.here) {
@@ -28618,10 +28671,10 @@ var CLAIM_EVENT_LABEL = {
28618
28671
  rejected: "the founder rejected"
28619
28672
  };
28620
28673
  var LINE_BREAKS = /\r\n|[\r\n\v\f\u0085\u2028\u2029]/;
28621
- var CONTROL_CHARS = /[\u0000-\u001F\u007F-\u009F]/g;
28674
+ var CONTROL_CHARS3 = /[\u0000-\u001F\u007F-\u009F]/g;
28622
28675
  function terminalSafeLines(raw) {
28623
28676
  if (typeof raw !== "string" || raw === "") return [];
28624
- return raw.split(LINE_BREAKS).map((l) => l.replace(CONTROL_CHARS, ""));
28677
+ return raw.split(LINE_BREAKS).map((l) => l.replace(CONTROL_CHARS3, ""));
28625
28678
  }
28626
28679
  function terminalSafeInline(raw) {
28627
28680
  return terminalSafeLines(raw).join(" ");
@@ -30066,6 +30119,7 @@ export {
30066
30119
  normalizeIntent,
30067
30120
  pickBodySource,
30068
30121
  pickExistingPr,
30122
+ pickStartableClaim,
30069
30123
  printNextSteps,
30070
30124
  readCredentialDisposition,
30071
30125
  renderClaimHistory,
@@ -30081,6 +30135,7 @@ export {
30081
30135
  selectCompetingPrs,
30082
30136
  selectPushRemote,
30083
30137
  shouldRequestAssignment,
30138
+ shouldStatePending,
30084
30139
  sliceWorkDirFor,
30085
30140
  stakeDecision,
30086
30141
  startBranchFor,
@@ -3617,8 +3617,24 @@ function nextPolledState(from, observed) {
3617
3617
  function nowISO2() {
3618
3618
  return (/* @__PURE__ */ new Date()).toISOString();
3619
3619
  }
3620
+ function defangText(s) {
3621
+ return typeof s === "string" ? s.replace(CONTROL_CHARS2, "") : s;
3622
+ }
3623
+ function finiteAmount(a) {
3624
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
3625
+ if (typeof a !== "string" || a.trim() === "") return null;
3626
+ const n = Number(a);
3627
+ return Number.isFinite(n) ? n : null;
3628
+ }
3620
3629
  function normalizeClaim(c) {
3621
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
3630
+ return {
3631
+ ...c,
3632
+ kind: c.kind ?? "bounty",
3633
+ policy: c.policy ?? null,
3634
+ title: defangText(c.title),
3635
+ repoFullName: defangText(c.repoFullName),
3636
+ amountUSD: finiteAmount(c.amountUSD)
3637
+ };
3622
3638
  }
3623
3639
  function readClaims() {
3624
3640
  try {
@@ -3740,7 +3756,7 @@ function acceptedPRRate(claims = readClaims()) {
3740
3756
  const merged = claims.filter((c) => c.state === "merged").length;
3741
3757
  return { merged, total, rate: total === 0 ? 0 : merged / total };
3742
3758
  }
3743
- var TERMINALHIRE_DIR7, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
3759
+ var TERMINALHIRE_DIR7, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS2;
3744
3760
  var init_claims = __esm({
3745
3761
  "src/claims.ts"() {
3746
3762
  "use strict";
@@ -3796,6 +3812,7 @@ var init_claims = __esm({
3796
3812
  ]),
3797
3813
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
3798
3814
  };
3815
+ CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
3799
3816
  }
3800
3817
  });
3801
3818
 
@@ -15181,8 +15181,24 @@ function nextPolledState(from, observed) {
15181
15181
  function nowISO2() {
15182
15182
  return (/* @__PURE__ */ new Date()).toISOString();
15183
15183
  }
15184
+ function defangText(s) {
15185
+ return typeof s === "string" ? s.replace(CONTROL_CHARS2, "") : s;
15186
+ }
15187
+ function finiteAmount(a) {
15188
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
15189
+ if (typeof a !== "string" || a.trim() === "") return null;
15190
+ const n = Number(a);
15191
+ return Number.isFinite(n) ? n : null;
15192
+ }
15184
15193
  function normalizeClaim(c) {
15185
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
15194
+ return {
15195
+ ...c,
15196
+ kind: c.kind ?? "bounty",
15197
+ policy: c.policy ?? null,
15198
+ title: defangText(c.title),
15199
+ repoFullName: defangText(c.repoFullName),
15200
+ amountUSD: finiteAmount(c.amountUSD)
15201
+ };
15186
15202
  }
15187
15203
  function readClaims() {
15188
15204
  try {
@@ -15304,7 +15320,7 @@ function acceptedPRRate(claims = readClaims()) {
15304
15320
  const merged = claims.filter((c) => c.state === "merged").length;
15305
15321
  return { merged, total, rate: total === 0 ? 0 : merged / total };
15306
15322
  }
15307
- var TERMINALHIRE_DIR11, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS2, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
15323
+ var TERMINALHIRE_DIR11, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS2, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS2;
15308
15324
  var init_claims = __esm({
15309
15325
  "src/claims.ts"() {
15310
15326
  "use strict";
@@ -15360,6 +15376,7 @@ var init_claims = __esm({
15360
15376
  ]),
15361
15377
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
15362
15378
  };
15379
+ CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
15363
15380
  }
15364
15381
  });
15365
15382
 
@@ -30909,6 +30926,7 @@ __export(jpi_claim_exports, {
30909
30926
  normalizeIntent: () => normalizeIntent,
30910
30927
  pickBodySource: () => pickBodySource,
30911
30928
  pickExistingPr: () => pickExistingPr,
30929
+ pickStartableClaim: () => pickStartableClaim,
30912
30930
  printNextSteps: () => printNextSteps,
30913
30931
  readCredentialDisposition: () => readCredentialDisposition,
30914
30932
  renderClaimHistory: () => renderClaimHistory,
@@ -30924,6 +30942,7 @@ __export(jpi_claim_exports, {
30924
30942
  selectCompetingPrs: () => selectCompetingPrs,
30925
30943
  selectPushRemote: () => selectPushRemote,
30926
30944
  shouldRequestAssignment: () => shouldRequestAssignment,
30945
+ shouldStatePending: () => shouldStatePending,
30927
30946
  sliceWorkDirFor: () => sliceWorkDirFor,
30928
30947
  stakeDecision: () => stakeDecision,
30929
30948
  startBranchFor: () => startBranchFor,
@@ -31583,7 +31602,10 @@ async function pollPR(prUrl) {
31583
31602
  }
31584
31603
  }
31585
31604
  function fmtAmount(a) {
31586
- return a != null ? "$" + a.toLocaleString() : "$\u2014";
31605
+ if (a == null) return "$\u2014";
31606
+ if (typeof a === "string" && a.trim() === "") return "$\u2014";
31607
+ const n = typeof a === "number" ? a : Number(a);
31608
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
31587
31609
  }
31588
31610
  function fmtClaimAmount(c) {
31589
31611
  return c.kind === "contribution" ? "contribution" : fmtAmount(c.amountUSD);
@@ -32890,35 +32912,62 @@ async function ensureForkExists(repoFullName, ghUser) {
32890
32912
  if (!isFork) throw new Error(`fork ${forkFullName} created but could not be verified as a fork`);
32891
32913
  return forkFullName;
32892
32914
  }
32915
+ function shouldStatePending(claim, approvalsChecked) {
32916
+ return Boolean(claim?.approval) && claim.approval.state === "pending" && Boolean(approvalsChecked);
32917
+ }
32918
+ function startableRow(c) {
32919
+ const bits = [fmtClaimAmount(c), sanitizeText(c.title)];
32920
+ if (c.repoFullName) bits.push(sanitizeText(c.repoFullName));
32921
+ return bits.join(" \xB7 ");
32922
+ }
32923
+ async function pickStartableClaim(claims, { prompt: prompt5 = ask, isTTY = process.stdin.isTTY } = {}) {
32924
+ const active = claims.listClaims({ active: true });
32925
+ const { approvalsChecked, approvalsUnavailable } = await syncFounderApprovals(claims, active);
32926
+ const startable = claims.listClaims({ active: true }).filter((c) => c.state === "claimed");
32927
+ if (startable.length === 0) {
32928
+ console.log("No claims ready to start. Claim one first: terminalhire claim <ref>");
32929
+ return null;
32930
+ }
32931
+ const interactive = Boolean(isTTY);
32932
+ console.log(`
32933
+ ${startable.length} claim${startable.length === 1 ? "" : "s"} ready to start:
32934
+ `);
32935
+ const width = String(startable.length).length;
32936
+ startable.forEach((c, i) => {
32937
+ console.log(` ${String(i + 1).padStart(width)}) ${startableRow(c)}`);
32938
+ if (!interactive) console.log(` terminalhire claim start ${c.id}`);
32939
+ });
32940
+ if (!interactive) {
32941
+ console.log("\nRun the command under the one you want to start.");
32942
+ return null;
32943
+ }
32944
+ const answer = await prompt5(`
32945
+ Which one? (1-${startable.length}, or q to quit) `);
32946
+ if (answer === "" || /^q(uit)?$/i.test(answer)) return null;
32947
+ const n = Number.parseInt(answer, 10);
32948
+ if (!/^\d+$/.test(answer) || !Number.isInteger(n) || n < 1 || n > startable.length) {
32949
+ console.log(`
32950
+ Nothing started \u2014 '${answer}' is not one of 1-${startable.length}.`);
32951
+ return null;
32952
+ }
32953
+ return { id: startable[n - 1].id, approvalsChecked, approvalsUnavailable };
32954
+ }
32893
32955
  async function cmdStart(id, flags = {}) {
32894
32956
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
32957
+ let approvalsChecked = false;
32958
+ let approvalsUnavailable = false;
32895
32959
  if (!id) {
32896
- let list = claims.listClaims({ active: true });
32897
- await syncFounderApprovals(claims, list);
32898
- const startable = claims.listClaims({ active: true }).filter((c) => c.state === "claimed");
32899
- if (startable.length === 0) {
32900
- console.log("No claims ready to start. Claim one first: terminalhire claim <ref>");
32901
- return;
32902
- }
32903
- console.log(
32904
- `
32905
- ${startable.length} claim${startable.length === 1 ? "" : "s"} ready to start:
32906
- `
32907
- );
32908
- for (const c of startable) {
32909
- console.log(` ${c.title}`);
32910
- console.log(` terminalhire claim start ${c.id}`);
32911
- }
32912
- console.log("\nRun the command under the one you want to start.");
32913
- return;
32960
+ const picked = await pickStartableClaim(claims);
32961
+ if (!picked) return;
32962
+ ({ id, approvalsChecked, approvalsUnavailable } = picked);
32914
32963
  }
32915
32964
  let claim = claims.findClaim(id);
32916
32965
  if (!claim) {
32917
32966
  console.error(`terminalhire claim: no claim with id '${id}'.`);
32918
32967
  process.exit(1);
32919
32968
  }
32920
- if (claim.approval?.state === "pending") {
32921
- await syncFounderApprovals(claims, [claim]);
32969
+ if (claim.approval?.state === "pending" && !approvalsChecked) {
32970
+ ({ approvalsChecked, approvalsUnavailable } = await syncFounderApprovals(claims, [claim]));
32922
32971
  claim = claims.findClaim(id);
32923
32972
  }
32924
32973
  if (claim.worktreePath) {
@@ -32939,19 +32988,18 @@ When it's done: terminalhire claim submit ${id}`);
32939
32988
  }
32940
32989
  if (claim.approval) {
32941
32990
  console.log(`
32942
- ${claim.title}`);
32943
- if (claim.approval.state === "pending") {
32991
+ ${sanitizeText(claim.title)}`);
32992
+ console.log("\n No fork was attempted \u2014 founder postings are never forked or cloned. Your");
32993
+ console.log(" work slice is delivered through terminalhire, and your patch goes back the");
32994
+ console.log(" same way.");
32995
+ if (shouldStatePending(claim, approvalsChecked)) {
32996
+ console.log("\n Access is pending \u2014 the founder has not approved your claim yet.");
32997
+ } else if (approvalsUnavailable) {
32944
32998
  console.log(
32945
- "\n Access is pending \u2014 this approval-only posting needs the founder to approve"
32999
+ "\n Terminalhire could not check approvals right now; local state was preserved.\n The next step below still works \u2014 it asks the server directly."
32946
33000
  );
32947
- console.log(" your claim before any work can be delivered. No fork was attempted: founder");
32948
- console.log(" postings are never forked or cloned; once approved, your work slice is");
32949
- console.log(" delivered through terminalhire.");
32950
- } else {
32951
- console.log("\n No fork was attempted \u2014 founder postings are never forked or cloned. Your");
32952
- console.log(" work slice is delivered through terminalhire, and your patch goes back the");
32953
- console.log(" same way.");
32954
33001
  }
33002
+ printNextSteps([claim]);
32955
33003
  return;
32956
33004
  }
32957
33005
  if (flags.here) {
@@ -33189,7 +33237,7 @@ function renderRunView(run32, message) {
33189
33237
  }
33190
33238
  function terminalSafeLines(raw) {
33191
33239
  if (typeof raw !== "string" || raw === "") return [];
33192
- return raw.split(LINE_BREAKS).map((l) => l.replace(CONTROL_CHARS2, ""));
33240
+ return raw.split(LINE_BREAKS).map((l) => l.replace(CONTROL_CHARS3, ""));
33193
33241
  }
33194
33242
  function terminalSafeInline(raw) {
33195
33243
  return terminalSafeLines(raw).join(" ");
@@ -34593,12 +34641,13 @@ async function run7() {
34593
34641
  process.exit(1);
34594
34642
  }
34595
34643
  }
34596
- var TERMINALHIRE_DIR17, INDEX_CACHE_FILE5, CLAIM_PUSH_MARKER, REPO_CONTINUITY_NUDGE_MARKER, API_URL6, CLAIM_SYNC_BASE3, CLAIM_CONSENT_VERSION, CLAIM_POLL_INTERVAL_MS, CLAIM_POLL_TIMEOUT_MS, GH_API3, GH_HEADERS2, CONTENTION_HINT, AI_DISCLOSURE_NOTE, pExecFile, VALUE_FLAGS, ASSIGNMENT_MARKER, STAKE_MARKER, STANDDOWN_MARKER, OUR_MARKERS, STAKE_POST_TIMEOUT_MS, STAKE_POSTING_GRACE_MS, TAKE_BOT_REPOS, SUBMIT_ACCEPTS, REVISE_RECOVERY_STATES, PUSH_TOKEN_REFUSAL, SYNC_BACKGROUND_PUSH_ACTIVE_FIELD, ISSUE_OUTCOME_TERMINAL, RUNS_POLL_INTERVAL_MS, RUNS_POLL_ATTEMPTS, CLAIM_EVENT_LABEL, LINE_BREAKS, CONTROL_CHARS2;
34644
+ var TERMINALHIRE_DIR17, INDEX_CACHE_FILE5, CLAIM_PUSH_MARKER, REPO_CONTINUITY_NUDGE_MARKER, API_URL6, CLAIM_SYNC_BASE3, CLAIM_CONSENT_VERSION, CLAIM_POLL_INTERVAL_MS, CLAIM_POLL_TIMEOUT_MS, GH_API3, GH_HEADERS2, CONTENTION_HINT, AI_DISCLOSURE_NOTE, pExecFile, VALUE_FLAGS, ASSIGNMENT_MARKER, STAKE_MARKER, STANDDOWN_MARKER, OUR_MARKERS, STAKE_POST_TIMEOUT_MS, STAKE_POSTING_GRACE_MS, TAKE_BOT_REPOS, SUBMIT_ACCEPTS, REVISE_RECOVERY_STATES, PUSH_TOKEN_REFUSAL, SYNC_BACKGROUND_PUSH_ACTIVE_FIELD, ISSUE_OUTCOME_TERMINAL, RUNS_POLL_INTERVAL_MS, RUNS_POLL_ATTEMPTS, CLAIM_EVENT_LABEL, LINE_BREAKS, CONTROL_CHARS3;
34597
34645
  var init_jpi_claim = __esm({
34598
34646
  "bin/jpi-claim.js"() {
34599
34647
  "use strict";
34600
34648
  init_src();
34601
34649
  init_open_url();
34650
+ init_sanitize();
34602
34651
  init_policy_acks();
34603
34652
  init_claims();
34604
34653
  init_state_dir();
@@ -34661,7 +34710,7 @@ var init_jpi_claim = __esm({
34661
34710
  rejected: "the founder rejected"
34662
34711
  };
34663
34712
  LINE_BREAKS = /\r\n|[\r\n\v\f\u0085\u2028\u2029]/;
34664
- CONTROL_CHARS2 = /[\u0000-\u001F\u007F-\u009F]/g;
34713
+ CONTROL_CHARS3 = /[\u0000-\u001F\u007F-\u009F]/g;
34665
34714
  }
34666
34715
  });
34667
34716
 
@@ -538,8 +538,24 @@ function nextPolledState(from, observed) {
538
538
  function nowISO() {
539
539
  return (/* @__PURE__ */ new Date()).toISOString();
540
540
  }
541
+ function defangText(s) {
542
+ return typeof s === "string" ? s.replace(CONTROL_CHARS, "") : s;
543
+ }
544
+ function finiteAmount(a) {
545
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
546
+ if (typeof a !== "string" || a.trim() === "") return null;
547
+ const n = Number(a);
548
+ return Number.isFinite(n) ? n : null;
549
+ }
541
550
  function normalizeClaim(c) {
542
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
551
+ return {
552
+ ...c,
553
+ kind: c.kind ?? "bounty",
554
+ policy: c.policy ?? null,
555
+ title: defangText(c.title),
556
+ repoFullName: defangText(c.repoFullName),
557
+ amountUSD: finiteAmount(c.amountUSD)
558
+ };
543
559
  }
544
560
  function readClaims() {
545
561
  try {
@@ -661,7 +677,7 @@ function acceptedPRRate(claims = readClaims()) {
661
677
  const merged = claims.filter((c) => c.state === "merged").length;
662
678
  return { merged, total, rate: total === 0 ? 0 : merged / total };
663
679
  }
664
- var TERMINALHIRE_DIR, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
680
+ var TERMINALHIRE_DIR, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS;
665
681
  var init_claims = __esm({
666
682
  "src/claims.ts"() {
667
683
  "use strict";
@@ -717,6 +733,7 @@ var init_claims = __esm({
717
733
  ]),
718
734
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
719
735
  };
736
+ CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
720
737
  }
721
738
  });
722
739
 
@@ -10977,6 +10977,19 @@ var init_open_url = __esm({
10977
10977
  }
10978
10978
  });
10979
10979
 
10980
+ // bin/sanitize.js
10981
+ function sanitizeText(s) {
10982
+ if (s == null) return "";
10983
+ return String(s).replace(CONTROL_CHARS, "");
10984
+ }
10985
+ var CONTROL_CHARS;
10986
+ var init_sanitize = __esm({
10987
+ "bin/sanitize.js"() {
10988
+ "use strict";
10989
+ CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
10990
+ }
10991
+ });
10992
+
10980
10993
  // src/policy-acks.ts
10981
10994
  var policy_acks_exports = {};
10982
10995
  __export(policy_acks_exports, {
@@ -11127,8 +11140,24 @@ function nextPolledState(from, observed) {
11127
11140
  function nowISO() {
11128
11141
  return (/* @__PURE__ */ new Date()).toISOString();
11129
11142
  }
11143
+ function defangText(s) {
11144
+ return typeof s === "string" ? s.replace(CONTROL_CHARS2, "") : s;
11145
+ }
11146
+ function finiteAmount(a) {
11147
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
11148
+ if (typeof a !== "string" || a.trim() === "") return null;
11149
+ const n = Number(a);
11150
+ return Number.isFinite(n) ? n : null;
11151
+ }
11130
11152
  function normalizeClaim(c) {
11131
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
11153
+ return {
11154
+ ...c,
11155
+ kind: c.kind ?? "bounty",
11156
+ policy: c.policy ?? null,
11157
+ title: defangText(c.title),
11158
+ repoFullName: defangText(c.repoFullName),
11159
+ amountUSD: finiteAmount(c.amountUSD)
11160
+ };
11132
11161
  }
11133
11162
  function readClaims() {
11134
11163
  try {
@@ -11250,7 +11279,7 @@ function acceptedPRRate(claims = readClaims()) {
11250
11279
  const merged = claims.filter((c) => c.state === "merged").length;
11251
11280
  return { merged, total, rate: total === 0 ? 0 : merged / total };
11252
11281
  }
11253
- var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
11282
+ var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS2;
11254
11283
  var init_claims = __esm({
11255
11284
  "src/claims.ts"() {
11256
11285
  "use strict";
@@ -11306,6 +11335,7 @@ var init_claims = __esm({
11306
11335
  ]),
11307
11336
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
11308
11337
  };
11338
+ CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
11309
11339
  }
11310
11340
  });
11311
11341
 
@@ -26374,6 +26404,7 @@ __export(jpi_claim_exports, {
26374
26404
  normalizeIntent: () => normalizeIntent,
26375
26405
  pickBodySource: () => pickBodySource,
26376
26406
  pickExistingPr: () => pickExistingPr,
26407
+ pickStartableClaim: () => pickStartableClaim,
26377
26408
  printNextSteps: () => printNextSteps,
26378
26409
  readCredentialDisposition: () => readCredentialDisposition,
26379
26410
  renderClaimHistory: () => renderClaimHistory,
@@ -26389,6 +26420,7 @@ __export(jpi_claim_exports, {
26389
26420
  selectCompetingPrs: () => selectCompetingPrs,
26390
26421
  selectPushRemote: () => selectPushRemote,
26391
26422
  shouldRequestAssignment: () => shouldRequestAssignment,
26423
+ shouldStatePending: () => shouldStatePending,
26392
26424
  sliceWorkDirFor: () => sliceWorkDirFor,
26393
26425
  stakeDecision: () => stakeDecision,
26394
26426
  startBranchFor: () => startBranchFor,
@@ -27048,7 +27080,10 @@ async function pollPR(prUrl) {
27048
27080
  }
27049
27081
  }
27050
27082
  function fmtAmount(a) {
27051
- return a != null ? "$" + a.toLocaleString() : "$\u2014";
27083
+ if (a == null) return "$\u2014";
27084
+ if (typeof a === "string" && a.trim() === "") return "$\u2014";
27085
+ const n = typeof a === "number" ? a : Number(a);
27086
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
27052
27087
  }
27053
27088
  function fmtClaimAmount(c) {
27054
27089
  return c.kind === "contribution" ? "contribution" : fmtAmount(c.amountUSD);
@@ -28355,35 +28390,62 @@ async function ensureForkExists(repoFullName, ghUser) {
28355
28390
  if (!isFork) throw new Error(`fork ${forkFullName} created but could not be verified as a fork`);
28356
28391
  return forkFullName;
28357
28392
  }
28393
+ function shouldStatePending(claim, approvalsChecked) {
28394
+ return Boolean(claim?.approval) && claim.approval.state === "pending" && Boolean(approvalsChecked);
28395
+ }
28396
+ function startableRow(c) {
28397
+ const bits = [fmtClaimAmount(c), sanitizeText(c.title)];
28398
+ if (c.repoFullName) bits.push(sanitizeText(c.repoFullName));
28399
+ return bits.join(" \xB7 ");
28400
+ }
28401
+ async function pickStartableClaim(claims, { prompt = ask, isTTY = process.stdin.isTTY } = {}) {
28402
+ const active = claims.listClaims({ active: true });
28403
+ const { approvalsChecked, approvalsUnavailable } = await syncFounderApprovals(claims, active);
28404
+ const startable = claims.listClaims({ active: true }).filter((c) => c.state === "claimed");
28405
+ if (startable.length === 0) {
28406
+ console.log("No claims ready to start. Claim one first: terminalhire claim <ref>");
28407
+ return null;
28408
+ }
28409
+ const interactive = Boolean(isTTY);
28410
+ console.log(`
28411
+ ${startable.length} claim${startable.length === 1 ? "" : "s"} ready to start:
28412
+ `);
28413
+ const width = String(startable.length).length;
28414
+ startable.forEach((c, i) => {
28415
+ console.log(` ${String(i + 1).padStart(width)}) ${startableRow(c)}`);
28416
+ if (!interactive) console.log(` terminalhire claim start ${c.id}`);
28417
+ });
28418
+ if (!interactive) {
28419
+ console.log("\nRun the command under the one you want to start.");
28420
+ return null;
28421
+ }
28422
+ const answer = await prompt(`
28423
+ Which one? (1-${startable.length}, or q to quit) `);
28424
+ if (answer === "" || /^q(uit)?$/i.test(answer)) return null;
28425
+ const n = Number.parseInt(answer, 10);
28426
+ if (!/^\d+$/.test(answer) || !Number.isInteger(n) || n < 1 || n > startable.length) {
28427
+ console.log(`
28428
+ Nothing started \u2014 '${answer}' is not one of 1-${startable.length}.`);
28429
+ return null;
28430
+ }
28431
+ return { id: startable[n - 1].id, approvalsChecked, approvalsUnavailable };
28432
+ }
28358
28433
  async function cmdStart(id, flags = {}) {
28359
28434
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
28435
+ let approvalsChecked = false;
28436
+ let approvalsUnavailable = false;
28360
28437
  if (!id) {
28361
- let list = claims.listClaims({ active: true });
28362
- await syncFounderApprovals(claims, list);
28363
- const startable = claims.listClaims({ active: true }).filter((c) => c.state === "claimed");
28364
- if (startable.length === 0) {
28365
- console.log("No claims ready to start. Claim one first: terminalhire claim <ref>");
28366
- return;
28367
- }
28368
- console.log(
28369
- `
28370
- ${startable.length} claim${startable.length === 1 ? "" : "s"} ready to start:
28371
- `
28372
- );
28373
- for (const c of startable) {
28374
- console.log(` ${c.title}`);
28375
- console.log(` terminalhire claim start ${c.id}`);
28376
- }
28377
- console.log("\nRun the command under the one you want to start.");
28378
- return;
28438
+ const picked = await pickStartableClaim(claims);
28439
+ if (!picked) return;
28440
+ ({ id, approvalsChecked, approvalsUnavailable } = picked);
28379
28441
  }
28380
28442
  let claim = claims.findClaim(id);
28381
28443
  if (!claim) {
28382
28444
  console.error(`terminalhire claim: no claim with id '${id}'.`);
28383
28445
  process.exit(1);
28384
28446
  }
28385
- if (claim.approval?.state === "pending") {
28386
- await syncFounderApprovals(claims, [claim]);
28447
+ if (claim.approval?.state === "pending" && !approvalsChecked) {
28448
+ ({ approvalsChecked, approvalsUnavailable } = await syncFounderApprovals(claims, [claim]));
28387
28449
  claim = claims.findClaim(id);
28388
28450
  }
28389
28451
  if (claim.worktreePath) {
@@ -28404,19 +28466,18 @@ When it's done: terminalhire claim submit ${id}`);
28404
28466
  }
28405
28467
  if (claim.approval) {
28406
28468
  console.log(`
28407
- ${claim.title}`);
28408
- if (claim.approval.state === "pending") {
28469
+ ${sanitizeText(claim.title)}`);
28470
+ console.log("\n No fork was attempted \u2014 founder postings are never forked or cloned. Your");
28471
+ console.log(" work slice is delivered through terminalhire, and your patch goes back the");
28472
+ console.log(" same way.");
28473
+ if (shouldStatePending(claim, approvalsChecked)) {
28474
+ console.log("\n Access is pending \u2014 the founder has not approved your claim yet.");
28475
+ } else if (approvalsUnavailable) {
28409
28476
  console.log(
28410
- "\n Access is pending \u2014 this approval-only posting needs the founder to approve"
28477
+ "\n Terminalhire could not check approvals right now; local state was preserved.\n The next step below still works \u2014 it asks the server directly."
28411
28478
  );
28412
- console.log(" your claim before any work can be delivered. No fork was attempted: founder");
28413
- console.log(" postings are never forked or cloned; once approved, your work slice is");
28414
- console.log(" delivered through terminalhire.");
28415
- } else {
28416
- console.log("\n No fork was attempted \u2014 founder postings are never forked or cloned. Your");
28417
- console.log(" work slice is delivered through terminalhire, and your patch goes back the");
28418
- console.log(" same way.");
28419
28479
  }
28480
+ printNextSteps([claim]);
28420
28481
  return;
28421
28482
  }
28422
28483
  if (flags.here) {
@@ -28654,7 +28715,7 @@ function renderRunView(run3, message) {
28654
28715
  }
28655
28716
  function terminalSafeLines(raw) {
28656
28717
  if (typeof raw !== "string" || raw === "") return [];
28657
- return raw.split(LINE_BREAKS).map((l) => l.replace(CONTROL_CHARS, ""));
28718
+ return raw.split(LINE_BREAKS).map((l) => l.replace(CONTROL_CHARS3, ""));
28658
28719
  }
28659
28720
  function terminalSafeInline(raw) {
28660
28721
  return terminalSafeLines(raw).join(" ");
@@ -30058,12 +30119,13 @@ async function run() {
30058
30119
  process.exit(1);
30059
30120
  }
30060
30121
  }
30061
- var TERMINALHIRE_DIR11, INDEX_CACHE_FILE2, CLAIM_PUSH_MARKER, REPO_CONTINUITY_NUDGE_MARKER, API_URL, 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, STAKE_MARKER, STANDDOWN_MARKER, OUR_MARKERS, STAKE_POST_TIMEOUT_MS, STAKE_POSTING_GRACE_MS, TAKE_BOT_REPOS, SUBMIT_ACCEPTS, REVISE_RECOVERY_STATES, PUSH_TOKEN_REFUSAL, SYNC_BACKGROUND_PUSH_ACTIVE_FIELD, ISSUE_OUTCOME_TERMINAL, RUNS_POLL_INTERVAL_MS, RUNS_POLL_ATTEMPTS, CLAIM_EVENT_LABEL, LINE_BREAKS, CONTROL_CHARS;
30122
+ var TERMINALHIRE_DIR11, INDEX_CACHE_FILE2, CLAIM_PUSH_MARKER, REPO_CONTINUITY_NUDGE_MARKER, API_URL, 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, STAKE_MARKER, STANDDOWN_MARKER, OUR_MARKERS, STAKE_POST_TIMEOUT_MS, STAKE_POSTING_GRACE_MS, TAKE_BOT_REPOS, SUBMIT_ACCEPTS, REVISE_RECOVERY_STATES, PUSH_TOKEN_REFUSAL, SYNC_BACKGROUND_PUSH_ACTIVE_FIELD, ISSUE_OUTCOME_TERMINAL, RUNS_POLL_INTERVAL_MS, RUNS_POLL_ATTEMPTS, CLAIM_EVENT_LABEL, LINE_BREAKS, CONTROL_CHARS3;
30062
30123
  var init_jpi_claim = __esm({
30063
30124
  "bin/jpi-claim.js"() {
30064
30125
  "use strict";
30065
30126
  init_src();
30066
30127
  init_open_url();
30128
+ init_sanitize();
30067
30129
  init_policy_acks();
30068
30130
  init_claims();
30069
30131
  init_state_dir();
@@ -30126,7 +30188,7 @@ var init_jpi_claim = __esm({
30126
30188
  rejected: "the founder rejected"
30127
30189
  };
30128
30190
  LINE_BREAKS = /\r\n|[\r\n\v\f\u0085\u2028\u2029]/;
30129
- CONTROL_CHARS = /[\u0000-\u001F\u007F-\u009F]/g;
30191
+ CONTROL_CHARS3 = /[\u0000-\u001F\u007F-\u009F]/g;
30130
30192
  }
30131
30193
  });
30132
30194
 
@@ -12562,8 +12562,24 @@ function nextPolledState(from, observed) {
12562
12562
  function nowISO() {
12563
12563
  return (/* @__PURE__ */ new Date()).toISOString();
12564
12564
  }
12565
+ function defangText(s) {
12566
+ return typeof s === "string" ? s.replace(CONTROL_CHARS, "") : s;
12567
+ }
12568
+ function finiteAmount(a) {
12569
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
12570
+ if (typeof a !== "string" || a.trim() === "") return null;
12571
+ const n = Number(a);
12572
+ return Number.isFinite(n) ? n : null;
12573
+ }
12565
12574
  function normalizeClaim(c) {
12566
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
12575
+ return {
12576
+ ...c,
12577
+ kind: c.kind ?? "bounty",
12578
+ policy: c.policy ?? null,
12579
+ title: defangText(c.title),
12580
+ repoFullName: defangText(c.repoFullName),
12581
+ amountUSD: finiteAmount(c.amountUSD)
12582
+ };
12567
12583
  }
12568
12584
  function readClaims() {
12569
12585
  try {
@@ -12685,7 +12701,7 @@ function acceptedPRRate(claims = readClaims()) {
12685
12701
  const merged = claims.filter((c) => c.state === "merged").length;
12686
12702
  return { merged, total, rate: total === 0 ? 0 : merged / total };
12687
12703
  }
12688
- var TERMINALHIRE_DIR8, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS2, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
12704
+ var TERMINALHIRE_DIR8, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS2, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS;
12689
12705
  var init_claims = __esm({
12690
12706
  "src/claims.ts"() {
12691
12707
  "use strict";
@@ -12741,6 +12757,7 @@ var init_claims = __esm({
12741
12757
  ]),
12742
12758
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
12743
12759
  };
12760
+ CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
12744
12761
  }
12745
12762
  });
12746
12763
 
@@ -1909,8 +1909,24 @@ function nextPolledState(from, observed) {
1909
1909
  function nowISO2() {
1910
1910
  return (/* @__PURE__ */ new Date()).toISOString();
1911
1911
  }
1912
+ function defangText(s) {
1913
+ return typeof s === "string" ? s.replace(CONTROL_CHARS, "") : s;
1914
+ }
1915
+ function finiteAmount(a) {
1916
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
1917
+ if (typeof a !== "string" || a.trim() === "") return null;
1918
+ const n = Number(a);
1919
+ return Number.isFinite(n) ? n : null;
1920
+ }
1912
1921
  function normalizeClaim(c) {
1913
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
1922
+ return {
1923
+ ...c,
1924
+ kind: c.kind ?? "bounty",
1925
+ policy: c.policy ?? null,
1926
+ title: defangText(c.title),
1927
+ repoFullName: defangText(c.repoFullName),
1928
+ amountUSD: finiteAmount(c.amountUSD)
1929
+ };
1914
1930
  }
1915
1931
  function readClaims() {
1916
1932
  try {
@@ -2032,7 +2048,7 @@ function acceptedPRRate(claims = readClaims()) {
2032
2048
  const merged = claims.filter((c) => c.state === "merged").length;
2033
2049
  return { merged, total, rate: total === 0 ? 0 : merged / total };
2034
2050
  }
2035
- var TERMINALHIRE_DIR4, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
2051
+ var TERMINALHIRE_DIR4, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS;
2036
2052
  var init_claims = __esm({
2037
2053
  "src/claims.ts"() {
2038
2054
  "use strict";
@@ -2088,6 +2104,7 @@ var init_claims = __esm({
2088
2104
  ]),
2089
2105
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
2090
2106
  };
2107
+ CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
2091
2108
  }
2092
2109
  });
2093
2110
 
@@ -92,7 +92,7 @@ function render() {
92
92
  const founderOpen = founderOpenCount(entry);
93
93
  const stale = sessionStale(entry) && unread === 0 && incoming === 0;
94
94
  const segments = [];
95
- if (approved > 0) segments.push(`\u2705 ${approved} approved \u2014 run: th claim list`);
95
+ if (approved > 0) segments.push(`\u2705 ${approved} approved \u2014 run: th claim start`);
96
96
  if (founderNeedsYou > 0) {
97
97
  segments.push(
98
98
  `\u{1F9ED} ${founderNeedsYou} claim${founderNeedsYou === 1 ? "" : "s"} await your decision \u2014 run: th bounties`
@@ -161,8 +161,25 @@ function nextPolledState(from, observed) {
161
161
  function nowISO() {
162
162
  return (/* @__PURE__ */ new Date()).toISOString();
163
163
  }
164
+ var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
165
+ function defangText(s) {
166
+ return typeof s === "string" ? s.replace(CONTROL_CHARS, "") : s;
167
+ }
168
+ function finiteAmount(a) {
169
+ if (typeof a === "number") return Number.isFinite(a) ? a : null;
170
+ if (typeof a !== "string" || a.trim() === "") return null;
171
+ const n = Number(a);
172
+ return Number.isFinite(n) ? n : null;
173
+ }
164
174
  function normalizeClaim(c) {
165
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
175
+ return {
176
+ ...c,
177
+ kind: c.kind ?? "bounty",
178
+ policy: c.policy ?? null,
179
+ title: defangText(c.title),
180
+ repoFullName: defangText(c.repoFullName),
181
+ amountUSD: finiteAmount(c.amountUSD)
182
+ };
166
183
  }
167
184
  function readClaims() {
168
185
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.41.0",
3
+ "version": "0.41.2",
4
4
  "description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Your profile never leaves your machine.",
5
5
  "repository": {
6
6
  "type": "git",