terminalhire 0.42.30 → 0.42.31

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.
@@ -34452,6 +34452,13 @@ var init_preview2 = __esm({
34452
34452
  }
34453
34453
  });
34454
34454
 
34455
+ // ../../packages/envspec/dist/runRequirements.js
34456
+ var init_runRequirements = __esm({
34457
+ "../../packages/envspec/dist/runRequirements.js"() {
34458
+ "use strict";
34459
+ }
34460
+ });
34461
+
34455
34462
  // ../../packages/envspec/dist/index.js
34456
34463
  var init_dist3 = __esm({
34457
34464
  "../../packages/envspec/dist/index.js"() {
@@ -34459,6 +34466,7 @@ var init_dist3 = __esm({
34459
34466
  init_derive();
34460
34467
  init_repo();
34461
34468
  init_preview2();
34469
+ init_runRequirements();
34462
34470
  init_yaml();
34463
34471
  }
34464
34472
  });
@@ -37742,7 +37750,9 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
37742
37750
  // `--sha` was here for `claim note approve` and went with it: the send moved to the
37743
37751
  // dashboard, so no CLI verb names a digest any more. Left in place it would parse a
37744
37752
  // flag nothing reads, which is how a removed feature looks half-removed.
37745
- "body"
37753
+ "body",
37754
+ // TERM-1259. `claim resolve --reason setup-failed --requirement <field>`.
37755
+ "requirement"
37746
37756
  ]);
37747
37757
  function parseArgs(argv) {
37748
37758
  const flags = {};
@@ -37955,7 +37965,12 @@ function extractClaimableFields(job) {
37955
37965
  // server-side (Decision 4). `claimMode` follows the index row's absent-means-
37956
37966
  // default convention: absent = open.
37957
37967
  founderPosting: b.bountySource === "founder",
37958
- claimMode: b.claimMode === "approval-only" ? "approval-only" : "open"
37968
+ claimMode: b.claimMode === "approval-only" ? "approval-only" : "open",
37969
+ // TERM-1259. What the work needs, as the SERVER projected it onto the index
37970
+ // (`publicRunRequirements`). Carried, never derived here: the CLI has no copy of the
37971
+ // repository before a claim, and a second derivation could disagree with the one the
37972
+ // poster saw. Null when the index carries none.
37973
+ runRequirements: b.runRequirements && typeof b.runRequirements === "object" ? b.runRequirements : null
37959
37974
  };
37960
37975
  }
37961
37976
  function parseGitHubUrl(url) {
@@ -38565,6 +38580,7 @@ async function resolveBounty(arg) {
38565
38580
  let bountyId, title, repoFullName, issueUrl, amountUSD, source, openPRsAtDiscovery, indexNativeId;
38566
38581
  let founderPosting = false;
38567
38582
  let claimMode = "open";
38583
+ let runRequirements = null;
38568
38584
  let job = findClaimableInCache(arg) ?? (looksLikeShortRef(arg) ? findClaimableByShortRef(arg) : null);
38569
38585
  let freshPool;
38570
38586
  if (!job && looksLikeShortRef(arg)) {
@@ -38581,7 +38597,8 @@ async function resolveBounty(arg) {
38581
38597
  source,
38582
38598
  openPRsAtDiscovery,
38583
38599
  founderPosting,
38584
- claimMode
38600
+ claimMode,
38601
+ runRequirements
38585
38602
  } = extractClaimableFields(job));
38586
38603
  indexNativeId = bountyId;
38587
38604
  } else {
@@ -38652,7 +38669,9 @@ async function resolveBounty(arg) {
38652
38669
  // GitHub issue), so the defaults above are correct there: a URL-tier claim is
38653
38670
  // never treated as founder supply.
38654
38671
  founderPosting,
38655
- claimMode
38672
+ claimMode,
38673
+ // TERM-1259. Only a founder index hit carries these; every other path is null.
38674
+ runRequirements: runRequirements ?? null
38656
38675
  };
38657
38676
  }
38658
38677
  function fmtOpenPRsLine(b) {
@@ -42477,7 +42496,16 @@ var CLAIM_RESOLUTION_REASONS = [
42477
42496
  "out-of-time",
42478
42497
  "already-implemented",
42479
42498
  "repo-does-not-build",
42480
- "brief-insufficient"
42499
+ "brief-insufficient",
42500
+ "setup-failed"
42501
+ ];
42502
+ var SETUP_FAILED_REQUIREMENTS = [
42503
+ "runtime",
42504
+ "runtimeVersion",
42505
+ "install",
42506
+ "test",
42507
+ "services",
42508
+ "os"
42481
42509
  ];
42482
42510
  var POSTING_LEVEL_RESOLUTION_REASONS = [
42483
42511
  "already-implemented",
@@ -42489,7 +42517,8 @@ var RESOLUTION_REASON_BLURB = {
42489
42517
  "out-of-time": "you ran out of time for it",
42490
42518
  "already-implemented": "the repo already has this",
42491
42519
  "repo-does-not-build": "the repo will not build, so nobody can finish it",
42492
- "brief-insufficient": "the task does not say enough to do the work"
42520
+ "brief-insufficient": "the task does not say enough to do the work",
42521
+ "setup-failed": "the environment would not set up; you may claim it again"
42493
42522
  };
42494
42523
  function isPostingLevelResolutionReason(reason) {
42495
42524
  return POSTING_LEVEL_RESOLUTION_REASONS.includes(reason);
@@ -42641,10 +42670,14 @@ async function postJson(fetchImpl, url, body, linked) {
42641
42670
  function resolveUsageLines() {
42642
42671
  return [
42643
42672
  'Usage: terminalhire claim resolve <id> --reason <reason> [--note "..."]',
42673
+ " [--requirement <field>]",
42644
42674
  "",
42645
42675
  " Hand a claim back and tell the poster why. Needs `terminalhire link`.",
42646
42676
  "",
42647
- ...resolutionReasonHelpLines()
42677
+ ...resolutionReasonHelpLines(),
42678
+ ` With --reason setup-failed, --requirement names what would not set up:`,
42679
+ ` ${SETUP_FAILED_REQUIREMENTS.join(", ")}`,
42680
+ ""
42648
42681
  ];
42649
42682
  }
42650
42683
  function printResolveHint(list, log) {
@@ -42700,6 +42733,16 @@ async function cmdResolve(id, flags = {}, deps = {}) {
42700
42733
  for (const line of resolutionReasonHelpLines()) err(line);
42701
42734
  return 1;
42702
42735
  }
42736
+ const requirement = typeof flags.requirement === "string" && flags.requirement.trim() ? flags.requirement.trim() : null;
42737
+ if (requirement !== null && reason !== "setup-failed") {
42738
+ err("terminalhire claim resolve: --requirement is used only with --reason setup-failed.");
42739
+ return 1;
42740
+ }
42741
+ if (requirement !== null && !SETUP_FAILED_REQUIREMENTS.includes(requirement)) {
42742
+ err(`terminalhire claim resolve: '${requirement}' is not a run requirement. One of:`);
42743
+ err(` ${SETUP_FAILED_REQUIREMENTS.join(", ")}`);
42744
+ return 1;
42745
+ }
42703
42746
  const linked = deps.cookie ? { cookie: deps.cookie, mismatch: null } : webSessionCookieForHost(CLAIM_SYNC_BASE4);
42704
42747
  if (linked.mismatch) {
42705
42748
  err(
@@ -42714,6 +42757,7 @@ async function cmdResolve(id, flags = {}, deps = {}) {
42714
42757
  const note = typeof flags.note === "string" && flags.note.trim() ? flags.note.trim() : null;
42715
42758
  const body = { bountyId: founderPostingIdOf(claim), claimId, reason };
42716
42759
  if (note) body.note = note;
42760
+ if (requirement) body.failedRequirement = requirement;
42717
42761
  let res;
42718
42762
  try {
42719
42763
  res = await fetchImpl(`${CLAIM_SYNC_BASE4}/api/claim/resolution`, {
@@ -42750,6 +42794,9 @@ async function cmdResolve(id, flags = {}, deps = {}) {
42750
42794
  log(
42751
42795
  suspended ? " The posting is off the market until the poster acts on it." : " The posting is open again for someone else."
42752
42796
  );
42797
+ if (reason === "setup-failed") {
42798
+ log(` A setup failure does not stop you: you may claim it again once it sets up.`);
42799
+ }
42753
42800
  log("");
42754
42801
  log(` The local record is still here. To drop it: terminalhire claim release ${id}`);
42755
42802
  log("");
@@ -42870,6 +42917,7 @@ export {
42870
42917
  POSTING_LEVEL_RESOLUTION_REASONS,
42871
42918
  PUSH_TOKEN_REFUSAL,
42872
42919
  REVISE_RECOVERY_STATES,
42920
+ SETUP_FAILED_REQUIREMENTS,
42873
42921
  SUBMIT_ACCEPTS,
42874
42922
  SYNC_BACKGROUND_PUSH_ACTIVE_FIELD,
42875
42923
  VERIFY_REL_PATH,
@@ -4711,10 +4711,10 @@ function parseComment(item) {
4711
4711
  if (!item.text || item.text.trim().length < 20) return null;
4712
4712
  const raw = stripHtml2(item.text);
4713
4713
  if (!raw) return null;
4714
- const firstLine = raw.split(/\n/)[0];
4715
- const parts = firstLine.split("|").map((s) => s.trim());
4714
+ const firstLine2 = raw.split(/\n/)[0];
4715
+ const parts = firstLine2.split("|").map((s) => s.trim());
4716
4716
  const company = parts[0] ?? "Unknown";
4717
- const title = parts[1] ?? firstLine.slice(0, 80).trim();
4717
+ const title = parts[1] ?? firstLine2.slice(0, 80).trim();
4718
4718
  const location = parts[2] ?? "";
4719
4719
  if (company.toLowerCase().startsWith("note:") || company.toLowerCase().startsWith("ps:") || title.length < 3) {
4720
4720
  return null;
@@ -40195,6 +40195,13 @@ var init_preview2 = __esm({
40195
40195
  }
40196
40196
  });
40197
40197
 
40198
+ // ../../packages/envspec/dist/runRequirements.js
40199
+ var init_runRequirements = __esm({
40200
+ "../../packages/envspec/dist/runRequirements.js"() {
40201
+ "use strict";
40202
+ }
40203
+ });
40204
+
40198
40205
  // ../../packages/envspec/dist/index.js
40199
40206
  var init_dist3 = __esm({
40200
40207
  "../../packages/envspec/dist/index.js"() {
@@ -40202,6 +40209,7 @@ var init_dist3 = __esm({
40202
40209
  init_derive();
40203
40210
  init_repo();
40204
40211
  init_preview2();
40212
+ init_runRequirements();
40205
40213
  init_yaml();
40206
40214
  }
40207
40215
  });
@@ -42804,6 +42812,7 @@ __export(jpi_claim_exports, {
42804
42812
  POSTING_LEVEL_RESOLUTION_REASONS: () => POSTING_LEVEL_RESOLUTION_REASONS,
42805
42813
  PUSH_TOKEN_REFUSAL: () => PUSH_TOKEN_REFUSAL,
42806
42814
  REVISE_RECOVERY_STATES: () => REVISE_RECOVERY_STATES,
42815
+ SETUP_FAILED_REQUIREMENTS: () => SETUP_FAILED_REQUIREMENTS,
42807
42816
  SUBMIT_ACCEPTS: () => SUBMIT_ACCEPTS,
42808
42817
  SYNC_BACKGROUND_PUSH_ACTIVE_FIELD: () => SYNC_BACKGROUND_PUSH_ACTIVE_FIELD,
42809
42818
  VERIFY_REL_PATH: () => VERIFY_REL_PATH,
@@ -43285,7 +43294,12 @@ function extractClaimableFields(job) {
43285
43294
  // server-side (Decision 4). `claimMode` follows the index row's absent-means-
43286
43295
  // default convention: absent = open.
43287
43296
  founderPosting: b.bountySource === "founder",
43288
- claimMode: b.claimMode === "approval-only" ? "approval-only" : "open"
43297
+ claimMode: b.claimMode === "approval-only" ? "approval-only" : "open",
43298
+ // TERM-1259. What the work needs, as the SERVER projected it onto the index
43299
+ // (`publicRunRequirements`). Carried, never derived here: the CLI has no copy of the
43300
+ // repository before a claim, and a second derivation could disagree with the one the
43301
+ // poster saw. Null when the index carries none.
43302
+ runRequirements: b.runRequirements && typeof b.runRequirements === "object" ? b.runRequirements : null
43289
43303
  };
43290
43304
  }
43291
43305
  function parseGitHubUrl(url) {
@@ -43884,6 +43898,7 @@ async function resolveBounty(arg) {
43884
43898
  let bountyId, title, repoFullName, issueUrl, amountUSD, source, openPRsAtDiscovery, indexNativeId;
43885
43899
  let founderPosting = false;
43886
43900
  let claimMode = "open";
43901
+ let runRequirements = null;
43887
43902
  let job = findClaimableInCache(arg) ?? (looksLikeShortRef(arg) ? findClaimableByShortRef(arg) : null);
43888
43903
  let freshPool;
43889
43904
  if (!job && looksLikeShortRef(arg)) {
@@ -43900,7 +43915,8 @@ async function resolveBounty(arg) {
43900
43915
  source,
43901
43916
  openPRsAtDiscovery,
43902
43917
  founderPosting,
43903
- claimMode
43918
+ claimMode,
43919
+ runRequirements
43904
43920
  } = extractClaimableFields(job));
43905
43921
  indexNativeId = bountyId;
43906
43922
  } else {
@@ -43971,7 +43987,9 @@ async function resolveBounty(arg) {
43971
43987
  // GitHub issue), so the defaults above are correct there: a URL-tier claim is
43972
43988
  // never treated as founder supply.
43973
43989
  founderPosting,
43974
- claimMode
43990
+ claimMode,
43991
+ // TERM-1259. Only a founder index hit carries these; every other path is null.
43992
+ runRequirements: runRequirements ?? null
43975
43993
  };
43976
43994
  }
43977
43995
  function fmtOpenPRsLine(b) {
@@ -47885,10 +47903,14 @@ async function postJson(fetchImpl, url, body, linked) {
47885
47903
  function resolveUsageLines() {
47886
47904
  return [
47887
47905
  'Usage: terminalhire claim resolve <id> --reason <reason> [--note "..."]',
47906
+ " [--requirement <field>]",
47888
47907
  "",
47889
47908
  " Hand a claim back and tell the poster why. Needs `terminalhire link`.",
47890
47909
  "",
47891
- ...resolutionReasonHelpLines()
47910
+ ...resolutionReasonHelpLines(),
47911
+ ` With --reason setup-failed, --requirement names what would not set up:`,
47912
+ ` ${SETUP_FAILED_REQUIREMENTS.join(", ")}`,
47913
+ ""
47892
47914
  ];
47893
47915
  }
47894
47916
  function printResolveHint(list, log) {
@@ -47944,6 +47966,16 @@ async function cmdResolve(id, flags = {}, deps = {}) {
47944
47966
  for (const line of resolutionReasonHelpLines()) err(line);
47945
47967
  return 1;
47946
47968
  }
47969
+ const requirement = typeof flags.requirement === "string" && flags.requirement.trim() ? flags.requirement.trim() : null;
47970
+ if (requirement !== null && reason !== "setup-failed") {
47971
+ err("terminalhire claim resolve: --requirement is used only with --reason setup-failed.");
47972
+ return 1;
47973
+ }
47974
+ if (requirement !== null && !SETUP_FAILED_REQUIREMENTS.includes(requirement)) {
47975
+ err(`terminalhire claim resolve: '${requirement}' is not a run requirement. One of:`);
47976
+ err(` ${SETUP_FAILED_REQUIREMENTS.join(", ")}`);
47977
+ return 1;
47978
+ }
47947
47979
  const linked = deps.cookie ? { cookie: deps.cookie, mismatch: null } : webSessionCookieForHost(CLAIM_SYNC_BASE4);
47948
47980
  if (linked.mismatch) {
47949
47981
  err(
@@ -47958,6 +47990,7 @@ async function cmdResolve(id, flags = {}, deps = {}) {
47958
47990
  const note = typeof flags.note === "string" && flags.note.trim() ? flags.note.trim() : null;
47959
47991
  const body = { bountyId: founderPostingIdOf(claim), claimId, reason };
47960
47992
  if (note) body.note = note;
47993
+ if (requirement) body.failedRequirement = requirement;
47961
47994
  let res;
47962
47995
  try {
47963
47996
  res = await fetchImpl(`${CLAIM_SYNC_BASE4}/api/claim/resolution`, {
@@ -47994,6 +48027,9 @@ async function cmdResolve(id, flags = {}, deps = {}) {
47994
48027
  log(
47995
48028
  suspended ? " The posting is off the market until the poster acts on it." : " The posting is open again for someone else."
47996
48029
  );
48030
+ if (reason === "setup-failed") {
48031
+ log(` A setup failure does not stop you: you may claim it again once it sets up.`);
48032
+ }
47997
48033
  log("");
47998
48034
  log(` The local record is still here. To drop it: terminalhire claim release ${id}`);
47999
48035
  log("");
@@ -48101,7 +48137,7 @@ async function run7() {
48101
48137
  process.exit(1);
48102
48138
  }
48103
48139
  }
48104
- var TERMINALHIRE_DIR17, INDEX_CACHE_FILE5, CLAIM_PUSH_MARKER, REPO_CONTINUITY_NUDGE_MARKER, API_URL6, CLAIM_SYNC_BASE4, CLAIM_CONSENT_VERSION, CLAIM_POLL_INTERVAL_MS, CLAIM_POLL_TIMEOUT_MS, CLAIM_SYNC_WRITE_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, CLOSED_STATES, GH_SESSION_COOKIE4, PUSH_TOKEN_REFUSAL, SYNC_BACKGROUND_PUSH_ACTIVE_FIELD, ISSUE_OUTCOME_TERMINAL, RUNS_POLL_INTERVAL_MS, RUNS_POLL_ATTEMPTS, OPENABLE_AGENTS, BRIEF_DIR, BRIEF_REL_PATH, VERIFY_REL_PATH, AGENTS_REL_PATH, BRIEF_EXCLUDE_LINE, PACK_SAFE_ID, CLAIM_EVENT_LABEL, LINE_BREAKS, CONTROL_CHARS3, FOUNDER_POSTING_ID, CLAIM_RESOLUTION_REASONS, POSTING_LEVEL_RESOLUTION_REASONS, RESOLUTION_REASON_BLURB;
48140
+ var TERMINALHIRE_DIR17, INDEX_CACHE_FILE5, CLAIM_PUSH_MARKER, REPO_CONTINUITY_NUDGE_MARKER, API_URL6, CLAIM_SYNC_BASE4, CLAIM_CONSENT_VERSION, CLAIM_POLL_INTERVAL_MS, CLAIM_POLL_TIMEOUT_MS, CLAIM_SYNC_WRITE_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, CLOSED_STATES, GH_SESSION_COOKIE4, PUSH_TOKEN_REFUSAL, SYNC_BACKGROUND_PUSH_ACTIVE_FIELD, ISSUE_OUTCOME_TERMINAL, RUNS_POLL_INTERVAL_MS, RUNS_POLL_ATTEMPTS, OPENABLE_AGENTS, BRIEF_DIR, BRIEF_REL_PATH, VERIFY_REL_PATH, AGENTS_REL_PATH, BRIEF_EXCLUDE_LINE, PACK_SAFE_ID, CLAIM_EVENT_LABEL, LINE_BREAKS, CONTROL_CHARS3, FOUNDER_POSTING_ID, CLAIM_RESOLUTION_REASONS, SETUP_FAILED_REQUIREMENTS, POSTING_LEVEL_RESOLUTION_REASONS, RESOLUTION_REASON_BLURB;
48105
48141
  var init_jpi_claim = __esm({
48106
48142
  "bin/jpi-claim.js"() {
48107
48143
  "use strict";
@@ -48149,7 +48185,9 @@ var init_jpi_claim = __esm({
48149
48185
  // `--sha` was here for `claim note approve` and went with it: the send moved to the
48150
48186
  // dashboard, so no CLI verb names a digest any more. Left in place it would parse a
48151
48187
  // flag nothing reads, which is how a removed feature looks half-removed.
48152
- "body"
48188
+ "body",
48189
+ // TERM-1259. `claim resolve --reason setup-failed --requirement <field>`.
48190
+ "requirement"
48153
48191
  ]);
48154
48192
  ASSIGNMENT_MARKER = "<!-- terminalhire:assignment-request -->";
48155
48193
  STAKE_MARKER = "<!-- terminalhire:claim-stake -->";
@@ -48223,7 +48261,16 @@ var init_jpi_claim = __esm({
48223
48261
  "out-of-time",
48224
48262
  "already-implemented",
48225
48263
  "repo-does-not-build",
48226
- "brief-insufficient"
48264
+ "brief-insufficient",
48265
+ "setup-failed"
48266
+ ];
48267
+ SETUP_FAILED_REQUIREMENTS = [
48268
+ "runtime",
48269
+ "runtimeVersion",
48270
+ "install",
48271
+ "test",
48272
+ "services",
48273
+ "os"
48227
48274
  ];
48228
48275
  POSTING_LEVEL_RESOLUTION_REASONS = [
48229
48276
  "already-implemented",
@@ -48235,7 +48282,8 @@ var init_jpi_claim = __esm({
48235
48282
  "out-of-time": "you ran out of time for it",
48236
48283
  "already-implemented": "the repo already has this",
48237
48284
  "repo-does-not-build": "the repo will not build, so nobody can finish it",
48238
- "brief-insufficient": "the task does not say enough to do the work"
48285
+ "brief-insufficient": "the task does not say enough to do the work",
48286
+ "setup-failed": "the environment would not set up; you may claim it again"
48239
48287
  };
48240
48288
  }
48241
48289
  });
@@ -55345,6 +55393,123 @@ var init_mcp_config = __esm({
55345
55393
  }
55346
55394
  });
55347
55395
 
55396
+ // bin/local-setup-probe.js
55397
+ import { execFile as execFile5 } from "child_process";
55398
+ function firstLine(text) {
55399
+ const line = String(text ?? "").split(/\r\n|\r|\n/).map((l) => l.trim()).find((l) => l !== "");
55400
+ return line ? line.replace(CONTROL, "").slice(0, 80) : null;
55401
+ }
55402
+ function runProbe(file, args5, timeoutMs = PROBE_TIMEOUT_MS3, { shell = false } = {}) {
55403
+ return new Promise((resolve8) => {
55404
+ let child;
55405
+ const opts = {
55406
+ timeout: timeoutMs,
55407
+ killSignal: "SIGKILL",
55408
+ windowsHide: true,
55409
+ maxBuffer: 64 * 1024
55410
+ };
55411
+ const done2 = (err, stdout, stderr) => {
55412
+ if (!err) {
55413
+ resolve8({ ok: true, version: firstLine(stdout) ?? firstLine(stderr) });
55414
+ return;
55415
+ }
55416
+ if (err.code === "ENOENT" || shell && SHELL_NOT_FOUND.has(err.code)) {
55417
+ resolve8({ ok: false, reason: "not found" });
55418
+ } else if (err.killed || err.signal) resolve8({ ok: false, reason: "timed out" });
55419
+ else resolve8({ ok: false, reason: "exited with an error" });
55420
+ };
55421
+ try {
55422
+ child = shell ? execFile5([file, ...args5].join(" "), [], { ...opts, shell: true }, done2) : execFile5(file, args5, opts, done2);
55423
+ } catch {
55424
+ resolve8({ ok: false, reason: "not found" });
55425
+ return;
55426
+ }
55427
+ child.stdin?.end();
55428
+ });
55429
+ }
55430
+ function probesFor(requirements) {
55431
+ if (!requirements || typeof requirements !== "object") return [];
55432
+ const out = [];
55433
+ const runtime = requirements.runtime?.value;
55434
+ if (typeof runtime === "string" && RUNTIME_PROBES[runtime]) {
55435
+ out.push({ ...RUNTIME_PROBES[runtime], for: "runtime" });
55436
+ }
55437
+ const install = requirements.install?.value;
55438
+ if (typeof install === "string") {
55439
+ const first = install.trim().split(/\s+/)[0];
55440
+ const probe = INSTALL_TOOL_PROBES[first];
55441
+ if (probe && !out.some((p) => p.tool === probe.tool)) out.push({ ...probe, for: "install" });
55442
+ }
55443
+ const services = requirements.services?.value;
55444
+ if (Array.isArray(services) && services.length > 0) {
55445
+ out.push({ ...DOCKER_PROBE, for: "services" });
55446
+ }
55447
+ return out;
55448
+ }
55449
+ async function probeLocalSetup(requirements, { run: run32 = runProbe, timeoutMs = PROBE_TIMEOUT_MS3, platform = process.platform } = {}) {
55450
+ const probes = probesFor(requirements);
55451
+ const shell = platform === "win32";
55452
+ const results = await Promise.all(probes.map((p) => run32(p.file, p.args, timeoutMs, { shell })));
55453
+ const have = [];
55454
+ const missing = [];
55455
+ probes.forEach((p, i) => {
55456
+ const r = results[i];
55457
+ const base = { tool: p.tool, for: p.for };
55458
+ if (r.ok) {
55459
+ have.push({
55460
+ ...base,
55461
+ ...p.foundNote ? { note: p.foundNote } : {},
55462
+ version: r.version ?? null
55463
+ });
55464
+ } else missing.push({ ...base, reason: r.reason });
55465
+ });
55466
+ return {
55467
+ note: "Checked on this machine. These results were not sent anywhere.",
55468
+ have,
55469
+ missing
55470
+ };
55471
+ }
55472
+ var PROBE_TIMEOUT_MS3, RUNTIME_PROBES, INSTALL_TOOL_PROBES, CONTROL, SHELL_NOT_FOUND, DOCKER_PROBE;
55473
+ var init_local_setup_probe = __esm({
55474
+ "bin/local-setup-probe.js"() {
55475
+ "use strict";
55476
+ PROBE_TIMEOUT_MS3 = 3e3;
55477
+ RUNTIME_PROBES = {
55478
+ node: { tool: "node", file: "node", args: ["--version"] },
55479
+ python: { tool: "python3", file: "python3", args: ["--version"] },
55480
+ go: { tool: "go", file: "go", args: ["version"] },
55481
+ rust: { tool: "cargo", file: "cargo", args: ["--version"] },
55482
+ jvm: { tool: "java", file: "java", args: ["-version"] },
55483
+ ruby: { tool: "ruby", file: "ruby", args: ["--version"] },
55484
+ dotnet: { tool: "dotnet", file: "dotnet", args: ["--version"] },
55485
+ cpp: { tool: "c++", file: "c++", args: ["--version"] }
55486
+ };
55487
+ INSTALL_TOOL_PROBES = {
55488
+ npm: { tool: "npm", file: "npm", args: ["--version"] },
55489
+ yarn: { tool: "yarn", file: "yarn", args: ["--version"] },
55490
+ pnpm: { tool: "pnpm", file: "pnpm", args: ["--version"] },
55491
+ bun: { tool: "bun", file: "bun", args: ["--version"] },
55492
+ pip: { tool: "pip", file: "pip", args: ["--version"] },
55493
+ pip3: { tool: "pip3", file: "pip3", args: ["--version"] },
55494
+ poetry: { tool: "poetry", file: "poetry", args: ["--version"] },
55495
+ uv: { tool: "uv", file: "uv", args: ["--version"] },
55496
+ pipenv: { tool: "pipenv", file: "pipenv", args: ["--version"] },
55497
+ bundle: { tool: "bundle", file: "bundle", args: ["--version"] },
55498
+ mvn: { tool: "mvn", file: "mvn", args: ["--version"] },
55499
+ gradle: { tool: "gradle", file: "gradle", args: ["--version"] },
55500
+ make: { tool: "make", file: "make", args: ["--version"] }
55501
+ };
55502
+ CONTROL = /[\u0000-\u001f\u007f-\u009f]/g;
55503
+ SHELL_NOT_FOUND = /* @__PURE__ */ new Set([127, 9009]);
55504
+ DOCKER_PROBE = {
55505
+ tool: "docker",
55506
+ file: "docker",
55507
+ args: ["--version"],
55508
+ foundNote: "Docker client found; daemon not checked."
55509
+ };
55510
+ }
55511
+ });
55512
+
55348
55513
  // ../../node_modules/zod/v4/core/core.js
55349
55514
  // @__NO_SIDE_EFFECTS__
55350
55515
  function $constructor(name, initializer3, params) {
@@ -77500,9 +77665,12 @@ function sessionDiagnostic(entry, hostDiffers) {
77500
77665
  detail: "This shell resolves a different host from the one the session was linked to, so the credential was withheld rather than presented to a server that did not mint it, and the counts above are empty. Run `terminalhire link` in this shell, or point it back at the host the session belongs to. No request went out, so nothing was refused."
77501
77666
  };
77502
77667
  }
77503
- const c = sessionCase({ ...entry, sessionHostMismatch: null }, {
77504
- stale: entry.sessionStale === true
77505
- });
77668
+ const c = sessionCase(
77669
+ { ...entry, sessionHostMismatch: null },
77670
+ {
77671
+ stale: entry.sessionStale === true
77672
+ }
77673
+ );
77506
77674
  if (c === null) return { state: "ok" };
77507
77675
  if (c.kind === "refused") {
77508
77676
  return {
@@ -77615,6 +77783,12 @@ async function resolveClaimPreview(opportunity, { semantic = false } = {}) {
77615
77783
  assignees: bounty.assignees,
77616
77784
  contested: isContested2(bounty),
77617
77785
  policy: policy ? publicPolicy(policy) : null,
77786
+ // TERM-1259 step 5. What the work needs, exactly as the server put it on the index;
77787
+ // null when the entry carries none. `localSetup` is this machine's answer to it, from
77788
+ // a fixed table of version probes (`local-setup-probe.js`). It goes back to the local
77789
+ // caller only and is never sent anywhere.
77790
+ requirements: bounty.runRequirements ?? null,
77791
+ localSetup: bounty.runRequirements ? await probeLocalSetup(bounty.runRequirements) : null,
77618
77792
  _policy: policy,
77619
77793
  // Internal routing bit from the CLI's shared resolver. Founder postings are
77620
77794
  // not ordinary local-only claims: the interactive CLI must register them
@@ -77991,6 +78165,7 @@ var init_jpi_mcp = __esm({
77991
78165
  init_api_base();
77992
78166
  init_web_session();
77993
78167
  init_session_case();
78168
+ init_local_setup_probe();
77994
78169
  TOOL_NAMES = [
77995
78170
  "jobs",
77996
78171
  "bounties",
@@ -78074,7 +78249,7 @@ var init_jpi_mcp = __esm({
78074
78249
  },
78075
78250
  {
78076
78251
  name: "claim_preview",
78077
- description: "Preview a bounty or contribution before claiming. Performs governed public reads for issue freshness, contention, and repository policy; writes nothing.",
78252
+ description: "Preview a bounty or contribution before claiming. Performs governed public reads for issue freshness, contention, and repository policy; writes nothing. For a posted bounty it also returns what the work needs (runtime, install and test commands, services, OS) and which of those tools this machine has, checked locally and sent nowhere.",
78078
78253
  inputSchema: CLAIM_PREVIEW_SCHEMA
78079
78254
  },
78080
78255
  {
@@ -1644,7 +1644,7 @@ function makeDefaultGovernorConfig(o) {
1644
1644
  budgetMs: o.budgetMs ?? readBuildBudgetMs(),
1645
1645
  sleep: o.sleep ?? realSleep,
1646
1646
  now: o.now ?? Date.now,
1647
- probeTimeoutMs: o.probeTimeoutMs ?? (o.paceEnabled ? PROBE_TIMEOUT_MS : null)
1647
+ probeTimeoutMs: o.probeTimeoutMs ?? (o.paceEnabled ? PROBE_TIMEOUT_MS2 : null)
1648
1648
  };
1649
1649
  }
1650
1650
  function makeGitHubGovernor(fetchImpl, cfg) {
@@ -1789,7 +1789,7 @@ function makeGitHubGovernor(fetchImpl, cfg) {
1789
1789
  }
1790
1790
  return { get, graphql, probe, setSecondaryHint, getStats, tripped, budgetExhausted };
1791
1791
  }
1792
- var DEFAULT_REQ_GAP_MS, SECONDARY_BACKOFF_CAP_S, DEFAULT_BUILD_BUDGET_MS, MIN_BUILD_BUDGET_MS, MAX_BUILD_BUDGET_MS, PROBE_TIMEOUT_MS, realSleep;
1792
+ var DEFAULT_REQ_GAP_MS, SECONDARY_BACKOFF_CAP_S, DEFAULT_BUILD_BUDGET_MS, MIN_BUILD_BUDGET_MS, MAX_BUILD_BUDGET_MS, PROBE_TIMEOUT_MS2, realSleep;
1793
1793
  var init_gh_governor = __esm({
1794
1794
  "../../packages/core/src/gh-governor.ts"() {
1795
1795
  "use strict";
@@ -1798,7 +1798,7 @@ var init_gh_governor = __esm({
1798
1798
  DEFAULT_BUILD_BUDGET_MS = 9e4;
1799
1799
  MIN_BUILD_BUDGET_MS = 1e4;
1800
1800
  MAX_BUILD_BUDGET_MS = 9e4;
1801
- PROBE_TIMEOUT_MS = 3e3;
1801
+ PROBE_TIMEOUT_MS2 = 3e3;
1802
1802
  realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
1803
1803
  }
1804
1804
  });
@@ -3726,10 +3726,10 @@ function parseComment(item) {
3726
3726
  if (!item.text || item.text.trim().length < 20) return null;
3727
3727
  const raw = stripHtml2(item.text);
3728
3728
  if (!raw) return null;
3729
- const firstLine = raw.split(/\n/)[0];
3730
- const parts = firstLine.split("|").map((s) => s.trim());
3729
+ const firstLine2 = raw.split(/\n/)[0];
3730
+ const parts = firstLine2.split("|").map((s) => s.trim());
3731
3731
  const company = parts[0] ?? "Unknown";
3732
- const title = parts[1] ?? firstLine.slice(0, 80).trim();
3732
+ const title = parts[1] ?? firstLine2.slice(0, 80).trim();
3733
3733
  const location = parts[2] ?? "";
3734
3734
  if (company.toLowerCase().startsWith("note:") || company.toLowerCase().startsWith("ps:") || title.length < 3) {
3735
3735
  return null;
@@ -4637,7 +4637,7 @@ async function aggregateContributions(opts = {}) {
4637
4637
  now: opts.nowImpl ?? Date.now,
4638
4638
  // Bound the unguarded probe on the REAL network only; injected-fetch tests get
4639
4639
  // null so the probe stays deterministic (and its shared spy sleeper untouched).
4640
- probeTimeoutMs: opts.fetchImpl ? null : PROBE_TIMEOUT_MS
4640
+ probeTimeoutMs: opts.fetchImpl ? null : PROBE_TIMEOUT_MS2
4641
4641
  },
4642
4642
  opts.token
4643
4643
  );
@@ -10940,7 +10940,7 @@ __export(src_exports, {
10940
10940
  METRICS: () => METRICS,
10941
10941
  MIN_CONTRIBUTORS: () => MIN_CONTRIBUTORS,
10942
10942
  MIN_STARS: () => MIN_STARS,
10943
- PROBE_TIMEOUT_MS: () => PROBE_TIMEOUT_MS,
10943
+ PROBE_TIMEOUT_MS: () => PROBE_TIMEOUT_MS2,
10944
10944
  PROVENANCE: () => PROVENANCE,
10945
10945
  RIGOR: () => RIGOR,
10946
10946
  ROLLUP_VERSION: () => ROLLUP_VERSION,
@@ -17674,7 +17674,7 @@ var init_fs_util = __esm({
17674
17674
  import * as fs2 from "fs/promises";
17675
17675
  import * as fssync from "fs";
17676
17676
  import * as path3 from "path";
17677
- import { execFile } from "child_process";
17677
+ import { execFile as execFile2 } from "child_process";
17678
17678
  import { promisify } from "util";
17679
17679
  import { Readable } from "stream";
17680
17680
  import { pipeline } from "stream/promises";
@@ -17832,7 +17832,7 @@ var init_skills = __esm({
17832
17832
  init_error();
17833
17833
  init_log();
17834
17834
  init_fs_util();
17835
- execFileAsync = promisify(execFile);
17835
+ execFileAsync = promisify(execFile2);
17836
17836
  }
17837
17837
  });
17838
17838
 
@@ -30213,7 +30213,7 @@ var init_dist2 = __esm({
30213
30213
  function readDaemonId(docker3, label) {
30214
30214
  let res;
30215
30215
  try {
30216
- res = docker3.sync(["info", "--format", "{{.ID}}"], { timeoutMs: PROBE_TIMEOUT_MS2 });
30216
+ res = docker3.sync(["info", "--format", "{{.ID}}"], { timeoutMs: PROBE_TIMEOUT_MS3 });
30217
30217
  } catch (err) {
30218
30218
  const msg = err instanceof Error ? err.message : String(err);
30219
30219
  return { id: null, detail: `${label} daemon probe threw: ${msg}` };
@@ -30260,12 +30260,12 @@ function describeVenueDaemon(verdict) {
30260
30260
  }
30261
30261
  return `venue daemon not distinct (${verdict.reason}): ${verdict.detail}`;
30262
30262
  }
30263
- var PROBE_TIMEOUT_MS2, DAEMON_ID;
30263
+ var PROBE_TIMEOUT_MS3, DAEMON_ID;
30264
30264
  var init_venueProof = __esm({
30265
30265
  "../../packages/envrun/dist/venueProof.js"() {
30266
30266
  "use strict";
30267
30267
  init_dist();
30268
- PROBE_TIMEOUT_MS2 = 2e4;
30268
+ PROBE_TIMEOUT_MS3 = 2e4;
30269
30269
  DAEMON_ID = /^[A-Za-z0-9:._-]+$/;
30270
30270
  }
30271
30271
  });
@@ -30275,7 +30275,7 @@ function readDaemonFacts(docker3) {
30275
30275
  let res;
30276
30276
  try {
30277
30277
  res = docker3.sync(["info", "--format", DAEMON_FACTS_FORMAT], {
30278
- timeoutMs: PROBE_TIMEOUT_MS2
30278
+ timeoutMs: PROBE_TIMEOUT_MS3
30279
30279
  });
30280
30280
  } catch {
30281
30281
  return null;
@@ -34970,6 +34970,13 @@ var init_preview2 = __esm({
34970
34970
  }
34971
34971
  });
34972
34972
 
34973
+ // ../../packages/envspec/dist/runRequirements.js
34974
+ var init_runRequirements = __esm({
34975
+ "../../packages/envspec/dist/runRequirements.js"() {
34976
+ "use strict";
34977
+ }
34978
+ });
34979
+
34973
34980
  // ../../packages/envspec/dist/index.js
34974
34981
  var init_dist3 = __esm({
34975
34982
  "../../packages/envspec/dist/index.js"() {
@@ -34977,6 +34984,7 @@ var init_dist3 = __esm({
34977
34984
  init_derive();
34978
34985
  init_repo();
34979
34986
  init_preview2();
34987
+ init_runRequirements();
34980
34988
  init_yaml();
34981
34989
  }
34982
34990
  });
@@ -37186,7 +37194,7 @@ __export(dist_exports, {
37186
37194
  PLACEMENT_ALIASES: () => PLACEMENT_ALIASES,
37187
37195
  PRISMA_IMAGE: () => PRISMA_IMAGE,
37188
37196
  PRISMA_INSTALL_ALLOWLIST: () => PRISMA_INSTALL_ALLOWLIST,
37189
- PROBE_TIMEOUT_MS: () => PROBE_TIMEOUT_MS2,
37197
+ PROBE_TIMEOUT_MS: () => PROBE_TIMEOUT_MS3,
37190
37198
  PSQL_IMAGE: () => PSQL_IMAGE,
37191
37199
  PreviewError: () => PreviewError,
37192
37200
  REDACTED_TARGET_REPO: () => REDACTED_TARGET_REPO,
@@ -37430,7 +37438,7 @@ var identity_exports = {};
37430
37438
  __export(identity_exports, {
37431
37439
  resolveCallerIdentity: () => resolveCallerIdentity
37432
37440
  });
37433
- import { execFile as execFile2 } from "child_process";
37441
+ import { execFile as execFile3 } from "child_process";
37434
37442
  import { promisify as promisify2 } from "util";
37435
37443
  async function resolveCallerIdentity() {
37436
37444
  try {
@@ -37448,7 +37456,7 @@ var execFileAsync2;
37448
37456
  var init_identity = __esm({
37449
37457
  "src/reputation/identity.ts"() {
37450
37458
  "use strict";
37451
- execFileAsync2 = promisify2(execFile2);
37459
+ execFileAsync2 = promisify2(execFile3);
37452
37460
  }
37453
37461
  });
37454
37462
 
@@ -37579,6 +37587,7 @@ __export(jpi_claim_exports, {
37579
37587
  POSTING_LEVEL_RESOLUTION_REASONS: () => POSTING_LEVEL_RESOLUTION_REASONS,
37580
37588
  PUSH_TOKEN_REFUSAL: () => PUSH_TOKEN_REFUSAL,
37581
37589
  REVISE_RECOVERY_STATES: () => REVISE_RECOVERY_STATES,
37590
+ SETUP_FAILED_REQUIREMENTS: () => SETUP_FAILED_REQUIREMENTS,
37582
37591
  SUBMIT_ACCEPTS: () => SUBMIT_ACCEPTS,
37583
37592
  SYNC_BACKGROUND_PUSH_ACTIVE_FIELD: () => SYNC_BACKGROUND_PUSH_ACTIVE_FIELD,
37584
37593
  VERIFY_REL_PATH: () => VERIFY_REL_PATH,
@@ -37690,7 +37699,7 @@ import {
37690
37699
  import { join as join32, dirname as dirname10, isAbsolute as isAbsolute5, resolve as pathResolve } from "path";
37691
37700
  import { createHash as createHash11 } from "crypto";
37692
37701
  import { homedir as homedir15, hostname as osHostname } from "os";
37693
- import { execFile as execFile3, execFileSync as execFileSync2, spawnSync as spawnSync8 } from "child_process";
37702
+ import { execFile as execFile4, execFileSync as execFileSync2, spawnSync as spawnSync8 } from "child_process";
37694
37703
  import { promisify as promisify3 } from "util";
37695
37704
  import { createInterface as createInterface2 } from "readline";
37696
37705
  function readNudgedClaimIds() {
@@ -38060,7 +38069,12 @@ function extractClaimableFields(job) {
38060
38069
  // server-side (Decision 4). `claimMode` follows the index row's absent-means-
38061
38070
  // default convention: absent = open.
38062
38071
  founderPosting: b.bountySource === "founder",
38063
- claimMode: b.claimMode === "approval-only" ? "approval-only" : "open"
38072
+ claimMode: b.claimMode === "approval-only" ? "approval-only" : "open",
38073
+ // TERM-1259. What the work needs, as the SERVER projected it onto the index
38074
+ // (`publicRunRequirements`). Carried, never derived here: the CLI has no copy of the
38075
+ // repository before a claim, and a second derivation could disagree with the one the
38076
+ // poster saw. Null when the index carries none.
38077
+ runRequirements: b.runRequirements && typeof b.runRequirements === "object" ? b.runRequirements : null
38064
38078
  };
38065
38079
  }
38066
38080
  function parseGitHubUrl(url) {
@@ -38659,6 +38673,7 @@ async function resolveBounty(arg) {
38659
38673
  let bountyId, title, repoFullName, issueUrl, amountUSD, source, openPRsAtDiscovery, indexNativeId;
38660
38674
  let founderPosting = false;
38661
38675
  let claimMode = "open";
38676
+ let runRequirements = null;
38662
38677
  let job = findClaimableInCache(arg) ?? (looksLikeShortRef(arg) ? findClaimableByShortRef(arg) : null);
38663
38678
  let freshPool;
38664
38679
  if (!job && looksLikeShortRef(arg)) {
@@ -38675,7 +38690,8 @@ async function resolveBounty(arg) {
38675
38690
  source,
38676
38691
  openPRsAtDiscovery,
38677
38692
  founderPosting,
38678
- claimMode
38693
+ claimMode,
38694
+ runRequirements
38679
38695
  } = extractClaimableFields(job));
38680
38696
  indexNativeId = bountyId;
38681
38697
  } else {
@@ -38746,7 +38762,9 @@ async function resolveBounty(arg) {
38746
38762
  // GitHub issue), so the defaults above are correct there: a URL-tier claim is
38747
38763
  // never treated as founder supply.
38748
38764
  founderPosting,
38749
- claimMode
38765
+ claimMode,
38766
+ // TERM-1259. Only a founder index hit carries these; every other path is null.
38767
+ runRequirements: runRequirements ?? null
38750
38768
  };
38751
38769
  }
38752
38770
  function fmtOpenPRsLine(b) {
@@ -42660,10 +42678,14 @@ async function postJson(fetchImpl, url, body, linked) {
42660
42678
  function resolveUsageLines() {
42661
42679
  return [
42662
42680
  'Usage: terminalhire claim resolve <id> --reason <reason> [--note "..."]',
42681
+ " [--requirement <field>]",
42663
42682
  "",
42664
42683
  " Hand a claim back and tell the poster why. Needs `terminalhire link`.",
42665
42684
  "",
42666
- ...resolutionReasonHelpLines()
42685
+ ...resolutionReasonHelpLines(),
42686
+ ` With --reason setup-failed, --requirement names what would not set up:`,
42687
+ ` ${SETUP_FAILED_REQUIREMENTS.join(", ")}`,
42688
+ ""
42667
42689
  ];
42668
42690
  }
42669
42691
  function printResolveHint(list, log) {
@@ -42719,6 +42741,16 @@ async function cmdResolve(id, flags = {}, deps = {}) {
42719
42741
  for (const line of resolutionReasonHelpLines()) err(line);
42720
42742
  return 1;
42721
42743
  }
42744
+ const requirement = typeof flags.requirement === "string" && flags.requirement.trim() ? flags.requirement.trim() : null;
42745
+ if (requirement !== null && reason !== "setup-failed") {
42746
+ err("terminalhire claim resolve: --requirement is used only with --reason setup-failed.");
42747
+ return 1;
42748
+ }
42749
+ if (requirement !== null && !SETUP_FAILED_REQUIREMENTS.includes(requirement)) {
42750
+ err(`terminalhire claim resolve: '${requirement}' is not a run requirement. One of:`);
42751
+ err(` ${SETUP_FAILED_REQUIREMENTS.join(", ")}`);
42752
+ return 1;
42753
+ }
42722
42754
  const linked = deps.cookie ? { cookie: deps.cookie, mismatch: null } : webSessionCookieForHost(CLAIM_SYNC_BASE4);
42723
42755
  if (linked.mismatch) {
42724
42756
  err(
@@ -42733,6 +42765,7 @@ async function cmdResolve(id, flags = {}, deps = {}) {
42733
42765
  const note = typeof flags.note === "string" && flags.note.trim() ? flags.note.trim() : null;
42734
42766
  const body = { bountyId: founderPostingIdOf(claim), claimId, reason };
42735
42767
  if (note) body.note = note;
42768
+ if (requirement) body.failedRequirement = requirement;
42736
42769
  let res;
42737
42770
  try {
42738
42771
  res = await fetchImpl(`${CLAIM_SYNC_BASE4}/api/claim/resolution`, {
@@ -42769,6 +42802,9 @@ async function cmdResolve(id, flags = {}, deps = {}) {
42769
42802
  log(
42770
42803
  suspended ? " The posting is off the market until the poster acts on it." : " The posting is open again for someone else."
42771
42804
  );
42805
+ if (reason === "setup-failed") {
42806
+ log(` A setup failure does not stop you: you may claim it again once it sets up.`);
42807
+ }
42772
42808
  log("");
42773
42809
  log(` The local record is still here. To drop it: terminalhire claim release ${id}`);
42774
42810
  log("");
@@ -42876,7 +42912,7 @@ async function run() {
42876
42912
  process.exit(1);
42877
42913
  }
42878
42914
  }
42879
- var TERMINALHIRE_DIR11, INDEX_CACHE_FILE2, CLAIM_PUSH_MARKER, REPO_CONTINUITY_NUDGE_MARKER, API_URL, CLAIM_SYNC_BASE4, CLAIM_CONSENT_VERSION, CLAIM_POLL_INTERVAL_MS, CLAIM_POLL_TIMEOUT_MS, CLAIM_SYNC_WRITE_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, CLOSED_STATES, GH_SESSION_COOKIE, PUSH_TOKEN_REFUSAL, SYNC_BACKGROUND_PUSH_ACTIVE_FIELD, ISSUE_OUTCOME_TERMINAL, RUNS_POLL_INTERVAL_MS, RUNS_POLL_ATTEMPTS, OPENABLE_AGENTS, BRIEF_DIR, BRIEF_REL_PATH, VERIFY_REL_PATH, AGENTS_REL_PATH, BRIEF_EXCLUDE_LINE, PACK_SAFE_ID, CLAIM_EVENT_LABEL, LINE_BREAKS, CONTROL_CHARS3, FOUNDER_POSTING_ID, CLAIM_RESOLUTION_REASONS, POSTING_LEVEL_RESOLUTION_REASONS, RESOLUTION_REASON_BLURB;
42915
+ var TERMINALHIRE_DIR11, INDEX_CACHE_FILE2, CLAIM_PUSH_MARKER, REPO_CONTINUITY_NUDGE_MARKER, API_URL, CLAIM_SYNC_BASE4, CLAIM_CONSENT_VERSION, CLAIM_POLL_INTERVAL_MS, CLAIM_POLL_TIMEOUT_MS, CLAIM_SYNC_WRITE_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, CLOSED_STATES, GH_SESSION_COOKIE, PUSH_TOKEN_REFUSAL, SYNC_BACKGROUND_PUSH_ACTIVE_FIELD, ISSUE_OUTCOME_TERMINAL, RUNS_POLL_INTERVAL_MS, RUNS_POLL_ATTEMPTS, OPENABLE_AGENTS, BRIEF_DIR, BRIEF_REL_PATH, VERIFY_REL_PATH, AGENTS_REL_PATH, BRIEF_EXCLUDE_LINE, PACK_SAFE_ID, CLAIM_EVENT_LABEL, LINE_BREAKS, CONTROL_CHARS3, FOUNDER_POSTING_ID, CLAIM_RESOLUTION_REASONS, SETUP_FAILED_REQUIREMENTS, POSTING_LEVEL_RESOLUTION_REASONS, RESOLUTION_REASON_BLURB;
42880
42916
  var init_jpi_claim = __esm({
42881
42917
  "bin/jpi-claim.js"() {
42882
42918
  "use strict";
@@ -42905,7 +42941,7 @@ var init_jpi_claim = __esm({
42905
42941
  GH_HEADERS2 = { "User-Agent": "terminalhire-claim", Accept: "application/vnd.github+json" };
42906
42942
  CONTENTION_HINT = " tip: if scopes overlap, comment on the ISSUE comparing scope \u2014 generous + compatible wins triage.";
42907
42943
  AI_DISCLOSURE_NOTE = "---\nThis contribution was developed with AI assistance via [terminalhire](https://terminalhire.com). The author has reviewed the change and takes responsibility for its content.";
42908
- pExecFile = promisify3(execFile3);
42944
+ pExecFile = promisify3(execFile4);
42909
42945
  VALUE_FLAGS = /* @__PURE__ */ new Set([
42910
42946
  "worktree",
42911
42947
  "branch",
@@ -42924,7 +42960,9 @@ var init_jpi_claim = __esm({
42924
42960
  // `--sha` was here for `claim note approve` and went with it: the send moved to the
42925
42961
  // dashboard, so no CLI verb names a digest any more. Left in place it would parse a
42926
42962
  // flag nothing reads, which is how a removed feature looks half-removed.
42927
- "body"
42963
+ "body",
42964
+ // TERM-1259. `claim resolve --reason setup-failed --requirement <field>`.
42965
+ "requirement"
42928
42966
  ]);
42929
42967
  ASSIGNMENT_MARKER = "<!-- terminalhire:assignment-request -->";
42930
42968
  STAKE_MARKER = "<!-- terminalhire:claim-stake -->";
@@ -42998,7 +43036,16 @@ var init_jpi_claim = __esm({
42998
43036
  "out-of-time",
42999
43037
  "already-implemented",
43000
43038
  "repo-does-not-build",
43001
- "brief-insufficient"
43039
+ "brief-insufficient",
43040
+ "setup-failed"
43041
+ ];
43042
+ SETUP_FAILED_REQUIREMENTS = [
43043
+ "runtime",
43044
+ "runtimeVersion",
43045
+ "install",
43046
+ "test",
43047
+ "services",
43048
+ "os"
43002
43049
  ];
43003
43050
  POSTING_LEVEL_RESOLUTION_REASONS = [
43004
43051
  "already-implemented",
@@ -43010,7 +43057,8 @@ var init_jpi_claim = __esm({
43010
43057
  "out-of-time": "you ran out of time for it",
43011
43058
  "already-implemented": "the repo already has this",
43012
43059
  "repo-does-not-build": "the repo will not build, so nobody can finish it",
43013
- "brief-insufficient": "the task does not say enough to do the work"
43060
+ "brief-insufficient": "the task does not say enough to do the work",
43061
+ "setup-failed": "the environment would not set up; you may claim it again"
43014
43062
  };
43015
43063
  }
43016
43064
  });
@@ -65310,6 +65358,117 @@ function sessionCase(entry, { stale }) {
65310
65358
  return { kind: "expired" };
65311
65359
  }
65312
65360
 
65361
+ // bin/local-setup-probe.js
65362
+ import { execFile } from "child_process";
65363
+ var PROBE_TIMEOUT_MS = 3e3;
65364
+ var RUNTIME_PROBES = {
65365
+ node: { tool: "node", file: "node", args: ["--version"] },
65366
+ python: { tool: "python3", file: "python3", args: ["--version"] },
65367
+ go: { tool: "go", file: "go", args: ["version"] },
65368
+ rust: { tool: "cargo", file: "cargo", args: ["--version"] },
65369
+ jvm: { tool: "java", file: "java", args: ["-version"] },
65370
+ ruby: { tool: "ruby", file: "ruby", args: ["--version"] },
65371
+ dotnet: { tool: "dotnet", file: "dotnet", args: ["--version"] },
65372
+ cpp: { tool: "c++", file: "c++", args: ["--version"] }
65373
+ };
65374
+ var INSTALL_TOOL_PROBES = {
65375
+ npm: { tool: "npm", file: "npm", args: ["--version"] },
65376
+ yarn: { tool: "yarn", file: "yarn", args: ["--version"] },
65377
+ pnpm: { tool: "pnpm", file: "pnpm", args: ["--version"] },
65378
+ bun: { tool: "bun", file: "bun", args: ["--version"] },
65379
+ pip: { tool: "pip", file: "pip", args: ["--version"] },
65380
+ pip3: { tool: "pip3", file: "pip3", args: ["--version"] },
65381
+ poetry: { tool: "poetry", file: "poetry", args: ["--version"] },
65382
+ uv: { tool: "uv", file: "uv", args: ["--version"] },
65383
+ pipenv: { tool: "pipenv", file: "pipenv", args: ["--version"] },
65384
+ bundle: { tool: "bundle", file: "bundle", args: ["--version"] },
65385
+ mvn: { tool: "mvn", file: "mvn", args: ["--version"] },
65386
+ gradle: { tool: "gradle", file: "gradle", args: ["--version"] },
65387
+ make: { tool: "make", file: "make", args: ["--version"] }
65388
+ };
65389
+ var CONTROL = /[\u0000-\u001f\u007f-\u009f]/g;
65390
+ function firstLine(text) {
65391
+ const line = String(text ?? "").split(/\r\n|\r|\n/).map((l) => l.trim()).find((l) => l !== "");
65392
+ return line ? line.replace(CONTROL, "").slice(0, 80) : null;
65393
+ }
65394
+ var SHELL_NOT_FOUND = /* @__PURE__ */ new Set([127, 9009]);
65395
+ function runProbe(file, args, timeoutMs = PROBE_TIMEOUT_MS, { shell = false } = {}) {
65396
+ return new Promise((resolve5) => {
65397
+ let child;
65398
+ const opts = {
65399
+ timeout: timeoutMs,
65400
+ killSignal: "SIGKILL",
65401
+ windowsHide: true,
65402
+ maxBuffer: 64 * 1024
65403
+ };
65404
+ const done2 = (err, stdout, stderr) => {
65405
+ if (!err) {
65406
+ resolve5({ ok: true, version: firstLine(stdout) ?? firstLine(stderr) });
65407
+ return;
65408
+ }
65409
+ if (err.code === "ENOENT" || shell && SHELL_NOT_FOUND.has(err.code)) {
65410
+ resolve5({ ok: false, reason: "not found" });
65411
+ } else if (err.killed || err.signal) resolve5({ ok: false, reason: "timed out" });
65412
+ else resolve5({ ok: false, reason: "exited with an error" });
65413
+ };
65414
+ try {
65415
+ child = shell ? execFile([file, ...args].join(" "), [], { ...opts, shell: true }, done2) : execFile(file, args, opts, done2);
65416
+ } catch {
65417
+ resolve5({ ok: false, reason: "not found" });
65418
+ return;
65419
+ }
65420
+ child.stdin?.end();
65421
+ });
65422
+ }
65423
+ var DOCKER_PROBE = {
65424
+ tool: "docker",
65425
+ file: "docker",
65426
+ args: ["--version"],
65427
+ foundNote: "Docker client found; daemon not checked."
65428
+ };
65429
+ function probesFor(requirements) {
65430
+ if (!requirements || typeof requirements !== "object") return [];
65431
+ const out = [];
65432
+ const runtime = requirements.runtime?.value;
65433
+ if (typeof runtime === "string" && RUNTIME_PROBES[runtime]) {
65434
+ out.push({ ...RUNTIME_PROBES[runtime], for: "runtime" });
65435
+ }
65436
+ const install = requirements.install?.value;
65437
+ if (typeof install === "string") {
65438
+ const first = install.trim().split(/\s+/)[0];
65439
+ const probe = INSTALL_TOOL_PROBES[first];
65440
+ if (probe && !out.some((p) => p.tool === probe.tool)) out.push({ ...probe, for: "install" });
65441
+ }
65442
+ const services = requirements.services?.value;
65443
+ if (Array.isArray(services) && services.length > 0) {
65444
+ out.push({ ...DOCKER_PROBE, for: "services" });
65445
+ }
65446
+ return out;
65447
+ }
65448
+ async function probeLocalSetup(requirements, { run: run3 = runProbe, timeoutMs = PROBE_TIMEOUT_MS, platform = process.platform } = {}) {
65449
+ const probes = probesFor(requirements);
65450
+ const shell = platform === "win32";
65451
+ const results = await Promise.all(probes.map((p) => run3(p.file, p.args, timeoutMs, { shell })));
65452
+ const have = [];
65453
+ const missing = [];
65454
+ probes.forEach((p, i) => {
65455
+ const r = results[i];
65456
+ const base = { tool: p.tool, for: p.for };
65457
+ if (r.ok) {
65458
+ have.push({
65459
+ ...base,
65460
+ ...p.foundNote ? { note: p.foundNote } : {},
65461
+ version: r.version ?? null
65462
+ });
65463
+ } else missing.push({ ...base, reason: r.reason });
65464
+ });
65465
+ return {
65466
+ note: "Checked on this machine. These results were not sent anywhere.",
65467
+ have,
65468
+ missing
65469
+ };
65470
+ }
65471
+
65313
65472
  // bin/jpi-mcp.js
65314
65473
  var TOOL_NAMES = [
65315
65474
  "jobs",
@@ -65407,9 +65566,12 @@ function sessionDiagnostic(entry, hostDiffers) {
65407
65566
  detail: "This shell resolves a different host from the one the session was linked to, so the credential was withheld rather than presented to a server that did not mint it, and the counts above are empty. Run `terminalhire link` in this shell, or point it back at the host the session belongs to. No request went out, so nothing was refused."
65408
65567
  };
65409
65568
  }
65410
- const c = sessionCase({ ...entry, sessionHostMismatch: null }, {
65411
- stale: entry.sessionStale === true
65412
- });
65569
+ const c = sessionCase(
65570
+ { ...entry, sessionHostMismatch: null },
65571
+ {
65572
+ stale: entry.sessionStale === true
65573
+ }
65574
+ );
65413
65575
  if (c === null) return { state: "ok" };
65414
65576
  if (c.kind === "refused") {
65415
65577
  return {
@@ -65522,6 +65684,12 @@ async function resolveClaimPreview(opportunity, { semantic = false } = {}) {
65522
65684
  assignees: bounty.assignees,
65523
65685
  contested: isContested2(bounty),
65524
65686
  policy: policy ? publicPolicy(policy) : null,
65687
+ // TERM-1259 step 5. What the work needs, exactly as the server put it on the index;
65688
+ // null when the entry carries none. `localSetup` is this machine's answer to it, from
65689
+ // a fixed table of version probes (`local-setup-probe.js`). It goes back to the local
65690
+ // caller only and is never sent anywhere.
65691
+ requirements: bounty.runRequirements ?? null,
65692
+ localSetup: bounty.runRequirements ? await probeLocalSetup(bounty.runRequirements) : null,
65525
65693
  _policy: policy,
65526
65694
  // Internal routing bit from the CLI's shared resolver. Founder postings are
65527
65695
  // not ordinary local-only claims: the interactive CLI must register them
@@ -65872,7 +66040,7 @@ var TOOL_DEFS = [
65872
66040
  },
65873
66041
  {
65874
66042
  name: "claim_preview",
65875
- description: "Preview a bounty or contribution before claiming. Performs governed public reads for issue freshness, contention, and repository policy; writes nothing.",
66043
+ description: "Preview a bounty or contribution before claiming. Performs governed public reads for issue freshness, contention, and repository policy; writes nothing. For a posted bounty it also returns what the work needs (runtime, install and test commands, services, OS) and which of those tools this machine has, checked locally and sent nowhere.",
65876
66044
  inputSchema: CLAIM_PREVIEW_SCHEMA
65877
66045
  },
65878
66046
  {
@@ -8445,6 +8445,13 @@ var init_preview2 = __esm({
8445
8445
  }
8446
8446
  });
8447
8447
 
8448
+ // ../../packages/envspec/dist/runRequirements.js
8449
+ var init_runRequirements = __esm({
8450
+ "../../packages/envspec/dist/runRequirements.js"() {
8451
+ "use strict";
8452
+ }
8453
+ });
8454
+
8448
8455
  // ../../packages/envspec/dist/index.js
8449
8456
  var init_dist3 = __esm({
8450
8457
  "../../packages/envspec/dist/index.js"() {
@@ -8452,6 +8459,7 @@ var init_dist3 = __esm({
8452
8459
  init_derive();
8453
8460
  init_repo();
8454
8461
  init_preview2();
8462
+ init_runRequirements();
8455
8463
  init_yaml();
8456
8464
  }
8457
8465
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.42.30",
3
+ "version": "0.42.31",
4
4
  "description": "Developer jobs and paid bounties, matched in the terminal — ambient matches in the Claude Code spinner. Claim a scoped bounty and get paid when the poster accepts it.",
5
5
  "repository": {
6
6
  "type": "git",