terminalhire 0.40.3 → 0.40.5

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.
@@ -90,6 +90,7 @@ var init_state_dir = __esm({
90
90
  // src/claims.ts
91
91
  var claims_exports = {};
92
92
  __export(claims_exports, {
93
+ CLAIM_STATES: () => CLAIM_STATES,
93
94
  PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
94
95
  acceptedPRRate: () => acceptedPRRate,
95
96
  countAwaitingFounderApproval: () => countAwaitingFounderApproval,
@@ -288,7 +289,7 @@ function acceptedPRRate(claims = readClaims()) {
288
289
  const merged = claims.filter((c) => c.state === "merged").length;
289
290
  return { merged, total, rate: total === 0 ? 0 : merged / total };
290
291
  }
291
- var TERMINALHIRE_DIR3, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
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;
292
293
  var init_claims = __esm({
293
294
  "src/claims.ts"() {
294
295
  "use strict";
@@ -299,6 +300,22 @@ var init_claims = __esm({
299
300
  LOCK_STALE_MS = Number(process.env.TERMINALHIRE_LOCK_STALE_MS) || 1e4;
300
301
  LOCK_RETRY_MS = Number(process.env.TERMINALHIRE_LOCK_RETRY_MS) || 25;
301
302
  LOCK_TIMEOUT_MS = Number(process.env.TERMINALHIRE_LOCK_TIMEOUT_MS) || 5e3;
303
+ CLAIM_STATES = Object.freeze([
304
+ "claimed",
305
+ // recorded, not started
306
+ "working",
307
+ // background executor running / work in progress
308
+ "in-review",
309
+ // gate ran / dev reviewing the diff
310
+ "ready",
311
+ // passed review, cleared to submit
312
+ "submitted",
313
+ // PR opened on the source platform (awaiting maintainer)
314
+ "merged",
315
+ // PR merged — accepted; counts toward the metric
316
+ "abandoned"
317
+ // released, or PR closed unmerged
318
+ ]);
302
319
  PUSHED_CLAIM_FIELDS = [
303
320
  "kind",
304
321
  "repoFullName",
@@ -10618,6 +10618,14 @@ function warnStderr(message) {
10618
10618
  process.stderr.write(`${message}
10619
10619
  `);
10620
10620
  }
10621
+ function makeWarnOnce() {
10622
+ const seen = /* @__PURE__ */ new Set();
10623
+ return (message) => {
10624
+ if (seen.has(message)) return;
10625
+ seen.add(message);
10626
+ warnStderr(message);
10627
+ };
10628
+ }
10621
10629
  function atomicWriteFileSync(filePath, content) {
10622
10630
  const dir = dirname(filePath);
10623
10631
  ensureStateDirForSecret(dir);
@@ -10663,11 +10671,11 @@ async function deleteKey() {
10663
10671
  if (e.code !== "ENOENT") throw e;
10664
10672
  }
10665
10673
  }
10666
- async function resolveKey(filePath, opts) {
10674
+ async function resolveKey(filePath, opts, warnOnce) {
10667
10675
  if (opts.keyPolicy === "keychain-required") {
10668
10676
  const key = await tryLoadFromKeytar();
10669
10677
  if (!key) {
10670
- warnStderr(
10678
+ warnOnce(
10671
10679
  `crypto-store: OS keychain unavailable \u2014 store at ${filePath} is disabled (no plaintext key file will be written)`
10672
10680
  );
10673
10681
  return null;
@@ -10677,8 +10685,9 @@ async function resolveKey(filePath, opts) {
10677
10685
  return loadOrCreateSharedKey();
10678
10686
  }
10679
10687
  function createEncryptedStore(filePath, opts) {
10688
+ const warnOnce = makeWarnOnce();
10680
10689
  async function read() {
10681
- const key = await resolveKey(filePath, opts);
10690
+ const key = await resolveKey(filePath, opts, warnOnce);
10682
10691
  if (!key) return opts.blank();
10683
10692
  if (!existsSync3(filePath)) return opts.blank();
10684
10693
  try {
@@ -10687,12 +10696,12 @@ function createEncryptedStore(filePath, opts) {
10687
10696
  const plaintext = decrypt(blob, key);
10688
10697
  return JSON.parse(plaintext);
10689
10698
  } catch {
10690
- warnStderr(`crypto-store: failed to decrypt ${filePath} \u2014 returning blank`);
10699
+ warnOnce(`crypto-store: failed to decrypt ${filePath} \u2014 returning blank`);
10691
10700
  return opts.blank();
10692
10701
  }
10693
10702
  }
10694
10703
  async function write(value) {
10695
- const key = await resolveKey(filePath, opts);
10704
+ const key = await resolveKey(filePath, opts, warnOnce);
10696
10705
  if (!key) return;
10697
10706
  const blob = encrypt(JSON.stringify(value), key);
10698
10707
  atomicWriteFileSync(filePath, JSON.stringify(blob, null, 2));
@@ -11122,6 +11131,7 @@ var init_repo_experience = __esm({
11122
11131
  // src/claims.ts
11123
11132
  var claims_exports = {};
11124
11133
  __export(claims_exports, {
11134
+ CLAIM_STATES: () => CLAIM_STATES,
11125
11135
  PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
11126
11136
  acceptedPRRate: () => acceptedPRRate,
11127
11137
  countAwaitingFounderApproval: () => countAwaitingFounderApproval,
@@ -11320,7 +11330,7 @@ function acceptedPRRate(claims = readClaims()) {
11320
11330
  const merged = claims.filter((c) => c.state === "merged").length;
11321
11331
  return { merged, total, rate: total === 0 ? 0 : merged / total };
11322
11332
  }
11323
- var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
11333
+ 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;
11324
11334
  var init_claims = __esm({
11325
11335
  "src/claims.ts"() {
11326
11336
  "use strict";
@@ -11331,6 +11341,22 @@ var init_claims = __esm({
11331
11341
  LOCK_STALE_MS = Number(process.env.TERMINALHIRE_LOCK_STALE_MS) || 1e4;
11332
11342
  LOCK_RETRY_MS = Number(process.env.TERMINALHIRE_LOCK_RETRY_MS) || 25;
11333
11343
  LOCK_TIMEOUT_MS = Number(process.env.TERMINALHIRE_LOCK_TIMEOUT_MS) || 5e3;
11344
+ CLAIM_STATES = Object.freeze([
11345
+ "claimed",
11346
+ // recorded, not started
11347
+ "working",
11348
+ // background executor running / work in progress
11349
+ "in-review",
11350
+ // gate ran / dev reviewing the diff
11351
+ "ready",
11352
+ // passed review, cleared to submit
11353
+ "submitted",
11354
+ // PR opened on the source platform (awaiting maintainer)
11355
+ "merged",
11356
+ // PR merged — accepted; counts toward the metric
11357
+ "abandoned"
11358
+ // released, or PR closed unmerged
11359
+ ]);
11334
11360
  PUSHED_CLAIM_FIELDS = [
11335
11361
  "kind",
11336
11362
  "repoFullName",
@@ -5478,6 +5478,14 @@ function warnStderr(message) {
5478
5478
  process.stderr.write(`${message}
5479
5479
  `);
5480
5480
  }
5481
+ function makeWarnOnce() {
5482
+ const seen = /* @__PURE__ */ new Set();
5483
+ return (message) => {
5484
+ if (seen.has(message)) return;
5485
+ seen.add(message);
5486
+ warnStderr(message);
5487
+ };
5488
+ }
5481
5489
  function atomicWriteFileSync(filePath, content) {
5482
5490
  const dir = dirname(filePath);
5483
5491
  ensureStateDirForSecret(dir);
@@ -5523,11 +5531,11 @@ async function deleteKey() {
5523
5531
  if (e.code !== "ENOENT") throw e;
5524
5532
  }
5525
5533
  }
5526
- async function resolveKey(filePath, opts) {
5534
+ async function resolveKey(filePath, opts, warnOnce) {
5527
5535
  if (opts.keyPolicy === "keychain-required") {
5528
5536
  const key = await tryLoadFromKeytar();
5529
5537
  if (!key) {
5530
- warnStderr(
5538
+ warnOnce(
5531
5539
  `crypto-store: OS keychain unavailable \u2014 store at ${filePath} is disabled (no plaintext key file will be written)`
5532
5540
  );
5533
5541
  return null;
@@ -5537,8 +5545,9 @@ async function resolveKey(filePath, opts) {
5537
5545
  return loadOrCreateSharedKey();
5538
5546
  }
5539
5547
  function createEncryptedStore(filePath, opts) {
5548
+ const warnOnce = makeWarnOnce();
5540
5549
  async function read() {
5541
- const key = await resolveKey(filePath, opts);
5550
+ const key = await resolveKey(filePath, opts, warnOnce);
5542
5551
  if (!key) return opts.blank();
5543
5552
  if (!existsSync9(filePath)) return opts.blank();
5544
5553
  try {
@@ -5547,12 +5556,12 @@ function createEncryptedStore(filePath, opts) {
5547
5556
  const plaintext = decrypt2(blob, key);
5548
5557
  return JSON.parse(plaintext);
5549
5558
  } catch {
5550
- warnStderr(`crypto-store: failed to decrypt ${filePath} \u2014 returning blank`);
5559
+ warnOnce(`crypto-store: failed to decrypt ${filePath} \u2014 returning blank`);
5551
5560
  return opts.blank();
5552
5561
  }
5553
5562
  }
5554
5563
  async function write(value) {
5555
- const key = await resolveKey(filePath, opts);
5564
+ const key = await resolveKey(filePath, opts, warnOnce);
5556
5565
  if (!key) return;
5557
5566
  const blob = encrypt2(JSON.stringify(value), key);
5558
5567
  atomicWriteFileSync(filePath, JSON.stringify(blob, null, 2));
@@ -10431,6 +10431,7 @@ var init_state_dir = __esm({
10431
10431
  // src/claims.ts
10432
10432
  var claims_exports = {};
10433
10433
  __export(claims_exports, {
10434
+ CLAIM_STATES: () => CLAIM_STATES,
10434
10435
  PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
10435
10436
  acceptedPRRate: () => acceptedPRRate,
10436
10437
  countAwaitingFounderApproval: () => countAwaitingFounderApproval,
@@ -10629,7 +10630,7 @@ function acceptedPRRate(claims = readClaims()) {
10629
10630
  const merged = claims.filter((c) => c.state === "merged").length;
10630
10631
  return { merged, total, rate: total === 0 ? 0 : merged / total };
10631
10632
  }
10632
- var TERMINALHIRE_DIR2, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
10633
+ 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;
10633
10634
  var init_claims = __esm({
10634
10635
  "src/claims.ts"() {
10635
10636
  "use strict";
@@ -10640,6 +10641,22 @@ var init_claims = __esm({
10640
10641
  LOCK_STALE_MS = Number(process.env.TERMINALHIRE_LOCK_STALE_MS) || 1e4;
10641
10642
  LOCK_RETRY_MS = Number(process.env.TERMINALHIRE_LOCK_RETRY_MS) || 25;
10642
10643
  LOCK_TIMEOUT_MS = Number(process.env.TERMINALHIRE_LOCK_TIMEOUT_MS) || 5e3;
10644
+ CLAIM_STATES = Object.freeze([
10645
+ "claimed",
10646
+ // recorded, not started
10647
+ "working",
10648
+ // background executor running / work in progress
10649
+ "in-review",
10650
+ // gate ran / dev reviewing the diff
10651
+ "ready",
10652
+ // passed review, cleared to submit
10653
+ "submitted",
10654
+ // PR opened on the source platform (awaiting maintainer)
10655
+ "merged",
10656
+ // PR merged — accepted; counts toward the metric
10657
+ "abandoned"
10658
+ // released, or PR closed unmerged
10659
+ ]);
10643
10660
  PUSHED_CLAIM_FIELDS = [
10644
10661
  "kind",
10645
10662
  "repoFullName",
@@ -24814,6 +24831,14 @@ function warnStderr(message) {
24814
24831
  process.stderr.write(`${message}
24815
24832
  `);
24816
24833
  }
24834
+ function makeWarnOnce() {
24835
+ const seen = /* @__PURE__ */ new Set();
24836
+ return (message) => {
24837
+ if (seen.has(message)) return;
24838
+ seen.add(message);
24839
+ warnStderr(message);
24840
+ };
24841
+ }
24817
24842
  function atomicWriteFileSync(filePath, content) {
24818
24843
  const dir = dirname4(filePath);
24819
24844
  ensureStateDirForSecret(dir);
@@ -24859,11 +24884,11 @@ async function deleteKey() {
24859
24884
  if (e.code !== "ENOENT") throw e;
24860
24885
  }
24861
24886
  }
24862
- async function resolveKey(filePath, opts) {
24887
+ async function resolveKey(filePath, opts, warnOnce) {
24863
24888
  if (opts.keyPolicy === "keychain-required") {
24864
24889
  const key = await tryLoadFromKeytar();
24865
24890
  if (!key) {
24866
- warnStderr(
24891
+ warnOnce(
24867
24892
  `crypto-store: OS keychain unavailable \u2014 store at ${filePath} is disabled (no plaintext key file will be written)`
24868
24893
  );
24869
24894
  return null;
@@ -24873,8 +24898,9 @@ async function resolveKey(filePath, opts) {
24873
24898
  return loadOrCreateSharedKey();
24874
24899
  }
24875
24900
  function createEncryptedStore(filePath, opts) {
24901
+ const warnOnce = makeWarnOnce();
24876
24902
  async function read() {
24877
- const key = await resolveKey(filePath, opts);
24903
+ const key = await resolveKey(filePath, opts, warnOnce);
24878
24904
  if (!key) return opts.blank();
24879
24905
  if (!existsSync6(filePath)) return opts.blank();
24880
24906
  try {
@@ -24883,12 +24909,12 @@ function createEncryptedStore(filePath, opts) {
24883
24909
  const plaintext = decrypt2(blob, key);
24884
24910
  return JSON.parse(plaintext);
24885
24911
  } catch {
24886
- warnStderr(`crypto-store: failed to decrypt ${filePath} \u2014 returning blank`);
24912
+ warnOnce(`crypto-store: failed to decrypt ${filePath} \u2014 returning blank`);
24887
24913
  return opts.blank();
24888
24914
  }
24889
24915
  }
24890
24916
  async function write(value) {
24891
- const key = await resolveKey(filePath, opts);
24917
+ const key = await resolveKey(filePath, opts, warnOnce);
24892
24918
  if (!key) return;
24893
24919
  const blob = encrypt2(JSON.stringify(value), key);
24894
24920
  atomicWriteFileSync(filePath, JSON.stringify(blob, null, 2));
@@ -26705,6 +26731,38 @@ function printMetric(rate) {
26705
26731
  console.log(`
26706
26732
  \u{1F4CA} Accepted-PR rate: ${rate.merged}/${rate.total} claims merged (${pct}%)`);
26707
26733
  }
26734
+ var SUBMIT_ACCEPTS = Object.freeze(["working", "ready"]);
26735
+ function nextStepFor(c) {
26736
+ const founder = Boolean(c.approval);
26737
+ switch (c.state) {
26738
+ case "claimed":
26739
+ return founder ? { cmd: `terminalhire claim slice ${c.id}`, why: "collect your granted work slice" } : { cmd: `terminalhire claim start ${c.id}`, why: "fork + clone into a worktree" };
26740
+ // NOT grouped with the two below. `cmdSubmit` accepts 'working' and 'ready'
26741
+ // and nothing else, so pointing an 'in-review' claim at submit would hand the
26742
+ // developer a command that exits 1 — and submit's own refusal then advises
26743
+ // `claim start`, which a founder claim can never use either. Two dead ends in
26744
+ // a row is worse than the silence this block replaced.
26745
+ case "in-review":
26746
+ return {
26747
+ cmd: `terminalhire claim update ${c.id} ready`,
26748
+ why: "clear the review gate \u2014 submit only accepts working/ready"
26749
+ };
26750
+ case "working":
26751
+ case "ready":
26752
+ return { cmd: `terminalhire claim submit ${c.id}`, why: "push the branch + open the PR" };
26753
+ case "submitted":
26754
+ return founder ? { cmd: `terminalhire claim runs ${c.id} --watch`, why: "read the CI result" } : { cmd: `terminalhire claim status ${c.id}`, why: "poll the PR merge state" };
26755
+ default:
26756
+ return null;
26757
+ }
26758
+ }
26759
+ function printNextSteps(list) {
26760
+ const steps = list.map(nextStepFor).filter(Boolean);
26761
+ if (steps.length === 0) return;
26762
+ const width = Math.max(...steps.map((s) => s.cmd.length));
26763
+ console.log("\n Next:");
26764
+ for (const s of steps) console.log(` ${s.cmd.padEnd(width)} \u2014 ${s.why}`);
26765
+ }
26708
26766
  async function resolveBounty(arg) {
26709
26767
  let bountyId, title, repoFullName, issueUrl, amountUSD, source, openPRsAtDiscovery, indexNativeId;
26710
26768
  let founderPosting = false;
@@ -27276,6 +27334,7 @@ ${list.length} ${active ? "active " : ""}claim${list.length === 1 ? "" : "s"}:
27276
27334
  console.log(` id: ${c.id}${pr}`);
27277
27335
  }
27278
27336
  printMetric(claims.acceptedPRRate());
27337
+ printNextSteps(list);
27279
27338
  try {
27280
27339
  if (await shouldNudgeUnpushed()) {
27281
27340
  console.log(
@@ -27424,7 +27483,7 @@ async function cmdStatus(id) {
27424
27483
  }
27425
27484
  async function cmdUpdate(id, state, prUrl) {
27426
27485
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
27427
- const VALID = ["claimed", "working", "in-review", "ready", "submitted", "merged", "abandoned"];
27486
+ const VALID = claims.CLAIM_STATES;
27428
27487
  if (!id || !state || !VALID.includes(state)) {
27429
27488
  console.error("Usage: terminalhire claim update <id> <state> [prUrl]");
27430
27489
  console.error(" state: " + VALID.join(" | "));
@@ -28284,7 +28343,7 @@ async function cmdSubmit(id, flags = {}) {
28284
28343
  console.error(`terminalhire claim: no claim with id '${id}'.`);
28285
28344
  process.exit(1);
28286
28345
  }
28287
- if (claim.state !== "working" && claim.state !== "ready") {
28346
+ if (!SUBMIT_ACCEPTS.includes(claim.state)) {
28288
28347
  console.error(
28289
28348
  `terminalhire claim: ${id} is '${claim.state}'. Submit runs once work has started ('working', via 'claim start') or the review gate cleared it ('ready'). Start it first:
28290
28349
  terminalhire claim start ${id}`
@@ -29236,6 +29295,7 @@ async function run() {
29236
29295
  export {
29237
29296
  AI_DISCLOSURE_NOTE,
29238
29297
  CLAIM_CONSENT_VERSION,
29298
+ SUBMIT_ACCEPTS,
29239
29299
  backgroundEnableFailed,
29240
29300
  buildAssignmentComment,
29241
29301
  buildPatchSubmission,
@@ -29261,9 +29321,11 @@ export {
29261
29321
  listMergedPRsReferencingIssue,
29262
29322
  listOpenPRsReferencingIssue,
29263
29323
  matchReferencingPrs,
29324
+ nextStepFor,
29264
29325
  normalizeIntent,
29265
29326
  pickBodySource,
29266
29327
  pickExistingPr,
29328
+ printNextSteps,
29267
29329
  renderRunView,
29268
29330
  renderServerRefusal,
29269
29331
  resolveBounty,
@@ -2742,6 +2742,14 @@ function warnStderr(message) {
2742
2742
  process.stderr.write(`${message}
2743
2743
  `);
2744
2744
  }
2745
+ function makeWarnOnce() {
2746
+ const seen = /* @__PURE__ */ new Set();
2747
+ return (message) => {
2748
+ if (seen.has(message)) return;
2749
+ seen.add(message);
2750
+ warnStderr(message);
2751
+ };
2752
+ }
2745
2753
  function atomicWriteFileSync(filePath, content) {
2746
2754
  const dir = dirname(filePath);
2747
2755
  ensureStateDirForSecret(dir);
@@ -2787,11 +2795,11 @@ async function deleteKey() {
2787
2795
  if (e.code !== "ENOENT") throw e;
2788
2796
  }
2789
2797
  }
2790
- async function resolveKey(filePath, opts) {
2798
+ async function resolveKey(filePath, opts, warnOnce) {
2791
2799
  if (opts.keyPolicy === "keychain-required") {
2792
2800
  const key = await tryLoadFromKeytar();
2793
2801
  if (!key) {
2794
- warnStderr(
2802
+ warnOnce(
2795
2803
  `crypto-store: OS keychain unavailable \u2014 store at ${filePath} is disabled (no plaintext key file will be written)`
2796
2804
  );
2797
2805
  return null;
@@ -2801,8 +2809,9 @@ async function resolveKey(filePath, opts) {
2801
2809
  return loadOrCreateSharedKey();
2802
2810
  }
2803
2811
  function createEncryptedStore(filePath, opts) {
2812
+ const warnOnce = makeWarnOnce();
2804
2813
  async function read() {
2805
- const key = await resolveKey(filePath, opts);
2814
+ const key = await resolveKey(filePath, opts, warnOnce);
2806
2815
  if (!key) return opts.blank();
2807
2816
  if (!existsSync4(filePath)) return opts.blank();
2808
2817
  try {
@@ -2811,12 +2820,12 @@ function createEncryptedStore(filePath, opts) {
2811
2820
  const plaintext = decrypt(blob, key);
2812
2821
  return JSON.parse(plaintext);
2813
2822
  } catch {
2814
- warnStderr(`crypto-store: failed to decrypt ${filePath} \u2014 returning blank`);
2823
+ warnOnce(`crypto-store: failed to decrypt ${filePath} \u2014 returning blank`);
2815
2824
  return opts.blank();
2816
2825
  }
2817
2826
  }
2818
2827
  async function write(value) {
2819
- const key = await resolveKey(filePath, opts);
2828
+ const key = await resolveKey(filePath, opts, warnOnce);
2820
2829
  if (!key) return;
2821
2830
  const blob = encrypt(JSON.stringify(value), key);
2822
2831
  atomicWriteFileSync(filePath, JSON.stringify(blob, null, 2));
@@ -3515,6 +3524,7 @@ var init_repo_experience = __esm({
3515
3524
  // src/claims.ts
3516
3525
  var claims_exports = {};
3517
3526
  __export(claims_exports, {
3527
+ CLAIM_STATES: () => CLAIM_STATES,
3518
3528
  PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
3519
3529
  acceptedPRRate: () => acceptedPRRate,
3520
3530
  countAwaitingFounderApproval: () => countAwaitingFounderApproval,
@@ -3713,7 +3723,7 @@ function acceptedPRRate(claims = readClaims()) {
3713
3723
  const merged = claims.filter((c) => c.state === "merged").length;
3714
3724
  return { merged, total, rate: total === 0 ? 0 : merged / total };
3715
3725
  }
3716
- var TERMINALHIRE_DIR7, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
3726
+ 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;
3717
3727
  var init_claims = __esm({
3718
3728
  "src/claims.ts"() {
3719
3729
  "use strict";
@@ -3724,6 +3734,22 @@ var init_claims = __esm({
3724
3734
  LOCK_STALE_MS = Number(process.env.TERMINALHIRE_LOCK_STALE_MS) || 1e4;
3725
3735
  LOCK_RETRY_MS = Number(process.env.TERMINALHIRE_LOCK_RETRY_MS) || 25;
3726
3736
  LOCK_TIMEOUT_MS = Number(process.env.TERMINALHIRE_LOCK_TIMEOUT_MS) || 5e3;
3737
+ CLAIM_STATES = Object.freeze([
3738
+ "claimed",
3739
+ // recorded, not started
3740
+ "working",
3741
+ // background executor running / work in progress
3742
+ "in-review",
3743
+ // gate ran / dev reviewing the diff
3744
+ "ready",
3745
+ // passed review, cleared to submit
3746
+ "submitted",
3747
+ // PR opened on the source platform (awaiting maintainer)
3748
+ "merged",
3749
+ // PR merged — accepted; counts toward the metric
3750
+ "abandoned"
3751
+ // released, or PR closed unmerged
3752
+ ]);
3727
3753
  PUSHED_CLAIM_FIELDS = [
3728
3754
  "kind",
3729
3755
  "repoFullName",
@@ -10566,6 +10566,14 @@ function warnStderr(message) {
10566
10566
  process.stderr.write(`${message}
10567
10567
  `);
10568
10568
  }
10569
+ function makeWarnOnce() {
10570
+ const seen = /* @__PURE__ */ new Set();
10571
+ return (message) => {
10572
+ if (seen.has(message)) return;
10573
+ seen.add(message);
10574
+ warnStderr(message);
10575
+ };
10576
+ }
10569
10577
  function atomicWriteFileSync(filePath, content) {
10570
10578
  const dir = dirname(filePath);
10571
10579
  ensureStateDirForSecret(dir);
@@ -10611,11 +10619,11 @@ async function deleteKey() {
10611
10619
  if (e.code !== "ENOENT") throw e;
10612
10620
  }
10613
10621
  }
10614
- async function resolveKey(filePath, opts) {
10622
+ async function resolveKey(filePath, opts, warnOnce) {
10615
10623
  if (opts.keyPolicy === "keychain-required") {
10616
10624
  const key = await tryLoadFromKeytar();
10617
10625
  if (!key) {
10618
- warnStderr(
10626
+ warnOnce(
10619
10627
  `crypto-store: OS keychain unavailable \u2014 store at ${filePath} is disabled (no plaintext key file will be written)`
10620
10628
  );
10621
10629
  return null;
@@ -10625,8 +10633,9 @@ async function resolveKey(filePath, opts) {
10625
10633
  return loadOrCreateSharedKey();
10626
10634
  }
10627
10635
  function createEncryptedStore(filePath, opts) {
10636
+ const warnOnce = makeWarnOnce();
10628
10637
  async function read() {
10629
- const key = await resolveKey(filePath, opts);
10638
+ const key = await resolveKey(filePath, opts, warnOnce);
10630
10639
  if (!key) return opts.blank();
10631
10640
  if (!existsSync3(filePath)) return opts.blank();
10632
10641
  try {
@@ -10635,12 +10644,12 @@ function createEncryptedStore(filePath, opts) {
10635
10644
  const plaintext = decrypt(blob, key);
10636
10645
  return JSON.parse(plaintext);
10637
10646
  } catch {
10638
- warnStderr(`crypto-store: failed to decrypt ${filePath} \u2014 returning blank`);
10647
+ warnOnce(`crypto-store: failed to decrypt ${filePath} \u2014 returning blank`);
10639
10648
  return opts.blank();
10640
10649
  }
10641
10650
  }
10642
10651
  async function write(value) {
10643
- const key = await resolveKey(filePath, opts);
10652
+ const key = await resolveKey(filePath, opts, warnOnce);
10644
10653
  if (!key) return;
10645
10654
  const blob = encrypt(JSON.stringify(value), key);
10646
10655
  atomicWriteFileSync(filePath, JSON.stringify(blob, null, 2));