terminalhire 0.40.2 → 0.40.4

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",
@@ -21,8 +21,7 @@ function gate(openIds, seenIds) {
21
21
  function computeFounderPaid(index, previous) {
22
22
  const open = openPaidIds(index);
23
23
  const prior = previous && previous.acknowledged;
24
- if (!Array.isArray(prior)) return { count: 0, acknowledged: open };
25
- return gate(open, prior);
24
+ return gate(open, Array.isArray(prior) ? prior : []);
26
25
  }
27
26
  function acknowledgeFounderPaid({ shown = [], open = [], previous } = {}) {
28
27
  const prior = previous && Array.isArray(previous.acknowledged) ? previous.acknowledged : [];
@@ -11122,6 +11122,7 @@ var init_repo_experience = __esm({
11122
11122
  // src/claims.ts
11123
11123
  var claims_exports = {};
11124
11124
  __export(claims_exports, {
11125
+ CLAIM_STATES: () => CLAIM_STATES,
11125
11126
  PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
11126
11127
  acceptedPRRate: () => acceptedPRRate,
11127
11128
  countAwaitingFounderApproval: () => countAwaitingFounderApproval,
@@ -11320,7 +11321,7 @@ function acceptedPRRate(claims = readClaims()) {
11320
11321
  const merged = claims.filter((c) => c.state === "merged").length;
11321
11322
  return { merged, total, rate: total === 0 ? 0 : merged / total };
11322
11323
  }
11323
- var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
11324
+ 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
11325
  var init_claims = __esm({
11325
11326
  "src/claims.ts"() {
11326
11327
  "use strict";
@@ -11331,6 +11332,22 @@ var init_claims = __esm({
11331
11332
  LOCK_STALE_MS = Number(process.env.TERMINALHIRE_LOCK_STALE_MS) || 1e4;
11332
11333
  LOCK_RETRY_MS = Number(process.env.TERMINALHIRE_LOCK_RETRY_MS) || 25;
11333
11334
  LOCK_TIMEOUT_MS = Number(process.env.TERMINALHIRE_LOCK_TIMEOUT_MS) || 5e3;
11335
+ CLAIM_STATES = Object.freeze([
11336
+ "claimed",
11337
+ // recorded, not started
11338
+ "working",
11339
+ // background executor running / work in progress
11340
+ "in-review",
11341
+ // gate ran / dev reviewing the diff
11342
+ "ready",
11343
+ // passed review, cleared to submit
11344
+ "submitted",
11345
+ // PR opened on the source platform (awaiting maintainer)
11346
+ "merged",
11347
+ // PR merged — accepted; counts toward the metric
11348
+ "abandoned"
11349
+ // released, or PR closed unmerged
11350
+ ]);
11334
11351
  PUSHED_CLAIM_FIELDS = [
11335
11352
  "kind",
11336
11353
  "repoFullName",
@@ -11387,8 +11404,7 @@ function gate(openIds, seenIds) {
11387
11404
  function computeFounderPaid(index, previous) {
11388
11405
  const open = openPaidIds(index);
11389
11406
  const prior = previous && previous.acknowledged;
11390
- if (!Array.isArray(prior)) return { count: 0, acknowledged: open };
11391
- return gate(open, prior);
11407
+ return gate(open, Array.isArray(prior) ? prior : []);
11392
11408
  }
11393
11409
  function acknowledgeFounderPaid({ shown = [], open = [], previous } = {}) {
11394
11410
  const prior = previous && Array.isArray(previous.acknowledged) ? previous.acknowledged : [];
@@ -11465,7 +11481,7 @@ function readIndexCache() {
11465
11481
  }
11466
11482
  }
11467
11483
  function writeIndexCache(index) {
11468
- updateIndexCache({ index });
11484
+ updateIndexCache({ index, indexETag: "" });
11469
11485
  }
11470
11486
  async function fetchIndex() {
11471
11487
  const cached = readIndexCache();
@@ -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",
@@ -26705,6 +26722,38 @@ function printMetric(rate) {
26705
26722
  console.log(`
26706
26723
  \u{1F4CA} Accepted-PR rate: ${rate.merged}/${rate.total} claims merged (${pct}%)`);
26707
26724
  }
26725
+ var SUBMIT_ACCEPTS = Object.freeze(["working", "ready"]);
26726
+ function nextStepFor(c) {
26727
+ const founder = Boolean(c.approval);
26728
+ switch (c.state) {
26729
+ case "claimed":
26730
+ 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" };
26731
+ // NOT grouped with the two below. `cmdSubmit` accepts 'working' and 'ready'
26732
+ // and nothing else, so pointing an 'in-review' claim at submit would hand the
26733
+ // developer a command that exits 1 — and submit's own refusal then advises
26734
+ // `claim start`, which a founder claim can never use either. Two dead ends in
26735
+ // a row is worse than the silence this block replaced.
26736
+ case "in-review":
26737
+ return {
26738
+ cmd: `terminalhire claim update ${c.id} ready`,
26739
+ why: "clear the review gate \u2014 submit only accepts working/ready"
26740
+ };
26741
+ case "working":
26742
+ case "ready":
26743
+ return { cmd: `terminalhire claim submit ${c.id}`, why: "push the branch + open the PR" };
26744
+ case "submitted":
26745
+ 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" };
26746
+ default:
26747
+ return null;
26748
+ }
26749
+ }
26750
+ function printNextSteps(list) {
26751
+ const steps = list.map(nextStepFor).filter(Boolean);
26752
+ if (steps.length === 0) return;
26753
+ const width = Math.max(...steps.map((s) => s.cmd.length));
26754
+ console.log("\n Next:");
26755
+ for (const s of steps) console.log(` ${s.cmd.padEnd(width)} \u2014 ${s.why}`);
26756
+ }
26708
26757
  async function resolveBounty(arg) {
26709
26758
  let bountyId, title, repoFullName, issueUrl, amountUSD, source, openPRsAtDiscovery, indexNativeId;
26710
26759
  let founderPosting = false;
@@ -27276,6 +27325,7 @@ ${list.length} ${active ? "active " : ""}claim${list.length === 1 ? "" : "s"}:
27276
27325
  console.log(` id: ${c.id}${pr}`);
27277
27326
  }
27278
27327
  printMetric(claims.acceptedPRRate());
27328
+ printNextSteps(list);
27279
27329
  try {
27280
27330
  if (await shouldNudgeUnpushed()) {
27281
27331
  console.log(
@@ -27424,7 +27474,7 @@ async function cmdStatus(id) {
27424
27474
  }
27425
27475
  async function cmdUpdate(id, state, prUrl) {
27426
27476
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
27427
- const VALID = ["claimed", "working", "in-review", "ready", "submitted", "merged", "abandoned"];
27477
+ const VALID = claims.CLAIM_STATES;
27428
27478
  if (!id || !state || !VALID.includes(state)) {
27429
27479
  console.error("Usage: terminalhire claim update <id> <state> [prUrl]");
27430
27480
  console.error(" state: " + VALID.join(" | "));
@@ -28284,7 +28334,7 @@ async function cmdSubmit(id, flags = {}) {
28284
28334
  console.error(`terminalhire claim: no claim with id '${id}'.`);
28285
28335
  process.exit(1);
28286
28336
  }
28287
- if (claim.state !== "working" && claim.state !== "ready") {
28337
+ if (!SUBMIT_ACCEPTS.includes(claim.state)) {
28288
28338
  console.error(
28289
28339
  `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
28340
  terminalhire claim start ${id}`
@@ -29236,6 +29286,7 @@ async function run() {
29236
29286
  export {
29237
29287
  AI_DISCLOSURE_NOTE,
29238
29288
  CLAIM_CONSENT_VERSION,
29289
+ SUBMIT_ACCEPTS,
29239
29290
  backgroundEnableFailed,
29240
29291
  buildAssignmentComment,
29241
29292
  buildPatchSubmission,
@@ -29261,9 +29312,11 @@ export {
29261
29312
  listMergedPRsReferencingIssue,
29262
29313
  listOpenPRsReferencingIssue,
29263
29314
  matchReferencingPrs,
29315
+ nextStepFor,
29264
29316
  normalizeIntent,
29265
29317
  pickBodySource,
29266
29318
  pickExistingPr,
29319
+ printNextSteps,
29267
29320
  renderRunView,
29268
29321
  renderServerRefusal,
29269
29322
  resolveBounty,
@@ -3515,6 +3515,7 @@ var init_repo_experience = __esm({
3515
3515
  // src/claims.ts
3516
3516
  var claims_exports = {};
3517
3517
  __export(claims_exports, {
3518
+ CLAIM_STATES: () => CLAIM_STATES,
3518
3519
  PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
3519
3520
  acceptedPRRate: () => acceptedPRRate,
3520
3521
  countAwaitingFounderApproval: () => countAwaitingFounderApproval,
@@ -3713,7 +3714,7 @@ function acceptedPRRate(claims = readClaims()) {
3713
3714
  const merged = claims.filter((c) => c.state === "merged").length;
3714
3715
  return { merged, total, rate: total === 0 ? 0 : merged / total };
3715
3716
  }
3716
- var TERMINALHIRE_DIR7, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
3717
+ 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
3718
  var init_claims = __esm({
3718
3719
  "src/claims.ts"() {
3719
3720
  "use strict";
@@ -3724,6 +3725,22 @@ var init_claims = __esm({
3724
3725
  LOCK_STALE_MS = Number(process.env.TERMINALHIRE_LOCK_STALE_MS) || 1e4;
3725
3726
  LOCK_RETRY_MS = Number(process.env.TERMINALHIRE_LOCK_RETRY_MS) || 25;
3726
3727
  LOCK_TIMEOUT_MS = Number(process.env.TERMINALHIRE_LOCK_TIMEOUT_MS) || 5e3;
3728
+ CLAIM_STATES = Object.freeze([
3729
+ "claimed",
3730
+ // recorded, not started
3731
+ "working",
3732
+ // background executor running / work in progress
3733
+ "in-review",
3734
+ // gate ran / dev reviewing the diff
3735
+ "ready",
3736
+ // passed review, cleared to submit
3737
+ "submitted",
3738
+ // PR opened on the source platform (awaiting maintainer)
3739
+ "merged",
3740
+ // PR merged — accepted; counts toward the metric
3741
+ "abandoned"
3742
+ // released, or PR closed unmerged
3743
+ ]);
3727
3744
  PUSHED_CLAIM_FIELDS = [
3728
3745
  "kind",
3729
3746
  "repoFullName",
@@ -4095,7 +4112,7 @@ function readIndexCache() {
4095
4112
  }
4096
4113
  }
4097
4114
  function writeIndexCache(index) {
4098
- updateIndexCache({ index });
4115
+ updateIndexCache({ index, indexETag: "" });
4099
4116
  }
4100
4117
  async function fetchIndex(fetchImpl, useCache = true) {
4101
4118
  if (useCache) {
@@ -13710,7 +13710,7 @@ function readIndexCache() {
13710
13710
  }
13711
13711
  }
13712
13712
  function writeIndexCache(index) {
13713
- updateIndexCache({ index });
13713
+ updateIndexCache({ index, indexETag: "" });
13714
13714
  }
13715
13715
  async function fetchIndex() {
13716
13716
  const cached2 = readIndexCache();
@@ -14859,6 +14859,7 @@ var init_repo_experience = __esm({
14859
14859
  // src/claims.ts
14860
14860
  var claims_exports = {};
14861
14861
  __export(claims_exports, {
14862
+ CLAIM_STATES: () => CLAIM_STATES,
14862
14863
  PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
14863
14864
  acceptedPRRate: () => acceptedPRRate,
14864
14865
  countAwaitingFounderApproval: () => countAwaitingFounderApproval,
@@ -15057,7 +15058,7 @@ function acceptedPRRate(claims = readClaims()) {
15057
15058
  const merged = claims.filter((c) => c.state === "merged").length;
15058
15059
  return { merged, total, rate: total === 0 ? 0 : merged / total };
15059
15060
  }
15060
- var TERMINALHIRE_DIR11, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS2, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
15061
+ 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;
15061
15062
  var init_claims = __esm({
15062
15063
  "src/claims.ts"() {
15063
15064
  "use strict";
@@ -15068,6 +15069,22 @@ var init_claims = __esm({
15068
15069
  LOCK_STALE_MS2 = Number(process.env.TERMINALHIRE_LOCK_STALE_MS) || 1e4;
15069
15070
  LOCK_RETRY_MS = Number(process.env.TERMINALHIRE_LOCK_RETRY_MS) || 25;
15070
15071
  LOCK_TIMEOUT_MS = Number(process.env.TERMINALHIRE_LOCK_TIMEOUT_MS) || 5e3;
15072
+ CLAIM_STATES = Object.freeze([
15073
+ "claimed",
15074
+ // recorded, not started
15075
+ "working",
15076
+ // background executor running / work in progress
15077
+ "in-review",
15078
+ // gate ran / dev reviewing the diff
15079
+ "ready",
15080
+ // passed review, cleared to submit
15081
+ "submitted",
15082
+ // PR opened on the source platform (awaiting maintainer)
15083
+ "merged",
15084
+ // PR merged — accepted; counts toward the metric
15085
+ "abandoned"
15086
+ // released, or PR closed unmerged
15087
+ ]);
15071
15088
  PUSHED_CLAIM_FIELDS = [
15072
15089
  "kind",
15073
15090
  "repoFullName",
@@ -15124,8 +15141,7 @@ function gate(openIds, seenIds) {
15124
15141
  function computeFounderPaid(index, previous) {
15125
15142
  const open3 = openPaidIds(index);
15126
15143
  const prior = previous && previous.acknowledged;
15127
- if (!Array.isArray(prior)) return { count: 0, acknowledged: open3 };
15128
- return gate(open3, prior);
15144
+ return gate(open3, Array.isArray(prior) ? prior : []);
15129
15145
  }
15130
15146
  function acknowledgeFounderPaid({ shown = [], open: open3 = [], previous } = {}) {
15131
15147
  const prior = previous && Array.isArray(previous.acknowledged) ? previous.acknowledged : [];
@@ -15165,7 +15181,7 @@ function readIndexCache2() {
15165
15181
  }
15166
15182
  }
15167
15183
  function writeIndexCache2(index) {
15168
- updateIndexCache({ index });
15184
+ updateIndexCache({ index, indexETag: "" });
15169
15185
  }
15170
15186
  async function fetchIndex2() {
15171
15187
  const cached2 = readIndexCache2();
@@ -15659,7 +15675,7 @@ function readIndexCache3() {
15659
15675
  }
15660
15676
  }
15661
15677
  function writeIndexCache3(index) {
15662
- updateIndexCache({ index });
15678
+ updateIndexCache({ index, indexETag: "" });
15663
15679
  }
15664
15680
  async function fetchIndex3(fetchImpl, useCache = true) {
15665
15681
  if (useCache) {
@@ -30463,6 +30479,7 @@ var jpi_claim_exports = {};
30463
30479
  __export(jpi_claim_exports, {
30464
30480
  AI_DISCLOSURE_NOTE: () => AI_DISCLOSURE_NOTE,
30465
30481
  CLAIM_CONSENT_VERSION: () => CLAIM_CONSENT_VERSION,
30482
+ SUBMIT_ACCEPTS: () => SUBMIT_ACCEPTS,
30466
30483
  backgroundEnableFailed: () => backgroundEnableFailed,
30467
30484
  buildAssignmentComment: () => buildAssignmentComment,
30468
30485
  buildPatchSubmission: () => buildPatchSubmission,
@@ -30488,9 +30505,11 @@ __export(jpi_claim_exports, {
30488
30505
  listMergedPRsReferencingIssue: () => listMergedPRsReferencingIssue,
30489
30506
  listOpenPRsReferencingIssue: () => listOpenPRsReferencingIssue,
30490
30507
  matchReferencingPrs: () => matchReferencingPrs,
30508
+ nextStepFor: () => nextStepFor,
30491
30509
  normalizeIntent: () => normalizeIntent,
30492
30510
  pickBodySource: () => pickBodySource,
30493
30511
  pickExistingPr: () => pickExistingPr,
30512
+ printNextSteps: () => printNextSteps,
30494
30513
  renderRunView: () => renderRunView,
30495
30514
  renderServerRefusal: () => renderServerRefusal,
30496
30515
  resolveBounty: () => resolveBounty,
@@ -31167,6 +31186,37 @@ function printMetric(rate) {
31167
31186
  console.log(`
31168
31187
  \u{1F4CA} Accepted-PR rate: ${rate.merged}/${rate.total} claims merged (${pct2}%)`);
31169
31188
  }
31189
+ function nextStepFor(c) {
31190
+ const founder = Boolean(c.approval);
31191
+ switch (c.state) {
31192
+ case "claimed":
31193
+ 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" };
31194
+ // NOT grouped with the two below. `cmdSubmit` accepts 'working' and 'ready'
31195
+ // and nothing else, so pointing an 'in-review' claim at submit would hand the
31196
+ // developer a command that exits 1 — and submit's own refusal then advises
31197
+ // `claim start`, which a founder claim can never use either. Two dead ends in
31198
+ // a row is worse than the silence this block replaced.
31199
+ case "in-review":
31200
+ return {
31201
+ cmd: `terminalhire claim update ${c.id} ready`,
31202
+ why: "clear the review gate \u2014 submit only accepts working/ready"
31203
+ };
31204
+ case "working":
31205
+ case "ready":
31206
+ return { cmd: `terminalhire claim submit ${c.id}`, why: "push the branch + open the PR" };
31207
+ case "submitted":
31208
+ 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" };
31209
+ default:
31210
+ return null;
31211
+ }
31212
+ }
31213
+ function printNextSteps(list) {
31214
+ const steps = list.map(nextStepFor).filter(Boolean);
31215
+ if (steps.length === 0) return;
31216
+ const width = Math.max(...steps.map((s) => s.cmd.length));
31217
+ console.log("\n Next:");
31218
+ for (const s of steps) console.log(` ${s.cmd.padEnd(width)} \u2014 ${s.why}`);
31219
+ }
31170
31220
  async function resolveBounty(arg) {
31171
31221
  let bountyId, title, repoFullName, issueUrl, amountUSD, source, openPRsAtDiscovery, indexNativeId;
31172
31222
  let founderPosting = false;
@@ -31738,6 +31788,7 @@ ${list.length} ${active ? "active " : ""}claim${list.length === 1 ? "" : "s"}:
31738
31788
  console.log(` id: ${c.id}${pr}`);
31739
31789
  }
31740
31790
  printMetric(claims.acceptedPRRate());
31791
+ printNextSteps(list);
31741
31792
  try {
31742
31793
  if (await shouldNudgeUnpushed()) {
31743
31794
  console.log(
@@ -31885,7 +31936,7 @@ async function cmdStatus(id) {
31885
31936
  }
31886
31937
  async function cmdUpdate(id, state, prUrl) {
31887
31938
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
31888
- const VALID = ["claimed", "working", "in-review", "ready", "submitted", "merged", "abandoned"];
31939
+ const VALID = claims.CLAIM_STATES;
31889
31940
  if (!id || !state || !VALID.includes(state)) {
31890
31941
  console.error("Usage: terminalhire claim update <id> <state> [prUrl]");
31891
31942
  console.error(" state: " + VALID.join(" | "));
@@ -32743,7 +32794,7 @@ async function cmdSubmit(id, flags = {}) {
32743
32794
  console.error(`terminalhire claim: no claim with id '${id}'.`);
32744
32795
  process.exit(1);
32745
32796
  }
32746
- if (claim.state !== "working" && claim.state !== "ready") {
32797
+ if (!SUBMIT_ACCEPTS.includes(claim.state)) {
32747
32798
  console.error(
32748
32799
  `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:
32749
32800
  terminalhire claim start ${id}`
@@ -33692,7 +33743,7 @@ async function run7() {
33692
33743
  process.exit(1);
33693
33744
  }
33694
33745
  }
33695
- var TERMINALHIRE_DIR17, INDEX_CACHE_FILE5, CLAIM_PUSH_MARKER, REPO_CONTINUITY_NUDGE_MARKER, API_URL6, CLAIM_SYNC_BASE2, 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, ISSUE_OUTCOME_TERMINAL, RUNS_POLL_INTERVAL_MS, RUNS_POLL_ATTEMPTS;
33746
+ var TERMINALHIRE_DIR17, INDEX_CACHE_FILE5, CLAIM_PUSH_MARKER, REPO_CONTINUITY_NUDGE_MARKER, API_URL6, CLAIM_SYNC_BASE2, 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, ISSUE_OUTCOME_TERMINAL, RUNS_POLL_INTERVAL_MS, RUNS_POLL_ATTEMPTS;
33696
33747
  var init_jpi_claim = __esm({
33697
33748
  "bin/jpi-claim.js"() {
33698
33749
  "use strict";
@@ -33724,6 +33775,7 @@ var init_jpi_claim = __esm({
33724
33775
  STAKE_POST_TIMEOUT_MS = 3e4;
33725
33776
  STAKE_POSTING_GRACE_MS = STAKE_POST_TIMEOUT_MS + 15e3;
33726
33777
  TAKE_BOT_REPOS = /* @__PURE__ */ new Set(["paradedb/paradedb"]);
33778
+ SUBMIT_ACCEPTS = Object.freeze(["working", "ready"]);
33727
33779
  ISSUE_OUTCOME_TERMINAL = /* @__PURE__ */ new Set(["own-merge", "other-merge", "closed-unmerged"]);
33728
33780
  RUNS_POLL_INTERVAL_MS = 15e3;
33729
33781
  RUNS_POLL_ATTEMPTS = 20;
@@ -63850,18 +63902,41 @@ import { hostname, homedir as osHomedir } from "os";
63850
63902
  async function run26() {
63851
63903
  try {
63852
63904
  let index;
63905
+ let indexETag;
63906
+ const cachedForRevalidation = readCacheEntry() ?? {};
63907
+ let sendValidator = typeof cachedForRevalidation.indexETag === "string" && cachedForRevalidation.indexETag !== "";
63853
63908
  for (let attempt2 = 1; ; attempt2++) {
63854
63909
  try {
63855
63910
  const res = await fetch(`${API_URL8}/api/index`, {
63856
63911
  signal: AbortSignal.timeout(15e3),
63857
- headers: { Accept: "application/json" }
63912
+ headers: sendValidator ? { Accept: "application/json", "If-None-Match": cachedForRevalidation.indexETag } : { Accept: "application/json" }
63858
63913
  });
63914
+ if (res.status === 304) {
63915
+ const cached2 = cachedForRevalidation.index;
63916
+ if (cached2 && Array.isArray(cached2.jobs)) {
63917
+ index = cached2;
63918
+ indexETag = cachedForRevalidation.indexETag;
63919
+ break;
63920
+ }
63921
+ sendValidator = false;
63922
+ if (attempt2 >= 2) {
63923
+ process.stderr.write(
63924
+ "terminalhire refresh: 304 with no usable cached index \u2014 giving up this tick\n"
63925
+ );
63926
+ process.exit(1);
63927
+ }
63928
+ process.stderr.write(
63929
+ "terminalhire refresh: 304 but the cached index is unusable (re-fetching in full)\n"
63930
+ );
63931
+ continue;
63932
+ }
63859
63933
  if (!res.ok) {
63860
63934
  process.stderr.write(`terminalhire refresh: index fetch failed (HTTP ${res.status})
63861
63935
  `);
63862
63936
  process.exit(1);
63863
63937
  }
63864
63938
  index = await res.json();
63939
+ indexETag = res.headers.get("etag") ?? "";
63865
63940
  break;
63866
63941
  } catch (err) {
63867
63942
  const msg = err instanceof Error ? err.message : String(err);
@@ -64139,6 +64214,7 @@ async function run26() {
64139
64214
  };
64140
64215
  if (founderPaid) cacheEntry.founderPaid = founderPaid;
64141
64216
  if (approvedClaims) cacheEntry.approvedClaims = approvedClaims;
64217
+ cacheEntry.indexETag = indexETag ?? "";
64142
64218
  updateIndexCache(cacheEntry);
64143
64219
  try {
64144
64220
  const { readSpinnerConfig: readSpinnerConfig2, renderRefreshSurface: renderRefreshSurface2 } = await Promise.resolve().then(() => (init_spinner(), spinner_exports));
@@ -462,6 +462,7 @@ var init_state_dir = __esm({
462
462
  // src/claims.ts
463
463
  var claims_exports = {};
464
464
  __export(claims_exports, {
465
+ CLAIM_STATES: () => CLAIM_STATES,
465
466
  PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
466
467
  acceptedPRRate: () => acceptedPRRate,
467
468
  countAwaitingFounderApproval: () => countAwaitingFounderApproval,
@@ -660,7 +661,7 @@ function acceptedPRRate(claims = readClaims()) {
660
661
  const merged = claims.filter((c) => c.state === "merged").length;
661
662
  return { merged, total, rate: total === 0 ? 0 : merged / total };
662
663
  }
663
- var TERMINALHIRE_DIR, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
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;
664
665
  var init_claims = __esm({
665
666
  "src/claims.ts"() {
666
667
  "use strict";
@@ -671,6 +672,22 @@ var init_claims = __esm({
671
672
  LOCK_STALE_MS = Number(process.env.TERMINALHIRE_LOCK_STALE_MS) || 1e4;
672
673
  LOCK_RETRY_MS = Number(process.env.TERMINALHIRE_LOCK_RETRY_MS) || 25;
673
674
  LOCK_TIMEOUT_MS = Number(process.env.TERMINALHIRE_LOCK_TIMEOUT_MS) || 5e3;
675
+ CLAIM_STATES = Object.freeze([
676
+ "claimed",
677
+ // recorded, not started
678
+ "working",
679
+ // background executor running / work in progress
680
+ "in-review",
681
+ // gate ran / dev reviewing the diff
682
+ "ready",
683
+ // passed review, cleared to submit
684
+ "submitted",
685
+ // PR opened on the source platform (awaiting maintainer)
686
+ "merged",
687
+ // PR merged — accepted; counts toward the metric
688
+ "abandoned"
689
+ // released, or PR closed unmerged
690
+ ]);
674
691
  PUSHED_CLAIM_FIELDS = [
675
692
  "kind",
676
693
  "repoFullName",
@@ -12541,7 +12558,7 @@ function readIndexCache() {
12541
12558
  }
12542
12559
  }
12543
12560
  function writeIndexCache(index) {
12544
- updateIndexCache({ index });
12561
+ updateIndexCache({ index, indexETag: "" });
12545
12562
  }
12546
12563
  async function fetchIndex() {
12547
12564
  const cached = readIndexCache();
@@ -12626,7 +12643,7 @@ function readIndexCache2() {
12626
12643
  }
12627
12644
  }
12628
12645
  function writeIndexCache2(index) {
12629
- updateIndexCache({ index });
12646
+ updateIndexCache({ index, indexETag: "" });
12630
12647
  }
12631
12648
  async function fetchIndex2() {
12632
12649
  const cached = readIndexCache2();
@@ -12141,7 +12141,7 @@ function readIndexCache() {
12141
12141
  }
12142
12142
  }
12143
12143
  function writeIndexCache(index) {
12144
- updateIndexCache({ index });
12144
+ updateIndexCache({ index, indexETag: "" });
12145
12145
  }
12146
12146
  async function fetchIndex() {
12147
12147
  const cached = readIndexCache();
@@ -10903,6 +10903,7 @@ var init_policy_acks = __esm({
10903
10903
  // src/claims.ts
10904
10904
  var claims_exports = {};
10905
10905
  __export(claims_exports, {
10906
+ CLAIM_STATES: () => CLAIM_STATES,
10906
10907
  PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
10907
10908
  acceptedPRRate: () => acceptedPRRate,
10908
10909
  countAwaitingFounderApproval: () => countAwaitingFounderApproval,
@@ -11101,7 +11102,7 @@ function acceptedPRRate(claims = readClaims()) {
11101
11102
  const merged = claims.filter((c) => c.state === "merged").length;
11102
11103
  return { merged, total, rate: total === 0 ? 0 : merged / total };
11103
11104
  }
11104
- var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
11105
+ 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;
11105
11106
  var init_claims = __esm({
11106
11107
  "src/claims.ts"() {
11107
11108
  "use strict";
@@ -11112,6 +11113,22 @@ var init_claims = __esm({
11112
11113
  LOCK_STALE_MS = Number(process.env.TERMINALHIRE_LOCK_STALE_MS) || 1e4;
11113
11114
  LOCK_RETRY_MS = Number(process.env.TERMINALHIRE_LOCK_RETRY_MS) || 25;
11114
11115
  LOCK_TIMEOUT_MS = Number(process.env.TERMINALHIRE_LOCK_TIMEOUT_MS) || 5e3;
11116
+ CLAIM_STATES = Object.freeze([
11117
+ "claimed",
11118
+ // recorded, not started
11119
+ "working",
11120
+ // background executor running / work in progress
11121
+ "in-review",
11122
+ // gate ran / dev reviewing the diff
11123
+ "ready",
11124
+ // passed review, cleared to submit
11125
+ "submitted",
11126
+ // PR opened on the source platform (awaiting maintainer)
11127
+ "merged",
11128
+ // PR merged — accepted; counts toward the metric
11129
+ "abandoned"
11130
+ // released, or PR closed unmerged
11131
+ ]);
11115
11132
  PUSHED_CLAIM_FIELDS = [
11116
11133
  "kind",
11117
11134
  "repoFullName",
@@ -26046,6 +26063,7 @@ var jpi_claim_exports = {};
26046
26063
  __export(jpi_claim_exports, {
26047
26064
  AI_DISCLOSURE_NOTE: () => AI_DISCLOSURE_NOTE,
26048
26065
  CLAIM_CONSENT_VERSION: () => CLAIM_CONSENT_VERSION,
26066
+ SUBMIT_ACCEPTS: () => SUBMIT_ACCEPTS,
26049
26067
  backgroundEnableFailed: () => backgroundEnableFailed,
26050
26068
  buildAssignmentComment: () => buildAssignmentComment,
26051
26069
  buildPatchSubmission: () => buildPatchSubmission,
@@ -26071,9 +26089,11 @@ __export(jpi_claim_exports, {
26071
26089
  listMergedPRsReferencingIssue: () => listMergedPRsReferencingIssue,
26072
26090
  listOpenPRsReferencingIssue: () => listOpenPRsReferencingIssue,
26073
26091
  matchReferencingPrs: () => matchReferencingPrs,
26092
+ nextStepFor: () => nextStepFor,
26074
26093
  normalizeIntent: () => normalizeIntent,
26075
26094
  pickBodySource: () => pickBodySource,
26076
26095
  pickExistingPr: () => pickExistingPr,
26096
+ printNextSteps: () => printNextSteps,
26077
26097
  renderRunView: () => renderRunView,
26078
26098
  renderServerRefusal: () => renderServerRefusal,
26079
26099
  resolveBounty: () => resolveBounty,
@@ -26750,6 +26770,37 @@ function printMetric(rate) {
26750
26770
  console.log(`
26751
26771
  \u{1F4CA} Accepted-PR rate: ${rate.merged}/${rate.total} claims merged (${pct}%)`);
26752
26772
  }
26773
+ function nextStepFor(c) {
26774
+ const founder = Boolean(c.approval);
26775
+ switch (c.state) {
26776
+ case "claimed":
26777
+ 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" };
26778
+ // NOT grouped with the two below. `cmdSubmit` accepts 'working' and 'ready'
26779
+ // and nothing else, so pointing an 'in-review' claim at submit would hand the
26780
+ // developer a command that exits 1 — and submit's own refusal then advises
26781
+ // `claim start`, which a founder claim can never use either. Two dead ends in
26782
+ // a row is worse than the silence this block replaced.
26783
+ case "in-review":
26784
+ return {
26785
+ cmd: `terminalhire claim update ${c.id} ready`,
26786
+ why: "clear the review gate \u2014 submit only accepts working/ready"
26787
+ };
26788
+ case "working":
26789
+ case "ready":
26790
+ return { cmd: `terminalhire claim submit ${c.id}`, why: "push the branch + open the PR" };
26791
+ case "submitted":
26792
+ 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" };
26793
+ default:
26794
+ return null;
26795
+ }
26796
+ }
26797
+ function printNextSteps(list) {
26798
+ const steps = list.map(nextStepFor).filter(Boolean);
26799
+ if (steps.length === 0) return;
26800
+ const width = Math.max(...steps.map((s) => s.cmd.length));
26801
+ console.log("\n Next:");
26802
+ for (const s of steps) console.log(` ${s.cmd.padEnd(width)} \u2014 ${s.why}`);
26803
+ }
26753
26804
  async function resolveBounty(arg) {
26754
26805
  let bountyId, title, repoFullName, issueUrl, amountUSD, source, openPRsAtDiscovery, indexNativeId;
26755
26806
  let founderPosting = false;
@@ -27321,6 +27372,7 @@ ${list.length} ${active ? "active " : ""}claim${list.length === 1 ? "" : "s"}:
27321
27372
  console.log(` id: ${c.id}${pr}`);
27322
27373
  }
27323
27374
  printMetric(claims.acceptedPRRate());
27375
+ printNextSteps(list);
27324
27376
  try {
27325
27377
  if (await shouldNudgeUnpushed()) {
27326
27378
  console.log(
@@ -27468,7 +27520,7 @@ async function cmdStatus(id) {
27468
27520
  }
27469
27521
  async function cmdUpdate(id, state, prUrl) {
27470
27522
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
27471
- const VALID = ["claimed", "working", "in-review", "ready", "submitted", "merged", "abandoned"];
27523
+ const VALID = claims.CLAIM_STATES;
27472
27524
  if (!id || !state || !VALID.includes(state)) {
27473
27525
  console.error("Usage: terminalhire claim update <id> <state> [prUrl]");
27474
27526
  console.error(" state: " + VALID.join(" | "));
@@ -28326,7 +28378,7 @@ async function cmdSubmit(id, flags = {}) {
28326
28378
  console.error(`terminalhire claim: no claim with id '${id}'.`);
28327
28379
  process.exit(1);
28328
28380
  }
28329
- if (claim.state !== "working" && claim.state !== "ready") {
28381
+ if (!SUBMIT_ACCEPTS.includes(claim.state)) {
28330
28382
  console.error(
28331
28383
  `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:
28332
28384
  terminalhire claim start ${id}`
@@ -29275,7 +29327,7 @@ async function run() {
29275
29327
  process.exit(1);
29276
29328
  }
29277
29329
  }
29278
- var TERMINALHIRE_DIR11, INDEX_CACHE_FILE2, CLAIM_PUSH_MARKER, REPO_CONTINUITY_NUDGE_MARKER, API_URL, CLAIM_SYNC_BASE, 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, ISSUE_OUTCOME_TERMINAL, RUNS_POLL_INTERVAL_MS, RUNS_POLL_ATTEMPTS;
29330
+ var TERMINALHIRE_DIR11, INDEX_CACHE_FILE2, CLAIM_PUSH_MARKER, REPO_CONTINUITY_NUDGE_MARKER, API_URL, CLAIM_SYNC_BASE, 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, ISSUE_OUTCOME_TERMINAL, RUNS_POLL_INTERVAL_MS, RUNS_POLL_ATTEMPTS;
29279
29331
  var init_jpi_claim = __esm({
29280
29332
  "bin/jpi-claim.js"() {
29281
29333
  "use strict";
@@ -29307,6 +29359,7 @@ var init_jpi_claim = __esm({
29307
29359
  STAKE_POST_TIMEOUT_MS = 3e4;
29308
29360
  STAKE_POSTING_GRACE_MS = STAKE_POST_TIMEOUT_MS + 15e3;
29309
29361
  TAKE_BOT_REPOS = /* @__PURE__ */ new Set(["paradedb/paradedb"]);
29362
+ SUBMIT_ACCEPTS = Object.freeze(["working", "ready"]);
29310
29363
  ISSUE_OUTCOME_TERMINAL = /* @__PURE__ */ new Set(["own-merge", "other-merge", "closed-unmerged"]);
29311
29364
  RUNS_POLL_INTERVAL_MS = 15e3;
29312
29365
  RUNS_POLL_ATTEMPTS = 20;
@@ -12283,6 +12283,7 @@ var init_github_auth = __esm({
12283
12283
  // src/claims.ts
12284
12284
  var claims_exports = {};
12285
12285
  __export(claims_exports, {
12286
+ CLAIM_STATES: () => CLAIM_STATES,
12286
12287
  PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
12287
12288
  acceptedPRRate: () => acceptedPRRate,
12288
12289
  countAwaitingFounderApproval: () => countAwaitingFounderApproval,
@@ -12481,7 +12482,7 @@ function acceptedPRRate(claims = readClaims()) {
12481
12482
  const merged = claims.filter((c) => c.state === "merged").length;
12482
12483
  return { merged, total, rate: total === 0 ? 0 : merged / total };
12483
12484
  }
12484
- var TERMINALHIRE_DIR8, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS2, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
12485
+ 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;
12485
12486
  var init_claims = __esm({
12486
12487
  "src/claims.ts"() {
12487
12488
  "use strict";
@@ -12492,6 +12493,22 @@ var init_claims = __esm({
12492
12493
  LOCK_STALE_MS2 = Number(process.env.TERMINALHIRE_LOCK_STALE_MS) || 1e4;
12493
12494
  LOCK_RETRY_MS = Number(process.env.TERMINALHIRE_LOCK_RETRY_MS) || 25;
12494
12495
  LOCK_TIMEOUT_MS = Number(process.env.TERMINALHIRE_LOCK_TIMEOUT_MS) || 5e3;
12496
+ CLAIM_STATES = Object.freeze([
12497
+ "claimed",
12498
+ // recorded, not started
12499
+ "working",
12500
+ // background executor running / work in progress
12501
+ "in-review",
12502
+ // gate ran / dev reviewing the diff
12503
+ "ready",
12504
+ // passed review, cleared to submit
12505
+ "submitted",
12506
+ // PR opened on the source platform (awaiting maintainer)
12507
+ "merged",
12508
+ // PR merged — accepted; counts toward the metric
12509
+ "abandoned"
12510
+ // released, or PR closed unmerged
12511
+ ]);
12495
12512
  PUSHED_CLAIM_FIELDS = [
12496
12513
  "kind",
12497
12514
  "repoFullName",
@@ -12739,8 +12756,7 @@ function gate(openIds, seenIds) {
12739
12756
  function computeFounderPaid(index, previous) {
12740
12757
  const open = openPaidIds(index);
12741
12758
  const prior = previous && previous.acknowledged;
12742
- if (!Array.isArray(prior)) return { count: 0, acknowledged: open };
12743
- return gate(open, prior);
12759
+ return gate(open, Array.isArray(prior) ? prior : []);
12744
12760
  }
12745
12761
  function acknowledgeFounderPaid({ shown = [], open = [], previous } = {}) {
12746
12762
  const prior = previous && Array.isArray(previous.acknowledged) ? previous.acknowledged : [];
@@ -13310,18 +13326,41 @@ var MMR_K = 8;
13310
13326
  async function run() {
13311
13327
  try {
13312
13328
  let index;
13329
+ let indexETag;
13330
+ const cachedForRevalidation = readCacheEntry() ?? {};
13331
+ let sendValidator = typeof cachedForRevalidation.indexETag === "string" && cachedForRevalidation.indexETag !== "";
13313
13332
  for (let attempt = 1; ; attempt++) {
13314
13333
  try {
13315
13334
  const res = await fetch(`${API_URL2}/api/index`, {
13316
13335
  signal: AbortSignal.timeout(15e3),
13317
- headers: { Accept: "application/json" }
13336
+ headers: sendValidator ? { Accept: "application/json", "If-None-Match": cachedForRevalidation.indexETag } : { Accept: "application/json" }
13318
13337
  });
13338
+ if (res.status === 304) {
13339
+ const cached = cachedForRevalidation.index;
13340
+ if (cached && Array.isArray(cached.jobs)) {
13341
+ index = cached;
13342
+ indexETag = cachedForRevalidation.indexETag;
13343
+ break;
13344
+ }
13345
+ sendValidator = false;
13346
+ if (attempt >= 2) {
13347
+ process.stderr.write(
13348
+ "terminalhire refresh: 304 with no usable cached index \u2014 giving up this tick\n"
13349
+ );
13350
+ process.exit(1);
13351
+ }
13352
+ process.stderr.write(
13353
+ "terminalhire refresh: 304 but the cached index is unusable (re-fetching in full)\n"
13354
+ );
13355
+ continue;
13356
+ }
13319
13357
  if (!res.ok) {
13320
13358
  process.stderr.write(`terminalhire refresh: index fetch failed (HTTP ${res.status})
13321
13359
  `);
13322
13360
  process.exit(1);
13323
13361
  }
13324
13362
  index = await res.json();
13363
+ indexETag = res.headers.get("etag") ?? "";
13325
13364
  break;
13326
13365
  } catch (err) {
13327
13366
  const msg = err instanceof Error ? err.message : String(err);
@@ -13599,6 +13638,7 @@ async function run() {
13599
13638
  };
13600
13639
  if (founderPaid) cacheEntry.founderPaid = founderPaid;
13601
13640
  if (approvedClaims) cacheEntry.approvedClaims = approvedClaims;
13641
+ cacheEntry.indexETag = indexETag ?? "";
13602
13642
  updateIndexCache(cacheEntry);
13603
13643
  try {
13604
13644
  const { readSpinnerConfig: readSpinnerConfig2, renderRefreshSurface: renderRefreshSurface2 } = await Promise.resolve().then(() => (init_spinner(), spinner_exports));
@@ -1824,6 +1824,7 @@ var init_repo_experience = __esm({
1824
1824
  // src/claims.ts
1825
1825
  var claims_exports = {};
1826
1826
  __export(claims_exports, {
1827
+ CLAIM_STATES: () => CLAIM_STATES,
1827
1828
  PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
1828
1829
  acceptedPRRate: () => acceptedPRRate,
1829
1830
  countAwaitingFounderApproval: () => countAwaitingFounderApproval,
@@ -2022,7 +2023,7 @@ function acceptedPRRate(claims = readClaims()) {
2022
2023
  const merged = claims.filter((c) => c.state === "merged").length;
2023
2024
  return { merged, total, rate: total === 0 ? 0 : merged / total };
2024
2025
  }
2025
- var TERMINALHIRE_DIR4, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
2026
+ 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;
2026
2027
  var init_claims = __esm({
2027
2028
  "src/claims.ts"() {
2028
2029
  "use strict";
@@ -2033,6 +2034,22 @@ var init_claims = __esm({
2033
2034
  LOCK_STALE_MS = Number(process.env.TERMINALHIRE_LOCK_STALE_MS) || 1e4;
2034
2035
  LOCK_RETRY_MS = Number(process.env.TERMINALHIRE_LOCK_RETRY_MS) || 25;
2035
2036
  LOCK_TIMEOUT_MS = Number(process.env.TERMINALHIRE_LOCK_TIMEOUT_MS) || 5e3;
2037
+ CLAIM_STATES = Object.freeze([
2038
+ "claimed",
2039
+ // recorded, not started
2040
+ "working",
2041
+ // background executor running / work in progress
2042
+ "in-review",
2043
+ // gate ran / dev reviewing the diff
2044
+ "ready",
2045
+ // passed review, cleared to submit
2046
+ "submitted",
2047
+ // PR opened on the source platform (awaiting maintainer)
2048
+ "merged",
2049
+ // PR merged — accepted; counts toward the metric
2050
+ "abandoned"
2051
+ // released, or PR closed unmerged
2052
+ ]);
2036
2053
  PUSHED_CLAIM_FIELDS = [
2037
2054
  "kind",
2038
2055
  "repoFullName",
@@ -99,6 +99,22 @@ function withClaimsLock(fn) {
99
99
  rmSync(LOCK_DIR, { recursive: true, force: true });
100
100
  }
101
101
  }
102
+ var CLAIM_STATES = Object.freeze([
103
+ "claimed",
104
+ // recorded, not started
105
+ "working",
106
+ // background executor running / work in progress
107
+ "in-review",
108
+ // gate ran / dev reviewing the diff
109
+ "ready",
110
+ // passed review, cleared to submit
111
+ "submitted",
112
+ // PR opened on the source platform (awaiting maintainer)
113
+ "merged",
114
+ // PR merged — accepted; counts toward the metric
115
+ "abandoned"
116
+ // released, or PR closed unmerged
117
+ ]);
102
118
  var PUSHED_CLAIM_FIELDS = [
103
119
  "kind",
104
120
  "repoFullName",
@@ -269,6 +285,7 @@ function acceptedPRRate(claims = readClaims()) {
269
285
  return { merged, total, rate: total === 0 ? 0 : merged / total };
270
286
  }
271
287
  export {
288
+ CLAIM_STATES,
272
289
  PUSHED_CLAIM_FIELDS,
273
290
  acceptedPRRate,
274
291
  countAwaitingFounderApproval,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.40.2",
3
+ "version": "0.40.4",
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",