terminalhire 0.42.41 → 0.42.43

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.
@@ -12500,6 +12500,12 @@ function linkTitle(title, url) {
12500
12500
 
12501
12501
  // bin/jpi-bounties.js
12502
12502
  init_api_base();
12503
+
12504
+ // bin/review-mode-copy.js
12505
+ var POSTER_REVIEWED = "poster";
12506
+ var CLAIMANT_NOTICE = "The poster reviews this work by hand. We don't run tests on it.";
12507
+
12508
+ // bin/jpi-bounties.js
12503
12509
  init_founder_pin();
12504
12510
  var TERMINALHIRE_DIR7 = process.env.TERMINALHIRE_DIR || join13(homedir10(), ".terminalhire");
12505
12511
  var INDEX_CACHE_FILE2 = join13(TERMINALHIRE_DIR7, "index-cache.json");
@@ -12614,6 +12620,9 @@ ${i + 1}. ${linkTitle(job.title, job.url)} [${ref}]`);
12614
12620
  );
12615
12621
  const marker = projectMarkerLine(b);
12616
12622
  if (marker) console.log(` ${marker}`);
12623
+ if (b.bountySource === "founder" && b.reviewMode === POSTER_REVIEWED) {
12624
+ console.log(` ${CLAIMANT_NOTICE}`);
12625
+ }
12617
12626
  if (reason) console.log(` ${reason}`);
12618
12627
  if (continuityNote) console.log(` ${continuityNote}`);
12619
12628
  if (b.publicSummary) {
@@ -31049,7 +31049,8 @@ var init_result = __esm({
31049
31049
  "leakState",
31050
31050
  "venue",
31051
31051
  "refusalOrigin",
31052
- "stoppedAt"
31052
+ "stoppedAt",
31053
+ "installFrozen"
31053
31054
  ];
31054
31055
  RENDER_NONE = null;
31055
31056
  FIELD_VIEWS = {
@@ -31114,7 +31115,10 @@ var init_result = __esm({
31114
31115
  // side refused in words a developer reads.
31115
31116
  refusalOrigin: RENDER_NONE,
31116
31117
  // Only when the run did not pass: on a green run "the last step" is just the last step.
31117
- stoppedAt: (r) => r.stoppedAt === null || r.outcome === "completed" ? null : `last step ${r.stoppedAt} (the last package script named in the output)`
31118
+ stoppedAt: (r) => r.stoppedAt === null || r.outcome === "completed" ? null : `last step ${r.stoppedAt} (the last package script named in the output)`,
31119
+ // The run already prints the unfrozen note while it runs (`installUnfrozenNote`); this is
31120
+ // the record of it for the worker and the intake.
31121
+ installFrozen: RENDER_NONE
31118
31122
  };
31119
31123
  }
31120
31124
  });
@@ -35152,6 +35156,10 @@ function nodeInstallCommand(repo) {
35152
35156
  return null;
35153
35157
  }
35154
35158
  function lockfileGaps(repo) {
35159
+ const read = readLockfileGaps(repo);
35160
+ return read === null || read.missing.length === 0 ? null : { lockfile: read.lockfile, missing: read.missing };
35161
+ }
35162
+ function readLockfileGaps(repo) {
35155
35163
  const pkg = readJsonObject(repo, "package.json");
35156
35164
  if (pkg === null)
35157
35165
  return null;
@@ -35178,7 +35186,7 @@ function lockfileGaps(repo) {
35178
35186
  missing.push(`${n}@${r} (${dir})`);
35179
35187
  }
35180
35188
  }
35181
- return missing.length === 0 ? null : { lockfile, missing };
35189
+ return { lockfile, missing, rootRead: rootListing !== null };
35182
35190
  }
35183
35191
  function wantedDependencies(pkg) {
35184
35192
  const wanted = [];
@@ -37679,7 +37687,9 @@ function refusedRun(fields) {
37679
37687
  venue: null,
37680
37688
  refusalOrigin: fields.origin,
37681
37689
  // No test step ran, so there is no output to read a step from.
37682
- stoppedAt: null
37690
+ stoppedAt: null,
37691
+ // Refused before any install step ran.
37692
+ installFrozen: null
37683
37693
  };
37684
37694
  }
37685
37695
  function refusalOriginOf(refusal2) {
@@ -37763,7 +37773,29 @@ function installUnfrozenNote(spec) {
37763
37773
  const u = spec.installUnfrozen;
37764
37774
  if (u === void 0)
37765
37775
  return null;
37766
- return `${u.lockfile} does not list ${u.missing.join(", ")}, so the install ran as \`${String(spec.installCommand)}\` without its frozen-lockfile check. Regenerate ${u.lockfile} before merging.`;
37776
+ return `${u.lockfile} does not list ${u.missing.join(", ")}, so the install ran as \`${String(spec.installCommand)}\` without its frozen-lockfile check. Include the updated ${u.lockfile} in the patch, or regenerate it before merging.`;
37777
+ }
37778
+ function isFrozenInstallCommand(command) {
37779
+ const lead = /^(?:(?:sudo|time)\s+|env(?:\s+[A-Za-z_]\w*=\S*)*\s+)*/.exec(command)?.[0] ?? "";
37780
+ const install = command.slice(lead.length).split(/;|&&|\|\|/)[0]?.trim() ?? "";
37781
+ if (/^npm ci(\s|$)/.test(install))
37782
+ return true;
37783
+ if (/^(yarn|pnpm|bun)\b/.test(install) && /\s--frozen-lockfile(?=\s|$)/.test(install)) {
37784
+ return true;
37785
+ }
37786
+ return /^yarn\b/.test(install) && /\s--immutable(?=\s|$)/.test(install);
37787
+ }
37788
+ function installFrozenFor(spec, install) {
37789
+ if (install === null)
37790
+ return null;
37791
+ const launched = install.exitCode !== INSTALL_NEVER_STARTED && (install.exitCode !== null || install.timedOut);
37792
+ if (!launched)
37793
+ return null;
37794
+ if (spec.runtime !== "node")
37795
+ return null;
37796
+ if (spec.installUnfrozen !== void 0)
37797
+ return false;
37798
+ return isFrozenInstallCommand(install.command);
37767
37799
  }
37768
37800
  async function verifyWorkingDiff(req) {
37769
37801
  const ctx = {
@@ -38132,7 +38164,8 @@ async function runVerification(req, ctx) {
38132
38164
  // A run that reached a verdict was not refused, so neither side refused it.
38133
38165
  refusalOrigin: null,
38134
38166
  // Read from the full test output, not `outputTail`, which is an excerpt.
38135
- stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? "")
38167
+ stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? ""),
38168
+ installFrozen: installFrozenFor(spec, verdict.install)
38136
38169
  };
38137
38170
  let later = null;
38138
38171
  if (req.screenshots !== void 0) {
@@ -38331,7 +38364,7 @@ async function prepareBaseTree(o) {
38331
38364
  throw err;
38332
38365
  }
38333
38366
  }
38334
- var ThRunError, OUTPUT_TAIL_BYTES, LOCKFILE_READ_CAP, FULL_COMMIT_ID, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, DEFERRED_GATE_TIMEOUT_MS, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA;
38367
+ var ThRunError, OUTPUT_TAIL_BYTES, LOCKFILE_READ_CAP, FULL_COMMIT_ID, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, DEFERRED_GATE_TIMEOUT_MS, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA, INSTALL_NEVER_STARTED;
38335
38368
  var init_thrun = __esm({
38336
38369
  "../../packages/envrun/dist/thrun.js"() {
38337
38370
  "use strict";
@@ -38420,6 +38453,7 @@ var init_thrun = __esm({
38420
38453
  FAILURE_LINE = /^(?:[ \t]*(?:not ok |FAILED |FAIL )|E {3}|# fail [1-9])/m;
38421
38454
  REDACTED_TARGET_REPO = "(refused before the target was accepted)";
38422
38455
  REDACTED_TARGET_SHA = "(refused)";
38456
+ INSTALL_NEVER_STARTED = 125;
38423
38457
  }
38424
38458
  });
38425
38459
 
@@ -39321,6 +39355,7 @@ __export(dist_exports, {
39321
39355
  imageRepo: () => imageRepo,
39322
39356
  imageVariantFor: () => imageVariantFor,
39323
39357
  installCommandFor: () => installCommandFor,
39358
+ installFrozenFor: () => installFrozenFor,
39324
39359
  installLocalMigrationTooling: () => installLocalMigrationTooling,
39325
39360
  installUnfrozenNote: () => installUnfrozenNote,
39326
39361
  isBookkeepingTable: () => isBookkeepingTable,
@@ -40162,6 +40197,13 @@ async function syncFounderVerdicts({
40162
40197
 
40163
40198
  // bin/jpi-claim.js
40164
40199
  init_founder_note_sync();
40200
+
40201
+ // bin/review-mode-copy.js
40202
+ var POSTER_REVIEWED = "poster";
40203
+ var CLAIMANT_NOTICE = "The poster reviews this work by hand. We don't run tests on it.";
40204
+ var HISTORY_LINE = "Submitted for the poster's review.";
40205
+
40206
+ // bin/jpi-claim.js
40165
40207
  var TERMINALHIRE_DIR11 = process.env.TERMINALHIRE_DIR || join31(homedir14(), ".terminalhire");
40166
40208
  var INDEX_CACHE_FILE2 = join31(TERMINALHIRE_DIR11, "index-cache.json");
40167
40209
  var CLAIM_PUSH_MARKER = join31(TERMINALHIRE_DIR11, "claim-push.json");
@@ -40631,9 +40673,15 @@ function extractClaimableFields(job) {
40631
40673
  // poster saw. Null when the index carries none.
40632
40674
  runRequirements: b.runRequirements && typeof b.runRequirements === "object" ? b.runRequirements : null,
40633
40675
  // TERM-1308. Present only when the index row carries them; no key otherwise.
40634
- ...b.bountySource === "founder" ? baselineFields(b) : {}
40676
+ ...b.bountySource === "founder" ? baselineFields(b) : {},
40677
+ // TERM-1344. The index carries `reviewMode` only for a poster-reviewed posting; any
40678
+ // other value is dropped rather than carried, the same as a malformed baseline field.
40679
+ ...b.bountySource === "founder" && b.reviewMode === POSTER_REVIEWED ? { reviewMode: POSTER_REVIEWED } : {}
40635
40680
  };
40636
40681
  }
40682
+ function reviewModeLine(b) {
40683
+ return b?.reviewMode === POSTER_REVIEWED ? CLAIMANT_NOTICE : null;
40684
+ }
40637
40685
  function parseGitHubUrl(url) {
40638
40686
  const m = String(url ?? "").match(/github\.com\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)/);
40639
40687
  if (!m) return null;
@@ -41250,7 +41298,7 @@ async function resolveBounty(arg) {
41250
41298
  if (freshPool) job = findByShortRefInPool(freshPool, arg);
41251
41299
  }
41252
41300
  if (job) {
41253
- let testBudgetMinutes, baselineStatus, baselineSha;
41301
+ let testBudgetMinutes, baselineStatus, baselineSha, reviewMode;
41254
41302
  ({
41255
41303
  bountyId,
41256
41304
  title,
@@ -41264,12 +41312,15 @@ async function resolveBounty(arg) {
41264
41312
  runRequirements,
41265
41313
  testBudgetMinutes,
41266
41314
  baselineStatus,
41267
- baselineSha
41315
+ baselineSha,
41316
+ reviewMode
41268
41317
  } = extractClaimableFields(job));
41269
41318
  baseline = {
41270
41319
  ...testBudgetMinutes === void 0 ? {} : { testBudgetMinutes },
41271
41320
  ...baselineStatus === void 0 ? {} : { baselineStatus },
41272
- ...baselineSha === void 0 ? {} : { baselineSha }
41321
+ ...baselineSha === void 0 ? {} : { baselineSha },
41322
+ // TERM-1344. Rides with the baseline fields: index-only, and no key when absent.
41323
+ ...reviewMode === void 0 ? {} : { reviewMode }
41273
41324
  };
41274
41325
  indexNativeId = bountyId;
41275
41326
  } else {
@@ -42054,6 +42105,8 @@ async function cmdPreview(arg, { json } = {}) {
42054
42105
  contested: isContested(b),
42055
42106
  // TERM-1308. The posting's test budget and baseline, only when the index carried them.
42056
42107
  ...baselineFields(b),
42108
+ // TERM-1344. Only for a poster-reviewed posting; absent means tests.
42109
+ ...b.reviewMode === POSTER_REVIEWED ? { reviewMode: POSTER_REVIEWED } : {},
42057
42110
  // Absent on a founder posting — see the scan above. A consumer must read
42058
42111
  // "no policy key" as "no policy step exists here", never as "clean".
42059
42112
  ...policy ? {
@@ -42084,8 +42137,8 @@ async function cmdPreview(arg, { json } = {}) {
42084
42137
  console.log(fmtOpenPRsLine(b));
42085
42138
  const previewContested = fmtContestedWarning(b);
42086
42139
  if (previewContested) console.log(previewContested);
42087
- const previewBaseline = baselineLine(b);
42088
- if (previewBaseline) console.log(` tests: ${previewBaseline}`);
42140
+ const previewTests = reviewModeLine(b) ?? baselineLine(b);
42141
+ if (previewTests) console.log(` tests: ${previewTests}`);
42089
42142
  if (policy) printPolicySection(policy);
42090
42143
  if (process.stdout.isTTY) {
42091
42144
  try {
@@ -43358,7 +43411,8 @@ function renderRunView(run2, message2, extra = {}) {
43358
43411
  const rateLimitCap = extra.rateLimitCap ?? run2.rateLimitCap ?? 5;
43359
43412
  const rateLimitCount = extra.rateLimitCount ?? run2.rateLimitCount;
43360
43413
  if (claimState) {
43361
- lines.push(` state: ${terminalSafeInline(claimState)}`);
43414
+ const stateText = claimState === "poster-review" ? HISTORY_LINE : terminalSafeInline(claimState);
43415
+ lines.push(` state: ${stateText}`);
43362
43416
  }
43363
43417
  if (unverifiedCause) {
43364
43418
  lines.push(` cause: ${terminalSafeInline(unverifiedCause)}`);
@@ -43417,7 +43471,8 @@ function renderClaimHistory(attempts, timeline) {
43417
43471
  for (const a of runs) {
43418
43472
  const sha = a.commitSha ? String(a.commitSha).slice(0, 10) : "";
43419
43473
  const jobs = Array.isArray(a.failingJobs) && a.failingJobs.length > 0 ? ` \u2014 ${a.failingJobs.map(terminalSafeInline).join(", ")}` : "";
43420
- const outcome = a.conclusion ? `checks ${terminalSafeInline(a.conclusion)}${jobs}` : a.status === "no-ci" ? "no checks ran" : "checks running";
43474
+ const note = typeof a.note === "string" ? terminalSafeInline(a.note).trim() : "";
43475
+ const outcome = a.conclusion ? `checks ${terminalSafeInline(a.conclusion)}${jobs}` : note !== "" ? note : a.status === "no-ci" ? "no checks ran" : "checks running";
43421
43476
  lines.push(` attempt ${a.attemptNumber} ${sha} ${outcome} ${shortWhen(a.at)}`);
43422
43477
  }
43423
43478
  for (const e of told) {
@@ -44169,6 +44224,67 @@ async function uploadClaimScreenshots(intake, uploads) {
44169
44224
  );
44170
44225
  }
44171
44226
  }
44227
+ var DEPENDENCY_SECTIONS2 = [
44228
+ "dependencies",
44229
+ "devDependencies",
44230
+ "optionalDependencies",
44231
+ "peerDependencies"
44232
+ ];
44233
+ var SENDABLE_LOCKFILES = [
44234
+ ["package-lock.json", "npm install"],
44235
+ ["yarn.lock", "yarn install"]
44236
+ ];
44237
+ async function packageJsonAt(wt, rev, path5) {
44238
+ let text;
44239
+ try {
44240
+ ({ stdout: text } = await pExecFile("git", ["-C", wt, "show", `${rev}:${path5}`], {
44241
+ shell: false,
44242
+ maxBuffer: 16 * 1024 * 1024
44243
+ }));
44244
+ } catch {
44245
+ return {};
44246
+ }
44247
+ try {
44248
+ const parsed = JSON.parse(String(text));
44249
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
44250
+ } catch {
44251
+ return null;
44252
+ }
44253
+ }
44254
+ function sectionKey(pkg, section) {
44255
+ const value = pkg[section];
44256
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
44257
+ return JSON.stringify(value ?? null);
44258
+ }
44259
+ return JSON.stringify(Object.keys(value).sort().map((k) => [k, value[k]]));
44260
+ }
44261
+ async function patchChangesDependencies(wt, base, touched) {
44262
+ for (const path5 of touched) {
44263
+ if (path5.split("/").pop() !== "package.json") continue;
44264
+ const before = await packageJsonAt(wt, base, path5);
44265
+ const after = await packageJsonAt(wt, "HEAD", path5);
44266
+ if (before === null || after === null) continue;
44267
+ if (DEPENDENCY_SECTIONS2.some((s) => sectionKey(before, s) !== sectionKey(after, s))) return true;
44268
+ }
44269
+ return false;
44270
+ }
44271
+ async function dependencyLockfileWarnings(wt, base, touched) {
44272
+ if (!await patchChangesDependencies(wt, base, touched)) return [];
44273
+ const warnings = [];
44274
+ for (const [lockfile, cmd] of SENDABLE_LOCKFILES) {
44275
+ let present;
44276
+ try {
44277
+ present = (await sh("git", ["-C", wt, "ls-tree", "--name-only", base, "--", lockfile])).trim() === lockfile;
44278
+ } catch {
44279
+ present = false;
44280
+ }
44281
+ if (!present || touched.includes(lockfile)) continue;
44282
+ warnings.push(
44283
+ ` \u26A0 package.json changes dependencies, but ${lockfile} is not in this patch. The run will install without its frozen-lockfile check. Run \`${cmd}\` and commit ${lockfile} to keep it frozen.`
44284
+ );
44285
+ }
44286
+ return warnings;
44287
+ }
44172
44288
  async function submitFounderPatch({ claims, claim, id, wt, flags }) {
44173
44289
  let roots;
44174
44290
  try {
@@ -44219,6 +44335,9 @@ async function submitFounderPatch({ claims, claim, id, wt, flags }) {
44219
44335
  );
44220
44336
  process.exit(1);
44221
44337
  }
44338
+ const lockfileWarnings = await dependencyLockfileWarnings(wt, base, touched);
44339
+ if (lockfileWarnings.length > 0) console.log(`
44340
+ ${lockfileWarnings.join("\n")}`);
44222
44341
  const authorName = await sh("git", ["-C", wt, "log", "-1", "--format=%an"]);
44223
44342
  const authorEmail = await sh("git", ["-C", wt, "log", "-1", "--format=%ae"]);
44224
44343
  const shots = flags.screenshots === void 0 ? null : loadClaimScreenshots(flags.screenshots);
@@ -45845,6 +45964,7 @@ export {
45845
45964
  resolveBounty,
45846
45965
  resolveDeliveryDir,
45847
45966
  resolveSubmitWorktree,
45967
+ reviewModeLine,
45848
45968
  reviseRecoveryCommand,
45849
45969
  revokeFailureAction,
45850
45970
  run,
@@ -15565,6 +15565,17 @@ var init_payout_split = __esm({
15565
15565
  }
15566
15566
  });
15567
15567
 
15568
+ // bin/review-mode-copy.js
15569
+ var POSTER_REVIEWED, CLAIMANT_NOTICE, HISTORY_LINE;
15570
+ var init_review_mode_copy = __esm({
15571
+ "bin/review-mode-copy.js"() {
15572
+ "use strict";
15573
+ POSTER_REVIEWED = "poster";
15574
+ CLAIMANT_NOTICE = "The poster reviews this work by hand. We don't run tests on it.";
15575
+ HISTORY_LINE = "Submitted for the poster's review.";
15576
+ }
15577
+ });
15578
+
15568
15579
  // src/repo-experience.ts
15569
15580
  var repo_experience_exports = {};
15570
15581
  __export(repo_experience_exports, {
@@ -16451,6 +16462,9 @@ ${i + 1}. ${linkTitle(job.title, job.url)} [${ref}]`);
16451
16462
  );
16452
16463
  const marker = projectMarkerLine(b);
16453
16464
  if (marker) console.log(` ${marker}`);
16465
+ if (b.bountySource === "founder" && b.reviewMode === POSTER_REVIEWED) {
16466
+ console.log(` ${CLAIMANT_NOTICE}`);
16467
+ }
16454
16468
  if (reason) console.log(` ${reason}`);
16455
16469
  if (continuityNote) console.log(` ${continuityNote}`);
16456
16470
  if (b.publicSummary) {
@@ -16706,6 +16720,7 @@ var init_jpi_bounties = __esm({
16706
16720
  init_index_revalidate();
16707
16721
  init_sanitize();
16708
16722
  init_api_base();
16723
+ init_review_mode_copy();
16709
16724
  init_founder_pin();
16710
16725
  TERMINALHIRE_DIR12 = process.env.TERMINALHIRE_DIR || join26(homedir20(), ".terminalhire");
16711
16726
  INDEX_CACHE_FILE3 = join26(TERMINALHIRE_DIR12, "index-cache.json");
@@ -36824,7 +36839,8 @@ var init_result = __esm({
36824
36839
  "leakState",
36825
36840
  "venue",
36826
36841
  "refusalOrigin",
36827
- "stoppedAt"
36842
+ "stoppedAt",
36843
+ "installFrozen"
36828
36844
  ];
36829
36845
  RENDER_NONE = null;
36830
36846
  FIELD_VIEWS = {
@@ -36889,7 +36905,10 @@ var init_result = __esm({
36889
36905
  // side refused in words a developer reads.
36890
36906
  refusalOrigin: RENDER_NONE,
36891
36907
  // Only when the run did not pass: on a green run "the last step" is just the last step.
36892
- stoppedAt: (r) => r.stoppedAt === null || r.outcome === "completed" ? null : `last step ${r.stoppedAt} (the last package script named in the output)`
36908
+ stoppedAt: (r) => r.stoppedAt === null || r.outcome === "completed" ? null : `last step ${r.stoppedAt} (the last package script named in the output)`,
36909
+ // The run already prints the unfrozen note while it runs (`installUnfrozenNote`); this is
36910
+ // the record of it for the worker and the intake.
36911
+ installFrozen: RENDER_NONE
36893
36912
  };
36894
36913
  }
36895
36914
  });
@@ -40927,6 +40946,10 @@ function nodeInstallCommand(repo) {
40927
40946
  return null;
40928
40947
  }
40929
40948
  function lockfileGaps(repo) {
40949
+ const read = readLockfileGaps(repo);
40950
+ return read === null || read.missing.length === 0 ? null : { lockfile: read.lockfile, missing: read.missing };
40951
+ }
40952
+ function readLockfileGaps(repo) {
40930
40953
  const pkg = readJsonObject(repo, "package.json");
40931
40954
  if (pkg === null)
40932
40955
  return null;
@@ -40953,7 +40976,7 @@ function lockfileGaps(repo) {
40953
40976
  missing.push(`${n}@${r} (${dir})`);
40954
40977
  }
40955
40978
  }
40956
- return missing.length === 0 ? null : { lockfile, missing };
40979
+ return { lockfile, missing, rootRead: rootListing !== null };
40957
40980
  }
40958
40981
  function wantedDependencies(pkg) {
40959
40982
  const wanted = [];
@@ -43454,7 +43477,9 @@ function refusedRun(fields) {
43454
43477
  venue: null,
43455
43478
  refusalOrigin: fields.origin,
43456
43479
  // No test step ran, so there is no output to read a step from.
43457
- stoppedAt: null
43480
+ stoppedAt: null,
43481
+ // Refused before any install step ran.
43482
+ installFrozen: null
43458
43483
  };
43459
43484
  }
43460
43485
  function refusalOriginOf(refusal2) {
@@ -43538,7 +43563,29 @@ function installUnfrozenNote(spec) {
43538
43563
  const u = spec.installUnfrozen;
43539
43564
  if (u === void 0)
43540
43565
  return null;
43541
- return `${u.lockfile} does not list ${u.missing.join(", ")}, so the install ran as \`${String(spec.installCommand)}\` without its frozen-lockfile check. Regenerate ${u.lockfile} before merging.`;
43566
+ return `${u.lockfile} does not list ${u.missing.join(", ")}, so the install ran as \`${String(spec.installCommand)}\` without its frozen-lockfile check. Include the updated ${u.lockfile} in the patch, or regenerate it before merging.`;
43567
+ }
43568
+ function isFrozenInstallCommand(command) {
43569
+ const lead = /^(?:(?:sudo|time)\s+|env(?:\s+[A-Za-z_]\w*=\S*)*\s+)*/.exec(command)?.[0] ?? "";
43570
+ const install = command.slice(lead.length).split(/;|&&|\|\|/)[0]?.trim() ?? "";
43571
+ if (/^npm ci(\s|$)/.test(install))
43572
+ return true;
43573
+ if (/^(yarn|pnpm|bun)\b/.test(install) && /\s--frozen-lockfile(?=\s|$)/.test(install)) {
43574
+ return true;
43575
+ }
43576
+ return /^yarn\b/.test(install) && /\s--immutable(?=\s|$)/.test(install);
43577
+ }
43578
+ function installFrozenFor(spec, install) {
43579
+ if (install === null)
43580
+ return null;
43581
+ const launched = install.exitCode !== INSTALL_NEVER_STARTED && (install.exitCode !== null || install.timedOut);
43582
+ if (!launched)
43583
+ return null;
43584
+ if (spec.runtime !== "node")
43585
+ return null;
43586
+ if (spec.installUnfrozen !== void 0)
43587
+ return false;
43588
+ return isFrozenInstallCommand(install.command);
43542
43589
  }
43543
43590
  async function verifyWorkingDiff(req) {
43544
43591
  const ctx = {
@@ -43907,7 +43954,8 @@ async function runVerification(req, ctx) {
43907
43954
  // A run that reached a verdict was not refused, so neither side refused it.
43908
43955
  refusalOrigin: null,
43909
43956
  // Read from the full test output, not `outputTail`, which is an excerpt.
43910
- stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? "")
43957
+ stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? ""),
43958
+ installFrozen: installFrozenFor(spec, verdict.install)
43911
43959
  };
43912
43960
  let later = null;
43913
43961
  if (req.screenshots !== void 0) {
@@ -44106,7 +44154,7 @@ async function prepareBaseTree(o) {
44106
44154
  throw err;
44107
44155
  }
44108
44156
  }
44109
- var ThRunError, OUTPUT_TAIL_BYTES, LOCKFILE_READ_CAP, FULL_COMMIT_ID, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, DEFERRED_GATE_TIMEOUT_MS, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA;
44157
+ var ThRunError, OUTPUT_TAIL_BYTES, LOCKFILE_READ_CAP, FULL_COMMIT_ID, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, DEFERRED_GATE_TIMEOUT_MS, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA, INSTALL_NEVER_STARTED;
44110
44158
  var init_thrun = __esm({
44111
44159
  "../../packages/envrun/dist/thrun.js"() {
44112
44160
  "use strict";
@@ -44195,6 +44243,7 @@ var init_thrun = __esm({
44195
44243
  FAILURE_LINE = /^(?:[ \t]*(?:not ok |FAILED |FAIL )|E {3}|# fail [1-9])/m;
44196
44244
  REDACTED_TARGET_REPO = "(refused before the target was accepted)";
44197
44245
  REDACTED_TARGET_SHA = "(refused)";
44246
+ INSTALL_NEVER_STARTED = 125;
44198
44247
  }
44199
44248
  });
44200
44249
 
@@ -45096,6 +45145,7 @@ __export(dist_exports, {
45096
45145
  imageRepo: () => imageRepo,
45097
45146
  imageVariantFor: () => imageVariantFor,
45098
45147
  installCommandFor: () => installCommandFor,
45148
+ installFrozenFor: () => installFrozenFor,
45099
45149
  installLocalMigrationTooling: () => installLocalMigrationTooling,
45100
45150
  installUnfrozenNote: () => installUnfrozenNote,
45101
45151
  isBookkeepingTable: () => isBookkeepingTable,
@@ -45492,6 +45542,7 @@ __export(jpi_claim_exports, {
45492
45542
  resolveBounty: () => resolveBounty,
45493
45543
  resolveDeliveryDir: () => resolveDeliveryDir,
45494
45544
  resolveSubmitWorktree: () => resolveSubmitWorktree,
45545
+ reviewModeLine: () => reviewModeLine,
45495
45546
  reviseRecoveryCommand: () => reviseRecoveryCommand,
45496
45547
  revokeFailureAction: () => revokeFailureAction,
45497
45548
  run: () => run7,
@@ -45959,9 +46010,15 @@ function extractClaimableFields(job) {
45959
46010
  // poster saw. Null when the index carries none.
45960
46011
  runRequirements: b.runRequirements && typeof b.runRequirements === "object" ? b.runRequirements : null,
45961
46012
  // TERM-1308. Present only when the index row carries them; no key otherwise.
45962
- ...b.bountySource === "founder" ? baselineFields(b) : {}
46013
+ ...b.bountySource === "founder" ? baselineFields(b) : {},
46014
+ // TERM-1344. The index carries `reviewMode` only for a poster-reviewed posting; any
46015
+ // other value is dropped rather than carried, the same as a malformed baseline field.
46016
+ ...b.bountySource === "founder" && b.reviewMode === POSTER_REVIEWED ? { reviewMode: POSTER_REVIEWED } : {}
45963
46017
  };
45964
46018
  }
46019
+ function reviewModeLine(b) {
46020
+ return b?.reviewMode === POSTER_REVIEWED ? CLAIMANT_NOTICE : null;
46021
+ }
45965
46022
  function parseGitHubUrl(url) {
45966
46023
  const m = String(url ?? "").match(/github\.com\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)/);
45967
46024
  if (!m) return null;
@@ -46567,7 +46624,7 @@ async function resolveBounty(arg) {
46567
46624
  if (freshPool) job = findByShortRefInPool(freshPool, arg);
46568
46625
  }
46569
46626
  if (job) {
46570
- let testBudgetMinutes, baselineStatus, baselineSha;
46627
+ let testBudgetMinutes, baselineStatus, baselineSha, reviewMode;
46571
46628
  ({
46572
46629
  bountyId,
46573
46630
  title,
@@ -46581,12 +46638,15 @@ async function resolveBounty(arg) {
46581
46638
  runRequirements,
46582
46639
  testBudgetMinutes,
46583
46640
  baselineStatus,
46584
- baselineSha
46641
+ baselineSha,
46642
+ reviewMode
46585
46643
  } = extractClaimableFields(job));
46586
46644
  baseline = {
46587
46645
  ...testBudgetMinutes === void 0 ? {} : { testBudgetMinutes },
46588
46646
  ...baselineStatus === void 0 ? {} : { baselineStatus },
46589
- ...baselineSha === void 0 ? {} : { baselineSha }
46647
+ ...baselineSha === void 0 ? {} : { baselineSha },
46648
+ // TERM-1344. Rides with the baseline fields: index-only, and no key when absent.
46649
+ ...reviewMode === void 0 ? {} : { reviewMode }
46590
46650
  };
46591
46651
  indexNativeId = bountyId;
46592
46652
  } else {
@@ -47346,6 +47406,8 @@ async function cmdPreview(arg, { json } = {}) {
47346
47406
  contested: isContested(b),
47347
47407
  // TERM-1308. The posting's test budget and baseline, only when the index carried them.
47348
47408
  ...baselineFields(b),
47409
+ // TERM-1344. Only for a poster-reviewed posting; absent means tests.
47410
+ ...b.reviewMode === POSTER_REVIEWED ? { reviewMode: POSTER_REVIEWED } : {},
47349
47411
  // Absent on a founder posting — see the scan above. A consumer must read
47350
47412
  // "no policy key" as "no policy step exists here", never as "clean".
47351
47413
  ...policy ? {
@@ -47376,8 +47438,8 @@ async function cmdPreview(arg, { json } = {}) {
47376
47438
  console.log(fmtOpenPRsLine(b));
47377
47439
  const previewContested = fmtContestedWarning(b);
47378
47440
  if (previewContested) console.log(previewContested);
47379
- const previewBaseline = baselineLine(b);
47380
- if (previewBaseline) console.log(` tests: ${previewBaseline}`);
47441
+ const previewTests = reviewModeLine(b) ?? baselineLine(b);
47442
+ if (previewTests) console.log(` tests: ${previewTests}`);
47381
47443
  if (policy) printPolicySection(policy);
47382
47444
  if (process.stdout.isTTY) {
47383
47445
  try {
@@ -48633,7 +48695,8 @@ function renderRunView(run32, message2, extra = {}) {
48633
48695
  const rateLimitCap = extra.rateLimitCap ?? run32.rateLimitCap ?? 5;
48634
48696
  const rateLimitCount = extra.rateLimitCount ?? run32.rateLimitCount;
48635
48697
  if (claimState) {
48636
- lines.push(` state: ${terminalSafeInline(claimState)}`);
48698
+ const stateText = claimState === "poster-review" ? HISTORY_LINE : terminalSafeInline(claimState);
48699
+ lines.push(` state: ${stateText}`);
48637
48700
  }
48638
48701
  if (unverifiedCause) {
48639
48702
  lines.push(` cause: ${terminalSafeInline(unverifiedCause)}`);
@@ -48679,7 +48742,8 @@ function renderClaimHistory(attempts, timeline) {
48679
48742
  for (const a of runs) {
48680
48743
  const sha = a.commitSha ? String(a.commitSha).slice(0, 10) : "";
48681
48744
  const jobs = Array.isArray(a.failingJobs) && a.failingJobs.length > 0 ? ` \u2014 ${a.failingJobs.map(terminalSafeInline).join(", ")}` : "";
48682
- const outcome = a.conclusion ? `checks ${terminalSafeInline(a.conclusion)}${jobs}` : a.status === "no-ci" ? "no checks ran" : "checks running";
48745
+ const note = typeof a.note === "string" ? terminalSafeInline(a.note).trim() : "";
48746
+ const outcome = a.conclusion ? `checks ${terminalSafeInline(a.conclusion)}${jobs}` : note !== "" ? note : a.status === "no-ci" ? "no checks ran" : "checks running";
48683
48747
  lines.push(` attempt ${a.attemptNumber} ${sha} ${outcome} ${shortWhen(a.at)}`);
48684
48748
  }
48685
48749
  for (const e of told) {
@@ -49429,6 +49493,57 @@ async function uploadClaimScreenshots(intake, uploads) {
49429
49493
  );
49430
49494
  }
49431
49495
  }
49496
+ async function packageJsonAt(wt, rev, path6) {
49497
+ let text;
49498
+ try {
49499
+ ({ stdout: text } = await pExecFile("git", ["-C", wt, "show", `${rev}:${path6}`], {
49500
+ shell: false,
49501
+ maxBuffer: 16 * 1024 * 1024
49502
+ }));
49503
+ } catch {
49504
+ return {};
49505
+ }
49506
+ try {
49507
+ const parsed = JSON.parse(String(text));
49508
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
49509
+ } catch {
49510
+ return null;
49511
+ }
49512
+ }
49513
+ function sectionKey(pkg, section2) {
49514
+ const value = pkg[section2];
49515
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
49516
+ return JSON.stringify(value ?? null);
49517
+ }
49518
+ return JSON.stringify(Object.keys(value).sort().map((k) => [k, value[k]]));
49519
+ }
49520
+ async function patchChangesDependencies(wt, base, touched) {
49521
+ for (const path6 of touched) {
49522
+ if (path6.split("/").pop() !== "package.json") continue;
49523
+ const before = await packageJsonAt(wt, base, path6);
49524
+ const after = await packageJsonAt(wt, "HEAD", path6);
49525
+ if (before === null || after === null) continue;
49526
+ if (DEPENDENCY_SECTIONS2.some((s) => sectionKey(before, s) !== sectionKey(after, s))) return true;
49527
+ }
49528
+ return false;
49529
+ }
49530
+ async function dependencyLockfileWarnings(wt, base, touched) {
49531
+ if (!await patchChangesDependencies(wt, base, touched)) return [];
49532
+ const warnings = [];
49533
+ for (const [lockfile, cmd] of SENDABLE_LOCKFILES) {
49534
+ let present;
49535
+ try {
49536
+ present = (await sh("git", ["-C", wt, "ls-tree", "--name-only", base, "--", lockfile])).trim() === lockfile;
49537
+ } catch {
49538
+ present = false;
49539
+ }
49540
+ if (!present || touched.includes(lockfile)) continue;
49541
+ warnings.push(
49542
+ ` \u26A0 package.json changes dependencies, but ${lockfile} is not in this patch. The run will install without its frozen-lockfile check. Run \`${cmd}\` and commit ${lockfile} to keep it frozen.`
49543
+ );
49544
+ }
49545
+ return warnings;
49546
+ }
49432
49547
  async function submitFounderPatch({ claims, claim, id, wt, flags }) {
49433
49548
  let roots;
49434
49549
  try {
@@ -49479,6 +49594,9 @@ async function submitFounderPatch({ claims, claim, id, wt, flags }) {
49479
49594
  );
49480
49595
  process.exit(1);
49481
49596
  }
49597
+ const lockfileWarnings = await dependencyLockfileWarnings(wt, base, touched);
49598
+ if (lockfileWarnings.length > 0) console.log(`
49599
+ ${lockfileWarnings.join("\n")}`);
49482
49600
  const authorName = await sh("git", ["-C", wt, "log", "-1", "--format=%an"]);
49483
49601
  const authorEmail = await sh("git", ["-C", wt, "log", "-1", "--format=%ae"]);
49484
49602
  const shots = flags.screenshots === void 0 ? null : loadClaimScreenshots(flags.screenshots);
@@ -50988,7 +51106,7 @@ async function run7() {
50988
51106
  process.exit(1);
50989
51107
  }
50990
51108
  }
50991
- 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, BASELINE_STATUSES, MIN_TEST_BUDGET_MINUTES, MAX_TEST_BUDGET_MINUTES, 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_SCREENSHOT_PUT_TIMEOUT_MS, CLAIM_RESOLUTION_REASONS, SETUP_FAILED_REQUIREMENTS, POSTING_LEVEL_RESOLUTION_REASONS, RESOLUTION_REASON_BLURB;
51109
+ 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, BASELINE_STATUSES, MIN_TEST_BUDGET_MINUTES, MAX_TEST_BUDGET_MINUTES, 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_SCREENSHOT_PUT_TIMEOUT_MS, DEPENDENCY_SECTIONS2, SENDABLE_LOCKFILES, CLAIM_RESOLUTION_REASONS, SETUP_FAILED_REQUIREMENTS, POSTING_LEVEL_RESOLUTION_REASONS, RESOLUTION_REASON_BLURB;
50992
51110
  var init_jpi_claim = __esm({
50993
51111
  "bin/jpi-claim.js"() {
50994
51112
  "use strict";
@@ -51004,6 +51122,7 @@ var init_jpi_claim = __esm({
51004
51122
  init_claim_push_bg();
51005
51123
  init_founder_verdict_sync();
51006
51124
  init_founder_note_sync();
51125
+ init_review_mode_copy();
51007
51126
  TERMINALHIRE_DIR17 = process.env.TERMINALHIRE_DIR || join46(homedir26(), ".terminalhire");
51008
51127
  INDEX_CACHE_FILE5 = join46(TERMINALHIRE_DIR17, "index-cache.json");
51009
51128
  CLAIM_PUSH_MARKER = join46(TERMINALHIRE_DIR17, "claim-push.json");
@@ -51121,6 +51240,16 @@ var init_jpi_claim = __esm({
51121
51240
  CONTROL_CHARS3 = /[\u0000-\u001F\u007F-\u009F]/g;
51122
51241
  FOUNDER_POSTING_ID = /^fb_[A-Za-z0-9_-]{1,80}$/;
51123
51242
  CLAIM_SCREENSHOT_PUT_TIMEOUT_MS = 2e4;
51243
+ DEPENDENCY_SECTIONS2 = [
51244
+ "dependencies",
51245
+ "devDependencies",
51246
+ "optionalDependencies",
51247
+ "peerDependencies"
51248
+ ];
51249
+ SENDABLE_LOCKFILES = [
51250
+ ["package-lock.json", "npm install"],
51251
+ ["yarn.lock", "yarn install"]
51252
+ ];
51124
51253
  CLAIM_RESOLUTION_REASONS = [
51125
51254
  "not-my-stack",
51126
51255
  "out-of-time",
@@ -12169,6 +12169,17 @@ var init_founder_note_sync = __esm({
12169
12169
  }
12170
12170
  });
12171
12171
 
12172
+ // bin/review-mode-copy.js
12173
+ var POSTER_REVIEWED, CLAIMANT_NOTICE, HISTORY_LINE;
12174
+ var init_review_mode_copy = __esm({
12175
+ "bin/review-mode-copy.js"() {
12176
+ "use strict";
12177
+ POSTER_REVIEWED = "poster";
12178
+ CLAIMANT_NOTICE = "The poster reviews this work by hand. We don't run tests on it.";
12179
+ HISTORY_LINE = "Submitted for the poster's review.";
12180
+ }
12181
+ });
12182
+
12172
12183
  // ../../packages/core/src/policy-audit.ts
12173
12184
  function fence(nonce, body) {
12174
12185
  return `<<<UNTRUSTED-${nonce}
@@ -31590,7 +31601,8 @@ var init_result = __esm({
31590
31601
  "leakState",
31591
31602
  "venue",
31592
31603
  "refusalOrigin",
31593
- "stoppedAt"
31604
+ "stoppedAt",
31605
+ "installFrozen"
31594
31606
  ];
31595
31607
  RENDER_NONE = null;
31596
31608
  FIELD_VIEWS = {
@@ -31655,7 +31667,10 @@ var init_result = __esm({
31655
31667
  // side refused in words a developer reads.
31656
31668
  refusalOrigin: RENDER_NONE,
31657
31669
  // Only when the run did not pass: on a green run "the last step" is just the last step.
31658
- stoppedAt: (r) => r.stoppedAt === null || r.outcome === "completed" ? null : `last step ${r.stoppedAt} (the last package script named in the output)`
31670
+ stoppedAt: (r) => r.stoppedAt === null || r.outcome === "completed" ? null : `last step ${r.stoppedAt} (the last package script named in the output)`,
31671
+ // The run already prints the unfrozen note while it runs (`installUnfrozenNote`); this is
31672
+ // the record of it for the worker and the intake.
31673
+ installFrozen: RENDER_NONE
31659
31674
  };
31660
31675
  }
31661
31676
  });
@@ -35693,6 +35708,10 @@ function nodeInstallCommand(repo) {
35693
35708
  return null;
35694
35709
  }
35695
35710
  function lockfileGaps(repo) {
35711
+ const read = readLockfileGaps(repo);
35712
+ return read === null || read.missing.length === 0 ? null : { lockfile: read.lockfile, missing: read.missing };
35713
+ }
35714
+ function readLockfileGaps(repo) {
35696
35715
  const pkg = readJsonObject(repo, "package.json");
35697
35716
  if (pkg === null)
35698
35717
  return null;
@@ -35719,7 +35738,7 @@ function lockfileGaps(repo) {
35719
35738
  missing.push(`${n}@${r} (${dir})`);
35720
35739
  }
35721
35740
  }
35722
- return missing.length === 0 ? null : { lockfile, missing };
35741
+ return { lockfile, missing, rootRead: rootListing !== null };
35723
35742
  }
35724
35743
  function wantedDependencies(pkg) {
35725
35744
  const wanted = [];
@@ -38220,7 +38239,9 @@ function refusedRun(fields) {
38220
38239
  venue: null,
38221
38240
  refusalOrigin: fields.origin,
38222
38241
  // No test step ran, so there is no output to read a step from.
38223
- stoppedAt: null
38242
+ stoppedAt: null,
38243
+ // Refused before any install step ran.
38244
+ installFrozen: null
38224
38245
  };
38225
38246
  }
38226
38247
  function refusalOriginOf(refusal2) {
@@ -38304,7 +38325,29 @@ function installUnfrozenNote(spec) {
38304
38325
  const u = spec.installUnfrozen;
38305
38326
  if (u === void 0)
38306
38327
  return null;
38307
- return `${u.lockfile} does not list ${u.missing.join(", ")}, so the install ran as \`${String(spec.installCommand)}\` without its frozen-lockfile check. Regenerate ${u.lockfile} before merging.`;
38328
+ return `${u.lockfile} does not list ${u.missing.join(", ")}, so the install ran as \`${String(spec.installCommand)}\` without its frozen-lockfile check. Include the updated ${u.lockfile} in the patch, or regenerate it before merging.`;
38329
+ }
38330
+ function isFrozenInstallCommand(command) {
38331
+ const lead = /^(?:(?:sudo|time)\s+|env(?:\s+[A-Za-z_]\w*=\S*)*\s+)*/.exec(command)?.[0] ?? "";
38332
+ const install = command.slice(lead.length).split(/;|&&|\|\|/)[0]?.trim() ?? "";
38333
+ if (/^npm ci(\s|$)/.test(install))
38334
+ return true;
38335
+ if (/^(yarn|pnpm|bun)\b/.test(install) && /\s--frozen-lockfile(?=\s|$)/.test(install)) {
38336
+ return true;
38337
+ }
38338
+ return /^yarn\b/.test(install) && /\s--immutable(?=\s|$)/.test(install);
38339
+ }
38340
+ function installFrozenFor(spec, install) {
38341
+ if (install === null)
38342
+ return null;
38343
+ const launched = install.exitCode !== INSTALL_NEVER_STARTED && (install.exitCode !== null || install.timedOut);
38344
+ if (!launched)
38345
+ return null;
38346
+ if (spec.runtime !== "node")
38347
+ return null;
38348
+ if (spec.installUnfrozen !== void 0)
38349
+ return false;
38350
+ return isFrozenInstallCommand(install.command);
38308
38351
  }
38309
38352
  async function verifyWorkingDiff(req) {
38310
38353
  const ctx = {
@@ -38673,7 +38716,8 @@ async function runVerification(req, ctx) {
38673
38716
  // A run that reached a verdict was not refused, so neither side refused it.
38674
38717
  refusalOrigin: null,
38675
38718
  // Read from the full test output, not `outputTail`, which is an excerpt.
38676
- stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? "")
38719
+ stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? ""),
38720
+ installFrozen: installFrozenFor(spec, verdict.install)
38677
38721
  };
38678
38722
  let later = null;
38679
38723
  if (req.screenshots !== void 0) {
@@ -38872,7 +38916,7 @@ async function prepareBaseTree(o) {
38872
38916
  throw err;
38873
38917
  }
38874
38918
  }
38875
- var ThRunError, OUTPUT_TAIL_BYTES, LOCKFILE_READ_CAP, FULL_COMMIT_ID, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, DEFERRED_GATE_TIMEOUT_MS, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA;
38919
+ var ThRunError, OUTPUT_TAIL_BYTES, LOCKFILE_READ_CAP, FULL_COMMIT_ID, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, DEFERRED_GATE_TIMEOUT_MS, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA, INSTALL_NEVER_STARTED;
38876
38920
  var init_thrun = __esm({
38877
38921
  "../../packages/envrun/dist/thrun.js"() {
38878
38922
  "use strict";
@@ -38961,6 +39005,7 @@ var init_thrun = __esm({
38961
39005
  FAILURE_LINE = /^(?:[ \t]*(?:not ok |FAILED |FAIL )|E {3}|# fail [1-9])/m;
38962
39006
  REDACTED_TARGET_REPO = "(refused before the target was accepted)";
38963
39007
  REDACTED_TARGET_SHA = "(refused)";
39008
+ INSTALL_NEVER_STARTED = 125;
38964
39009
  }
38965
39010
  });
38966
39011
 
@@ -39862,6 +39907,7 @@ __export(dist_exports, {
39862
39907
  imageRepo: () => imageRepo,
39863
39908
  imageVariantFor: () => imageVariantFor,
39864
39909
  installCommandFor: () => installCommandFor,
39910
+ installFrozenFor: () => installFrozenFor,
39865
39911
  installLocalMigrationTooling: () => installLocalMigrationTooling,
39866
39912
  installUnfrozenNote: () => installUnfrozenNote,
39867
39913
  isBookkeepingTable: () => isBookkeepingTable,
@@ -40258,6 +40304,7 @@ __export(jpi_claim_exports, {
40258
40304
  resolveBounty: () => resolveBounty,
40259
40305
  resolveDeliveryDir: () => resolveDeliveryDir,
40260
40306
  resolveSubmitWorktree: () => resolveSubmitWorktree,
40307
+ reviewModeLine: () => reviewModeLine,
40261
40308
  reviseRecoveryCommand: () => reviseRecoveryCommand,
40262
40309
  revokeFailureAction: () => revokeFailureAction,
40263
40310
  run: () => run,
@@ -40725,9 +40772,15 @@ function extractClaimableFields(job) {
40725
40772
  // poster saw. Null when the index carries none.
40726
40773
  runRequirements: b.runRequirements && typeof b.runRequirements === "object" ? b.runRequirements : null,
40727
40774
  // TERM-1308. Present only when the index row carries them; no key otherwise.
40728
- ...b.bountySource === "founder" ? baselineFields(b) : {}
40775
+ ...b.bountySource === "founder" ? baselineFields(b) : {},
40776
+ // TERM-1344. The index carries `reviewMode` only for a poster-reviewed posting; any
40777
+ // other value is dropped rather than carried, the same as a malformed baseline field.
40778
+ ...b.bountySource === "founder" && b.reviewMode === POSTER_REVIEWED ? { reviewMode: POSTER_REVIEWED } : {}
40729
40779
  };
40730
40780
  }
40781
+ function reviewModeLine(b) {
40782
+ return b?.reviewMode === POSTER_REVIEWED ? CLAIMANT_NOTICE : null;
40783
+ }
40731
40784
  function parseGitHubUrl(url) {
40732
40785
  const m = String(url ?? "").match(/github\.com\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)/);
40733
40786
  if (!m) return null;
@@ -41333,7 +41386,7 @@ async function resolveBounty(arg) {
41333
41386
  if (freshPool) job = findByShortRefInPool(freshPool, arg);
41334
41387
  }
41335
41388
  if (job) {
41336
- let testBudgetMinutes, baselineStatus, baselineSha;
41389
+ let testBudgetMinutes, baselineStatus, baselineSha, reviewMode;
41337
41390
  ({
41338
41391
  bountyId,
41339
41392
  title,
@@ -41347,12 +41400,15 @@ async function resolveBounty(arg) {
41347
41400
  runRequirements,
41348
41401
  testBudgetMinutes,
41349
41402
  baselineStatus,
41350
- baselineSha
41403
+ baselineSha,
41404
+ reviewMode
41351
41405
  } = extractClaimableFields(job));
41352
41406
  baseline = {
41353
41407
  ...testBudgetMinutes === void 0 ? {} : { testBudgetMinutes },
41354
41408
  ...baselineStatus === void 0 ? {} : { baselineStatus },
41355
- ...baselineSha === void 0 ? {} : { baselineSha }
41409
+ ...baselineSha === void 0 ? {} : { baselineSha },
41410
+ // TERM-1344. Rides with the baseline fields: index-only, and no key when absent.
41411
+ ...reviewMode === void 0 ? {} : { reviewMode }
41356
41412
  };
41357
41413
  indexNativeId = bountyId;
41358
41414
  } else {
@@ -42112,6 +42168,8 @@ async function cmdPreview(arg, { json } = {}) {
42112
42168
  contested: isContested(b),
42113
42169
  // TERM-1308. The posting's test budget and baseline, only when the index carried them.
42114
42170
  ...baselineFields(b),
42171
+ // TERM-1344. Only for a poster-reviewed posting; absent means tests.
42172
+ ...b.reviewMode === POSTER_REVIEWED ? { reviewMode: POSTER_REVIEWED } : {},
42115
42173
  // Absent on a founder posting — see the scan above. A consumer must read
42116
42174
  // "no policy key" as "no policy step exists here", never as "clean".
42117
42175
  ...policy ? {
@@ -42142,8 +42200,8 @@ async function cmdPreview(arg, { json } = {}) {
42142
42200
  console.log(fmtOpenPRsLine(b));
42143
42201
  const previewContested = fmtContestedWarning(b);
42144
42202
  if (previewContested) console.log(previewContested);
42145
- const previewBaseline = baselineLine(b);
42146
- if (previewBaseline) console.log(` tests: ${previewBaseline}`);
42203
+ const previewTests = reviewModeLine(b) ?? baselineLine(b);
42204
+ if (previewTests) console.log(` tests: ${previewTests}`);
42147
42205
  if (policy) printPolicySection(policy);
42148
42206
  if (process.stdout.isTTY) {
42149
42207
  try {
@@ -43399,7 +43457,8 @@ function renderRunView(run3, message2, extra = {}) {
43399
43457
  const rateLimitCap = extra.rateLimitCap ?? run3.rateLimitCap ?? 5;
43400
43458
  const rateLimitCount = extra.rateLimitCount ?? run3.rateLimitCount;
43401
43459
  if (claimState) {
43402
- lines.push(` state: ${terminalSafeInline(claimState)}`);
43460
+ const stateText = claimState === "poster-review" ? HISTORY_LINE : terminalSafeInline(claimState);
43461
+ lines.push(` state: ${stateText}`);
43403
43462
  }
43404
43463
  if (unverifiedCause) {
43405
43464
  lines.push(` cause: ${terminalSafeInline(unverifiedCause)}`);
@@ -43445,7 +43504,8 @@ function renderClaimHistory(attempts, timeline) {
43445
43504
  for (const a of runs) {
43446
43505
  const sha = a.commitSha ? String(a.commitSha).slice(0, 10) : "";
43447
43506
  const jobs = Array.isArray(a.failingJobs) && a.failingJobs.length > 0 ? ` \u2014 ${a.failingJobs.map(terminalSafeInline).join(", ")}` : "";
43448
- const outcome = a.conclusion ? `checks ${terminalSafeInline(a.conclusion)}${jobs}` : a.status === "no-ci" ? "no checks ran" : "checks running";
43507
+ const note = typeof a.note === "string" ? terminalSafeInline(a.note).trim() : "";
43508
+ const outcome = a.conclusion ? `checks ${terminalSafeInline(a.conclusion)}${jobs}` : note !== "" ? note : a.status === "no-ci" ? "no checks ran" : "checks running";
43449
43509
  lines.push(` attempt ${a.attemptNumber} ${sha} ${outcome} ${shortWhen(a.at)}`);
43450
43510
  }
43451
43511
  for (const e of told) {
@@ -44195,6 +44255,57 @@ async function uploadClaimScreenshots(intake, uploads) {
44195
44255
  );
44196
44256
  }
44197
44257
  }
44258
+ async function packageJsonAt(wt, rev, path5) {
44259
+ let text;
44260
+ try {
44261
+ ({ stdout: text } = await pExecFile("git", ["-C", wt, "show", `${rev}:${path5}`], {
44262
+ shell: false,
44263
+ maxBuffer: 16 * 1024 * 1024
44264
+ }));
44265
+ } catch {
44266
+ return {};
44267
+ }
44268
+ try {
44269
+ const parsed = JSON.parse(String(text));
44270
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
44271
+ } catch {
44272
+ return null;
44273
+ }
44274
+ }
44275
+ function sectionKey(pkg, section) {
44276
+ const value = pkg[section];
44277
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
44278
+ return JSON.stringify(value ?? null);
44279
+ }
44280
+ return JSON.stringify(Object.keys(value).sort().map((k) => [k, value[k]]));
44281
+ }
44282
+ async function patchChangesDependencies(wt, base, touched) {
44283
+ for (const path5 of touched) {
44284
+ if (path5.split("/").pop() !== "package.json") continue;
44285
+ const before = await packageJsonAt(wt, base, path5);
44286
+ const after = await packageJsonAt(wt, "HEAD", path5);
44287
+ if (before === null || after === null) continue;
44288
+ if (DEPENDENCY_SECTIONS2.some((s) => sectionKey(before, s) !== sectionKey(after, s))) return true;
44289
+ }
44290
+ return false;
44291
+ }
44292
+ async function dependencyLockfileWarnings(wt, base, touched) {
44293
+ if (!await patchChangesDependencies(wt, base, touched)) return [];
44294
+ const warnings = [];
44295
+ for (const [lockfile, cmd] of SENDABLE_LOCKFILES) {
44296
+ let present;
44297
+ try {
44298
+ present = (await sh("git", ["-C", wt, "ls-tree", "--name-only", base, "--", lockfile])).trim() === lockfile;
44299
+ } catch {
44300
+ present = false;
44301
+ }
44302
+ if (!present || touched.includes(lockfile)) continue;
44303
+ warnings.push(
44304
+ ` \u26A0 package.json changes dependencies, but ${lockfile} is not in this patch. The run will install without its frozen-lockfile check. Run \`${cmd}\` and commit ${lockfile} to keep it frozen.`
44305
+ );
44306
+ }
44307
+ return warnings;
44308
+ }
44198
44309
  async function submitFounderPatch({ claims, claim, id, wt, flags }) {
44199
44310
  let roots;
44200
44311
  try {
@@ -44245,6 +44356,9 @@ async function submitFounderPatch({ claims, claim, id, wt, flags }) {
44245
44356
  );
44246
44357
  process.exit(1);
44247
44358
  }
44359
+ const lockfileWarnings = await dependencyLockfileWarnings(wt, base, touched);
44360
+ if (lockfileWarnings.length > 0) console.log(`
44361
+ ${lockfileWarnings.join("\n")}`);
44248
44362
  const authorName = await sh("git", ["-C", wt, "log", "-1", "--format=%an"]);
44249
44363
  const authorEmail = await sh("git", ["-C", wt, "log", "-1", "--format=%ae"]);
44250
44364
  const shots = flags.screenshots === void 0 ? null : loadClaimScreenshots(flags.screenshots);
@@ -45754,7 +45868,7 @@ async function run() {
45754
45868
  process.exit(1);
45755
45869
  }
45756
45870
  }
45757
- 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, BASELINE_STATUSES, MIN_TEST_BUDGET_MINUTES, MAX_TEST_BUDGET_MINUTES, 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_SCREENSHOT_PUT_TIMEOUT_MS, CLAIM_RESOLUTION_REASONS, SETUP_FAILED_REQUIREMENTS, POSTING_LEVEL_RESOLUTION_REASONS, RESOLUTION_REASON_BLURB;
45871
+ 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, BASELINE_STATUSES, MIN_TEST_BUDGET_MINUTES, MAX_TEST_BUDGET_MINUTES, 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_SCREENSHOT_PUT_TIMEOUT_MS, DEPENDENCY_SECTIONS2, SENDABLE_LOCKFILES, CLAIM_RESOLUTION_REASONS, SETUP_FAILED_REQUIREMENTS, POSTING_LEVEL_RESOLUTION_REASONS, RESOLUTION_REASON_BLURB;
45758
45872
  var init_jpi_claim = __esm({
45759
45873
  "bin/jpi-claim.js"() {
45760
45874
  "use strict";
@@ -45770,6 +45884,7 @@ var init_jpi_claim = __esm({
45770
45884
  init_claim_push_bg();
45771
45885
  init_founder_verdict_sync();
45772
45886
  init_founder_note_sync();
45887
+ init_review_mode_copy();
45773
45888
  TERMINALHIRE_DIR11 = process.env.TERMINALHIRE_DIR || join32(homedir15(), ".terminalhire");
45774
45889
  INDEX_CACHE_FILE2 = join32(TERMINALHIRE_DIR11, "index-cache.json");
45775
45890
  CLAIM_PUSH_MARKER = join32(TERMINALHIRE_DIR11, "claim-push.json");
@@ -45887,6 +46002,16 @@ var init_jpi_claim = __esm({
45887
46002
  CONTROL_CHARS3 = /[\u0000-\u001F\u007F-\u009F]/g;
45888
46003
  FOUNDER_POSTING_ID = /^fb_[A-Za-z0-9_-]{1,80}$/;
45889
46004
  CLAIM_SCREENSHOT_PUT_TIMEOUT_MS = 2e4;
46005
+ DEPENDENCY_SECTIONS2 = [
46006
+ "dependencies",
46007
+ "devDependencies",
46008
+ "optionalDependencies",
46009
+ "peerDependencies"
46010
+ ];
46011
+ SENDABLE_LOCKFILES = [
46012
+ ["package-lock.json", "npm install"],
46013
+ ["yarn.lock", "yarn install"]
46014
+ ];
45890
46015
  CLAIM_RESOLUTION_REASONS = [
45891
46016
  "not-my-stack",
45892
46017
  "out-of-time",
@@ -5424,7 +5424,8 @@ var init_result = __esm({
5424
5424
  "leakState",
5425
5425
  "venue",
5426
5426
  "refusalOrigin",
5427
- "stoppedAt"
5427
+ "stoppedAt",
5428
+ "installFrozen"
5428
5429
  ];
5429
5430
  RENDER_NONE = null;
5430
5431
  FIELD_VIEWS = {
@@ -5489,7 +5490,10 @@ var init_result = __esm({
5489
5490
  // side refused in words a developer reads.
5490
5491
  refusalOrigin: RENDER_NONE,
5491
5492
  // Only when the run did not pass: on a green run "the last step" is just the last step.
5492
- stoppedAt: (r) => r.stoppedAt === null || r.outcome === "completed" ? null : `last step ${r.stoppedAt} (the last package script named in the output)`
5493
+ stoppedAt: (r) => r.stoppedAt === null || r.outcome === "completed" ? null : `last step ${r.stoppedAt} (the last package script named in the output)`,
5494
+ // The run already prints the unfrozen note while it runs (`installUnfrozenNote`); this is
5495
+ // the record of it for the worker and the intake.
5496
+ installFrozen: RENDER_NONE
5493
5497
  };
5494
5498
  }
5495
5499
  });
@@ -9527,6 +9531,10 @@ function nodeInstallCommand(repo) {
9527
9531
  return null;
9528
9532
  }
9529
9533
  function lockfileGaps(repo) {
9534
+ const read = readLockfileGaps(repo);
9535
+ return read === null || read.missing.length === 0 ? null : { lockfile: read.lockfile, missing: read.missing };
9536
+ }
9537
+ function readLockfileGaps(repo) {
9530
9538
  const pkg = readJsonObject(repo, "package.json");
9531
9539
  if (pkg === null)
9532
9540
  return null;
@@ -9553,7 +9561,7 @@ function lockfileGaps(repo) {
9553
9561
  missing.push(`${n}@${r} (${dir})`);
9554
9562
  }
9555
9563
  }
9556
- return missing.length === 0 ? null : { lockfile, missing };
9564
+ return { lockfile, missing, rootRead: rootListing !== null };
9557
9565
  }
9558
9566
  function wantedDependencies(pkg) {
9559
9567
  const wanted = [];
@@ -12229,7 +12237,9 @@ function refusedRun(fields) {
12229
12237
  venue: null,
12230
12238
  refusalOrigin: fields.origin,
12231
12239
  // No test step ran, so there is no output to read a step from.
12232
- stoppedAt: null
12240
+ stoppedAt: null,
12241
+ // Refused before any install step ran.
12242
+ installFrozen: null
12233
12243
  };
12234
12244
  }
12235
12245
  function refusalOriginOf(refusal2) {
@@ -12313,7 +12323,29 @@ function installUnfrozenNote(spec) {
12313
12323
  const u = spec.installUnfrozen;
12314
12324
  if (u === void 0)
12315
12325
  return null;
12316
- return `${u.lockfile} does not list ${u.missing.join(", ")}, so the install ran as \`${String(spec.installCommand)}\` without its frozen-lockfile check. Regenerate ${u.lockfile} before merging.`;
12326
+ return `${u.lockfile} does not list ${u.missing.join(", ")}, so the install ran as \`${String(spec.installCommand)}\` without its frozen-lockfile check. Include the updated ${u.lockfile} in the patch, or regenerate it before merging.`;
12327
+ }
12328
+ function isFrozenInstallCommand(command) {
12329
+ const lead = /^(?:(?:sudo|time)\s+|env(?:\s+[A-Za-z_]\w*=\S*)*\s+)*/.exec(command)?.[0] ?? "";
12330
+ const install = command.slice(lead.length).split(/;|&&|\|\|/)[0]?.trim() ?? "";
12331
+ if (/^npm ci(\s|$)/.test(install))
12332
+ return true;
12333
+ if (/^(yarn|pnpm|bun)\b/.test(install) && /\s--frozen-lockfile(?=\s|$)/.test(install)) {
12334
+ return true;
12335
+ }
12336
+ return /^yarn\b/.test(install) && /\s--immutable(?=\s|$)/.test(install);
12337
+ }
12338
+ function installFrozenFor(spec, install) {
12339
+ if (install === null)
12340
+ return null;
12341
+ const launched = install.exitCode !== INSTALL_NEVER_STARTED && (install.exitCode !== null || install.timedOut);
12342
+ if (!launched)
12343
+ return null;
12344
+ if (spec.runtime !== "node")
12345
+ return null;
12346
+ if (spec.installUnfrozen !== void 0)
12347
+ return false;
12348
+ return isFrozenInstallCommand(install.command);
12317
12349
  }
12318
12350
  async function verifyWorkingDiff(req) {
12319
12351
  const ctx = {
@@ -12682,7 +12714,8 @@ async function runVerification(req, ctx) {
12682
12714
  // A run that reached a verdict was not refused, so neither side refused it.
12683
12715
  refusalOrigin: null,
12684
12716
  // Read from the full test output, not `outputTail`, which is an excerpt.
12685
- stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? "")
12717
+ stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? ""),
12718
+ installFrozen: installFrozenFor(spec, verdict.install)
12686
12719
  };
12687
12720
  let later = null;
12688
12721
  if (req.screenshots !== void 0) {
@@ -12881,7 +12914,7 @@ async function prepareBaseTree(o) {
12881
12914
  throw err;
12882
12915
  }
12883
12916
  }
12884
- var ThRunError, OUTPUT_TAIL_BYTES, LOCKFILE_READ_CAP, FULL_COMMIT_ID, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, DEFERRED_GATE_TIMEOUT_MS, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA;
12917
+ var ThRunError, OUTPUT_TAIL_BYTES, LOCKFILE_READ_CAP, FULL_COMMIT_ID, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, DEFERRED_GATE_TIMEOUT_MS, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA, INSTALL_NEVER_STARTED;
12885
12918
  var init_thrun = __esm({
12886
12919
  "../../packages/envrun/dist/thrun.js"() {
12887
12920
  "use strict";
@@ -12970,6 +13003,7 @@ var init_thrun = __esm({
12970
13003
  FAILURE_LINE = /^(?:[ \t]*(?:not ok |FAILED |FAIL )|E {3}|# fail [1-9])/m;
12971
13004
  REDACTED_TARGET_REPO = "(refused before the target was accepted)";
12972
13005
  REDACTED_TARGET_SHA = "(refused)";
13006
+ INSTALL_NEVER_STARTED = 125;
12973
13007
  }
12974
13008
  });
12975
13009
 
@@ -13871,6 +13905,7 @@ __export(dist_exports, {
13871
13905
  imageRepo: () => imageRepo,
13872
13906
  imageVariantFor: () => imageVariantFor,
13873
13907
  installCommandFor: () => installCommandFor,
13908
+ installFrozenFor: () => installFrozenFor,
13874
13909
  installLocalMigrationTooling: () => installLocalMigrationTooling,
13875
13910
  installUnfrozenNote: () => installUnfrozenNote,
13876
13911
  isBookkeepingTable: () => isBookkeepingTable,
@@ -25318,6 +25353,17 @@ var init_founder_note_sync = __esm({
25318
25353
  }
25319
25354
  });
25320
25355
 
25356
+ // bin/review-mode-copy.js
25357
+ var POSTER_REVIEWED, CLAIMANT_NOTICE, HISTORY_LINE;
25358
+ var init_review_mode_copy = __esm({
25359
+ "bin/review-mode-copy.js"() {
25360
+ "use strict";
25361
+ POSTER_REVIEWED = "poster";
25362
+ CLAIMANT_NOTICE = "The poster reviews this work by hand. We don't run tests on it.";
25363
+ HISTORY_LINE = "Submitted for the poster's review.";
25364
+ }
25365
+ });
25366
+
25321
25367
  // ../../packages/core/src/policy-audit.ts
25322
25368
  function fence(nonce, body) {
25323
25369
  return `<<<UNTRUSTED-${nonce}
@@ -40251,6 +40297,7 @@ __export(jpi_claim_exports, {
40251
40297
  resolveBounty: () => resolveBounty,
40252
40298
  resolveDeliveryDir: () => resolveDeliveryDir,
40253
40299
  resolveSubmitWorktree: () => resolveSubmitWorktree,
40300
+ reviewModeLine: () => reviewModeLine,
40254
40301
  reviseRecoveryCommand: () => reviseRecoveryCommand,
40255
40302
  revokeFailureAction: () => revokeFailureAction,
40256
40303
  run: () => run,
@@ -40718,9 +40765,15 @@ function extractClaimableFields(job) {
40718
40765
  // poster saw. Null when the index carries none.
40719
40766
  runRequirements: b.runRequirements && typeof b.runRequirements === "object" ? b.runRequirements : null,
40720
40767
  // TERM-1308. Present only when the index row carries them; no key otherwise.
40721
- ...b.bountySource === "founder" ? baselineFields(b) : {}
40768
+ ...b.bountySource === "founder" ? baselineFields(b) : {},
40769
+ // TERM-1344. The index carries `reviewMode` only for a poster-reviewed posting; any
40770
+ // other value is dropped rather than carried, the same as a malformed baseline field.
40771
+ ...b.bountySource === "founder" && b.reviewMode === POSTER_REVIEWED ? { reviewMode: POSTER_REVIEWED } : {}
40722
40772
  };
40723
40773
  }
40774
+ function reviewModeLine(b) {
40775
+ return b?.reviewMode === POSTER_REVIEWED ? CLAIMANT_NOTICE : null;
40776
+ }
40724
40777
  function parseGitHubUrl(url) {
40725
40778
  const m = String(url ?? "").match(/github\.com\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)/);
40726
40779
  if (!m) return null;
@@ -41326,7 +41379,7 @@ async function resolveBounty(arg) {
41326
41379
  if (freshPool) job = findByShortRefInPool(freshPool, arg);
41327
41380
  }
41328
41381
  if (job) {
41329
- let testBudgetMinutes, baselineStatus, baselineSha;
41382
+ let testBudgetMinutes, baselineStatus, baselineSha, reviewMode;
41330
41383
  ({
41331
41384
  bountyId,
41332
41385
  title,
@@ -41340,12 +41393,15 @@ async function resolveBounty(arg) {
41340
41393
  runRequirements,
41341
41394
  testBudgetMinutes,
41342
41395
  baselineStatus,
41343
- baselineSha
41396
+ baselineSha,
41397
+ reviewMode
41344
41398
  } = extractClaimableFields(job));
41345
41399
  baseline = {
41346
41400
  ...testBudgetMinutes === void 0 ? {} : { testBudgetMinutes },
41347
41401
  ...baselineStatus === void 0 ? {} : { baselineStatus },
41348
- ...baselineSha === void 0 ? {} : { baselineSha }
41402
+ ...baselineSha === void 0 ? {} : { baselineSha },
41403
+ // TERM-1344. Rides with the baseline fields: index-only, and no key when absent.
41404
+ ...reviewMode === void 0 ? {} : { reviewMode }
41349
41405
  };
41350
41406
  indexNativeId = bountyId;
41351
41407
  } else {
@@ -42105,6 +42161,8 @@ async function cmdPreview(arg, { json } = {}) {
42105
42161
  contested: isContested(b),
42106
42162
  // TERM-1308. The posting's test budget and baseline, only when the index carried them.
42107
42163
  ...baselineFields(b),
42164
+ // TERM-1344. Only for a poster-reviewed posting; absent means tests.
42165
+ ...b.reviewMode === POSTER_REVIEWED ? { reviewMode: POSTER_REVIEWED } : {},
42108
42166
  // Absent on a founder posting — see the scan above. A consumer must read
42109
42167
  // "no policy key" as "no policy step exists here", never as "clean".
42110
42168
  ...policy ? {
@@ -42135,8 +42193,8 @@ async function cmdPreview(arg, { json } = {}) {
42135
42193
  console.log(fmtOpenPRsLine(b));
42136
42194
  const previewContested = fmtContestedWarning(b);
42137
42195
  if (previewContested) console.log(previewContested);
42138
- const previewBaseline = baselineLine(b);
42139
- if (previewBaseline) console.log(` tests: ${previewBaseline}`);
42196
+ const previewTests = reviewModeLine(b) ?? baselineLine(b);
42197
+ if (previewTests) console.log(` tests: ${previewTests}`);
42140
42198
  if (policy) printPolicySection(policy);
42141
42199
  if (process.stdout.isTTY) {
42142
42200
  try {
@@ -43392,7 +43450,8 @@ function renderRunView(run3, message2, extra = {}) {
43392
43450
  const rateLimitCap = extra.rateLimitCap ?? run3.rateLimitCap ?? 5;
43393
43451
  const rateLimitCount = extra.rateLimitCount ?? run3.rateLimitCount;
43394
43452
  if (claimState) {
43395
- lines.push(` state: ${terminalSafeInline(claimState)}`);
43453
+ const stateText = claimState === "poster-review" ? HISTORY_LINE : terminalSafeInline(claimState);
43454
+ lines.push(` state: ${stateText}`);
43396
43455
  }
43397
43456
  if (unverifiedCause) {
43398
43457
  lines.push(` cause: ${terminalSafeInline(unverifiedCause)}`);
@@ -43438,7 +43497,8 @@ function renderClaimHistory(attempts, timeline) {
43438
43497
  for (const a of runs) {
43439
43498
  const sha = a.commitSha ? String(a.commitSha).slice(0, 10) : "";
43440
43499
  const jobs = Array.isArray(a.failingJobs) && a.failingJobs.length > 0 ? ` \u2014 ${a.failingJobs.map(terminalSafeInline).join(", ")}` : "";
43441
- const outcome = a.conclusion ? `checks ${terminalSafeInline(a.conclusion)}${jobs}` : a.status === "no-ci" ? "no checks ran" : "checks running";
43500
+ const note = typeof a.note === "string" ? terminalSafeInline(a.note).trim() : "";
43501
+ const outcome = a.conclusion ? `checks ${terminalSafeInline(a.conclusion)}${jobs}` : note !== "" ? note : a.status === "no-ci" ? "no checks ran" : "checks running";
43442
43502
  lines.push(` attempt ${a.attemptNumber} ${sha} ${outcome} ${shortWhen(a.at)}`);
43443
43503
  }
43444
43504
  for (const e of told) {
@@ -44188,6 +44248,57 @@ async function uploadClaimScreenshots(intake, uploads) {
44188
44248
  );
44189
44249
  }
44190
44250
  }
44251
+ async function packageJsonAt(wt, rev, path5) {
44252
+ let text;
44253
+ try {
44254
+ ({ stdout: text } = await pExecFile("git", ["-C", wt, "show", `${rev}:${path5}`], {
44255
+ shell: false,
44256
+ maxBuffer: 16 * 1024 * 1024
44257
+ }));
44258
+ } catch {
44259
+ return {};
44260
+ }
44261
+ try {
44262
+ const parsed = JSON.parse(String(text));
44263
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
44264
+ } catch {
44265
+ return null;
44266
+ }
44267
+ }
44268
+ function sectionKey(pkg, section) {
44269
+ const value = pkg[section];
44270
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
44271
+ return JSON.stringify(value ?? null);
44272
+ }
44273
+ return JSON.stringify(Object.keys(value).sort().map((k) => [k, value[k]]));
44274
+ }
44275
+ async function patchChangesDependencies(wt, base, touched) {
44276
+ for (const path5 of touched) {
44277
+ if (path5.split("/").pop() !== "package.json") continue;
44278
+ const before = await packageJsonAt(wt, base, path5);
44279
+ const after = await packageJsonAt(wt, "HEAD", path5);
44280
+ if (before === null || after === null) continue;
44281
+ if (DEPENDENCY_SECTIONS2.some((s) => sectionKey(before, s) !== sectionKey(after, s))) return true;
44282
+ }
44283
+ return false;
44284
+ }
44285
+ async function dependencyLockfileWarnings(wt, base, touched) {
44286
+ if (!await patchChangesDependencies(wt, base, touched)) return [];
44287
+ const warnings = [];
44288
+ for (const [lockfile, cmd] of SENDABLE_LOCKFILES) {
44289
+ let present;
44290
+ try {
44291
+ present = (await sh("git", ["-C", wt, "ls-tree", "--name-only", base, "--", lockfile])).trim() === lockfile;
44292
+ } catch {
44293
+ present = false;
44294
+ }
44295
+ if (!present || touched.includes(lockfile)) continue;
44296
+ warnings.push(
44297
+ ` \u26A0 package.json changes dependencies, but ${lockfile} is not in this patch. The run will install without its frozen-lockfile check. Run \`${cmd}\` and commit ${lockfile} to keep it frozen.`
44298
+ );
44299
+ }
44300
+ return warnings;
44301
+ }
44191
44302
  async function submitFounderPatch({ claims, claim, id, wt, flags }) {
44192
44303
  let roots;
44193
44304
  try {
@@ -44238,6 +44349,9 @@ async function submitFounderPatch({ claims, claim, id, wt, flags }) {
44238
44349
  );
44239
44350
  process.exit(1);
44240
44351
  }
44352
+ const lockfileWarnings = await dependencyLockfileWarnings(wt, base, touched);
44353
+ if (lockfileWarnings.length > 0) console.log(`
44354
+ ${lockfileWarnings.join("\n")}`);
44241
44355
  const authorName = await sh("git", ["-C", wt, "log", "-1", "--format=%an"]);
44242
44356
  const authorEmail = await sh("git", ["-C", wt, "log", "-1", "--format=%ae"]);
44243
44357
  const shots = flags.screenshots === void 0 ? null : loadClaimScreenshots(flags.screenshots);
@@ -45747,7 +45861,7 @@ async function run() {
45747
45861
  process.exit(1);
45748
45862
  }
45749
45863
  }
45750
- 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, BASELINE_STATUSES, MIN_TEST_BUDGET_MINUTES, MAX_TEST_BUDGET_MINUTES, 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_SCREENSHOT_PUT_TIMEOUT_MS, CLAIM_RESOLUTION_REASONS, SETUP_FAILED_REQUIREMENTS, POSTING_LEVEL_RESOLUTION_REASONS, RESOLUTION_REASON_BLURB;
45864
+ 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, BASELINE_STATUSES, MIN_TEST_BUDGET_MINUTES, MAX_TEST_BUDGET_MINUTES, 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_SCREENSHOT_PUT_TIMEOUT_MS, DEPENDENCY_SECTIONS2, SENDABLE_LOCKFILES, CLAIM_RESOLUTION_REASONS, SETUP_FAILED_REQUIREMENTS, POSTING_LEVEL_RESOLUTION_REASONS, RESOLUTION_REASON_BLURB;
45751
45865
  var init_jpi_claim = __esm({
45752
45866
  "bin/jpi-claim.js"() {
45753
45867
  "use strict";
@@ -45763,6 +45877,7 @@ var init_jpi_claim = __esm({
45763
45877
  init_claim_push_bg();
45764
45878
  init_founder_verdict_sync();
45765
45879
  init_founder_note_sync();
45880
+ init_review_mode_copy();
45766
45881
  TERMINALHIRE_DIR11 = process.env.TERMINALHIRE_DIR || join33(homedir15(), ".terminalhire");
45767
45882
  INDEX_CACHE_FILE2 = join33(TERMINALHIRE_DIR11, "index-cache.json");
45768
45883
  CLAIM_PUSH_MARKER = join33(TERMINALHIRE_DIR11, "claim-push.json");
@@ -45880,6 +45995,16 @@ var init_jpi_claim = __esm({
45880
45995
  CONTROL_CHARS3 = /[\u0000-\u001F\u007F-\u009F]/g;
45881
45996
  FOUNDER_POSTING_ID = /^fb_[A-Za-z0-9_-]{1,80}$/;
45882
45997
  CLAIM_SCREENSHOT_PUT_TIMEOUT_MS = 2e4;
45998
+ DEPENDENCY_SECTIONS2 = [
45999
+ "dependencies",
46000
+ "devDependencies",
46001
+ "optionalDependencies",
46002
+ "peerDependencies"
46003
+ ];
46004
+ SENDABLE_LOCKFILES = [
46005
+ ["package-lock.json", "npm install"],
46006
+ ["yarn.lock", "yarn install"]
46007
+ ];
45883
46008
  CLAIM_RESOLUTION_REASONS = [
45884
46009
  "not-my-stack",
45885
46010
  "out-of-time",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.42.41",
3
+ "version": "0.42.43",
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",