terminalhire 0.42.41 → 0.42.42

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.
@@ -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,
@@ -44169,6 +44204,67 @@ async function uploadClaimScreenshots(intake, uploads) {
44169
44204
  );
44170
44205
  }
44171
44206
  }
44207
+ var DEPENDENCY_SECTIONS2 = [
44208
+ "dependencies",
44209
+ "devDependencies",
44210
+ "optionalDependencies",
44211
+ "peerDependencies"
44212
+ ];
44213
+ var SENDABLE_LOCKFILES = [
44214
+ ["package-lock.json", "npm install"],
44215
+ ["yarn.lock", "yarn install"]
44216
+ ];
44217
+ async function packageJsonAt(wt, rev, path5) {
44218
+ let text;
44219
+ try {
44220
+ ({ stdout: text } = await pExecFile("git", ["-C", wt, "show", `${rev}:${path5}`], {
44221
+ shell: false,
44222
+ maxBuffer: 16 * 1024 * 1024
44223
+ }));
44224
+ } catch {
44225
+ return {};
44226
+ }
44227
+ try {
44228
+ const parsed = JSON.parse(String(text));
44229
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
44230
+ } catch {
44231
+ return null;
44232
+ }
44233
+ }
44234
+ function sectionKey(pkg, section) {
44235
+ const value = pkg[section];
44236
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
44237
+ return JSON.stringify(value ?? null);
44238
+ }
44239
+ return JSON.stringify(Object.keys(value).sort().map((k) => [k, value[k]]));
44240
+ }
44241
+ async function patchChangesDependencies(wt, base, touched) {
44242
+ for (const path5 of touched) {
44243
+ if (path5.split("/").pop() !== "package.json") continue;
44244
+ const before = await packageJsonAt(wt, base, path5);
44245
+ const after = await packageJsonAt(wt, "HEAD", path5);
44246
+ if (before === null || after === null) continue;
44247
+ if (DEPENDENCY_SECTIONS2.some((s) => sectionKey(before, s) !== sectionKey(after, s))) return true;
44248
+ }
44249
+ return false;
44250
+ }
44251
+ async function dependencyLockfileWarnings(wt, base, touched) {
44252
+ if (!await patchChangesDependencies(wt, base, touched)) return [];
44253
+ const warnings = [];
44254
+ for (const [lockfile, cmd] of SENDABLE_LOCKFILES) {
44255
+ let present;
44256
+ try {
44257
+ present = (await sh("git", ["-C", wt, "ls-tree", "--name-only", base, "--", lockfile])).trim() === lockfile;
44258
+ } catch {
44259
+ present = false;
44260
+ }
44261
+ if (!present || touched.includes(lockfile)) continue;
44262
+ warnings.push(
44263
+ ` \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.`
44264
+ );
44265
+ }
44266
+ return warnings;
44267
+ }
44172
44268
  async function submitFounderPatch({ claims, claim, id, wt, flags }) {
44173
44269
  let roots;
44174
44270
  try {
@@ -44219,6 +44315,9 @@ async function submitFounderPatch({ claims, claim, id, wt, flags }) {
44219
44315
  );
44220
44316
  process.exit(1);
44221
44317
  }
44318
+ const lockfileWarnings = await dependencyLockfileWarnings(wt, base, touched);
44319
+ if (lockfileWarnings.length > 0) console.log(`
44320
+ ${lockfileWarnings.join("\n")}`);
44222
44321
  const authorName = await sh("git", ["-C", wt, "log", "-1", "--format=%an"]);
44223
44322
  const authorEmail = await sh("git", ["-C", wt, "log", "-1", "--format=%ae"]);
44224
44323
  const shots = flags.screenshots === void 0 ? null : loadClaimScreenshots(flags.screenshots);
@@ -36824,7 +36824,8 @@ var init_result = __esm({
36824
36824
  "leakState",
36825
36825
  "venue",
36826
36826
  "refusalOrigin",
36827
- "stoppedAt"
36827
+ "stoppedAt",
36828
+ "installFrozen"
36828
36829
  ];
36829
36830
  RENDER_NONE = null;
36830
36831
  FIELD_VIEWS = {
@@ -36889,7 +36890,10 @@ var init_result = __esm({
36889
36890
  // side refused in words a developer reads.
36890
36891
  refusalOrigin: RENDER_NONE,
36891
36892
  // 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)`
36893
+ stoppedAt: (r) => r.stoppedAt === null || r.outcome === "completed" ? null : `last step ${r.stoppedAt} (the last package script named in the output)`,
36894
+ // The run already prints the unfrozen note while it runs (`installUnfrozenNote`); this is
36895
+ // the record of it for the worker and the intake.
36896
+ installFrozen: RENDER_NONE
36893
36897
  };
36894
36898
  }
36895
36899
  });
@@ -40927,6 +40931,10 @@ function nodeInstallCommand(repo) {
40927
40931
  return null;
40928
40932
  }
40929
40933
  function lockfileGaps(repo) {
40934
+ const read = readLockfileGaps(repo);
40935
+ return read === null || read.missing.length === 0 ? null : { lockfile: read.lockfile, missing: read.missing };
40936
+ }
40937
+ function readLockfileGaps(repo) {
40930
40938
  const pkg = readJsonObject(repo, "package.json");
40931
40939
  if (pkg === null)
40932
40940
  return null;
@@ -40953,7 +40961,7 @@ function lockfileGaps(repo) {
40953
40961
  missing.push(`${n}@${r} (${dir})`);
40954
40962
  }
40955
40963
  }
40956
- return missing.length === 0 ? null : { lockfile, missing };
40964
+ return { lockfile, missing, rootRead: rootListing !== null };
40957
40965
  }
40958
40966
  function wantedDependencies(pkg) {
40959
40967
  const wanted = [];
@@ -43454,7 +43462,9 @@ function refusedRun(fields) {
43454
43462
  venue: null,
43455
43463
  refusalOrigin: fields.origin,
43456
43464
  // No test step ran, so there is no output to read a step from.
43457
- stoppedAt: null
43465
+ stoppedAt: null,
43466
+ // Refused before any install step ran.
43467
+ installFrozen: null
43458
43468
  };
43459
43469
  }
43460
43470
  function refusalOriginOf(refusal2) {
@@ -43538,7 +43548,29 @@ function installUnfrozenNote(spec) {
43538
43548
  const u = spec.installUnfrozen;
43539
43549
  if (u === void 0)
43540
43550
  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.`;
43551
+ 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.`;
43552
+ }
43553
+ function isFrozenInstallCommand(command) {
43554
+ const lead = /^(?:(?:sudo|time)\s+|env(?:\s+[A-Za-z_]\w*=\S*)*\s+)*/.exec(command)?.[0] ?? "";
43555
+ const install = command.slice(lead.length).split(/;|&&|\|\|/)[0]?.trim() ?? "";
43556
+ if (/^npm ci(\s|$)/.test(install))
43557
+ return true;
43558
+ if (/^(yarn|pnpm|bun)\b/.test(install) && /\s--frozen-lockfile(?=\s|$)/.test(install)) {
43559
+ return true;
43560
+ }
43561
+ return /^yarn\b/.test(install) && /\s--immutable(?=\s|$)/.test(install);
43562
+ }
43563
+ function installFrozenFor(spec, install) {
43564
+ if (install === null)
43565
+ return null;
43566
+ const launched = install.exitCode !== INSTALL_NEVER_STARTED && (install.exitCode !== null || install.timedOut);
43567
+ if (!launched)
43568
+ return null;
43569
+ if (spec.runtime !== "node")
43570
+ return null;
43571
+ if (spec.installUnfrozen !== void 0)
43572
+ return false;
43573
+ return isFrozenInstallCommand(install.command);
43542
43574
  }
43543
43575
  async function verifyWorkingDiff(req) {
43544
43576
  const ctx = {
@@ -43907,7 +43939,8 @@ async function runVerification(req, ctx) {
43907
43939
  // A run that reached a verdict was not refused, so neither side refused it.
43908
43940
  refusalOrigin: null,
43909
43941
  // Read from the full test output, not `outputTail`, which is an excerpt.
43910
- stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? "")
43942
+ stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? ""),
43943
+ installFrozen: installFrozenFor(spec, verdict.install)
43911
43944
  };
43912
43945
  let later = null;
43913
43946
  if (req.screenshots !== void 0) {
@@ -44106,7 +44139,7 @@ async function prepareBaseTree(o) {
44106
44139
  throw err;
44107
44140
  }
44108
44141
  }
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;
44142
+ 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
44143
  var init_thrun = __esm({
44111
44144
  "../../packages/envrun/dist/thrun.js"() {
44112
44145
  "use strict";
@@ -44195,6 +44228,7 @@ var init_thrun = __esm({
44195
44228
  FAILURE_LINE = /^(?:[ \t]*(?:not ok |FAILED |FAIL )|E {3}|# fail [1-9])/m;
44196
44229
  REDACTED_TARGET_REPO = "(refused before the target was accepted)";
44197
44230
  REDACTED_TARGET_SHA = "(refused)";
44231
+ INSTALL_NEVER_STARTED = 125;
44198
44232
  }
44199
44233
  });
44200
44234
 
@@ -45096,6 +45130,7 @@ __export(dist_exports, {
45096
45130
  imageRepo: () => imageRepo,
45097
45131
  imageVariantFor: () => imageVariantFor,
45098
45132
  installCommandFor: () => installCommandFor,
45133
+ installFrozenFor: () => installFrozenFor,
45099
45134
  installLocalMigrationTooling: () => installLocalMigrationTooling,
45100
45135
  installUnfrozenNote: () => installUnfrozenNote,
45101
45136
  isBookkeepingTable: () => isBookkeepingTable,
@@ -49429,6 +49464,57 @@ async function uploadClaimScreenshots(intake, uploads) {
49429
49464
  );
49430
49465
  }
49431
49466
  }
49467
+ async function packageJsonAt(wt, rev, path6) {
49468
+ let text;
49469
+ try {
49470
+ ({ stdout: text } = await pExecFile("git", ["-C", wt, "show", `${rev}:${path6}`], {
49471
+ shell: false,
49472
+ maxBuffer: 16 * 1024 * 1024
49473
+ }));
49474
+ } catch {
49475
+ return {};
49476
+ }
49477
+ try {
49478
+ const parsed = JSON.parse(String(text));
49479
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
49480
+ } catch {
49481
+ return null;
49482
+ }
49483
+ }
49484
+ function sectionKey(pkg, section2) {
49485
+ const value = pkg[section2];
49486
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
49487
+ return JSON.stringify(value ?? null);
49488
+ }
49489
+ return JSON.stringify(Object.keys(value).sort().map((k) => [k, value[k]]));
49490
+ }
49491
+ async function patchChangesDependencies(wt, base, touched) {
49492
+ for (const path6 of touched) {
49493
+ if (path6.split("/").pop() !== "package.json") continue;
49494
+ const before = await packageJsonAt(wt, base, path6);
49495
+ const after = await packageJsonAt(wt, "HEAD", path6);
49496
+ if (before === null || after === null) continue;
49497
+ if (DEPENDENCY_SECTIONS2.some((s) => sectionKey(before, s) !== sectionKey(after, s))) return true;
49498
+ }
49499
+ return false;
49500
+ }
49501
+ async function dependencyLockfileWarnings(wt, base, touched) {
49502
+ if (!await patchChangesDependencies(wt, base, touched)) return [];
49503
+ const warnings = [];
49504
+ for (const [lockfile, cmd] of SENDABLE_LOCKFILES) {
49505
+ let present;
49506
+ try {
49507
+ present = (await sh("git", ["-C", wt, "ls-tree", "--name-only", base, "--", lockfile])).trim() === lockfile;
49508
+ } catch {
49509
+ present = false;
49510
+ }
49511
+ if (!present || touched.includes(lockfile)) continue;
49512
+ warnings.push(
49513
+ ` \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.`
49514
+ );
49515
+ }
49516
+ return warnings;
49517
+ }
49432
49518
  async function submitFounderPatch({ claims, claim, id, wt, flags }) {
49433
49519
  let roots;
49434
49520
  try {
@@ -49479,6 +49565,9 @@ async function submitFounderPatch({ claims, claim, id, wt, flags }) {
49479
49565
  );
49480
49566
  process.exit(1);
49481
49567
  }
49568
+ const lockfileWarnings = await dependencyLockfileWarnings(wt, base, touched);
49569
+ if (lockfileWarnings.length > 0) console.log(`
49570
+ ${lockfileWarnings.join("\n")}`);
49482
49571
  const authorName = await sh("git", ["-C", wt, "log", "-1", "--format=%an"]);
49483
49572
  const authorEmail = await sh("git", ["-C", wt, "log", "-1", "--format=%ae"]);
49484
49573
  const shots = flags.screenshots === void 0 ? null : loadClaimScreenshots(flags.screenshots);
@@ -50988,7 +51077,7 @@ async function run7() {
50988
51077
  process.exit(1);
50989
51078
  }
50990
51079
  }
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;
51080
+ 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
51081
  var init_jpi_claim = __esm({
50993
51082
  "bin/jpi-claim.js"() {
50994
51083
  "use strict";
@@ -51121,6 +51210,16 @@ var init_jpi_claim = __esm({
51121
51210
  CONTROL_CHARS3 = /[\u0000-\u001F\u007F-\u009F]/g;
51122
51211
  FOUNDER_POSTING_ID = /^fb_[A-Za-z0-9_-]{1,80}$/;
51123
51212
  CLAIM_SCREENSHOT_PUT_TIMEOUT_MS = 2e4;
51213
+ DEPENDENCY_SECTIONS2 = [
51214
+ "dependencies",
51215
+ "devDependencies",
51216
+ "optionalDependencies",
51217
+ "peerDependencies"
51218
+ ];
51219
+ SENDABLE_LOCKFILES = [
51220
+ ["package-lock.json", "npm install"],
51221
+ ["yarn.lock", "yarn install"]
51222
+ ];
51124
51223
  CLAIM_RESOLUTION_REASONS = [
51125
51224
  "not-my-stack",
51126
51225
  "out-of-time",
@@ -31590,7 +31590,8 @@ var init_result = __esm({
31590
31590
  "leakState",
31591
31591
  "venue",
31592
31592
  "refusalOrigin",
31593
- "stoppedAt"
31593
+ "stoppedAt",
31594
+ "installFrozen"
31594
31595
  ];
31595
31596
  RENDER_NONE = null;
31596
31597
  FIELD_VIEWS = {
@@ -31655,7 +31656,10 @@ var init_result = __esm({
31655
31656
  // side refused in words a developer reads.
31656
31657
  refusalOrigin: RENDER_NONE,
31657
31658
  // 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)`
31659
+ stoppedAt: (r) => r.stoppedAt === null || r.outcome === "completed" ? null : `last step ${r.stoppedAt} (the last package script named in the output)`,
31660
+ // The run already prints the unfrozen note while it runs (`installUnfrozenNote`); this is
31661
+ // the record of it for the worker and the intake.
31662
+ installFrozen: RENDER_NONE
31659
31663
  };
31660
31664
  }
31661
31665
  });
@@ -35693,6 +35697,10 @@ function nodeInstallCommand(repo) {
35693
35697
  return null;
35694
35698
  }
35695
35699
  function lockfileGaps(repo) {
35700
+ const read = readLockfileGaps(repo);
35701
+ return read === null || read.missing.length === 0 ? null : { lockfile: read.lockfile, missing: read.missing };
35702
+ }
35703
+ function readLockfileGaps(repo) {
35696
35704
  const pkg = readJsonObject(repo, "package.json");
35697
35705
  if (pkg === null)
35698
35706
  return null;
@@ -35719,7 +35727,7 @@ function lockfileGaps(repo) {
35719
35727
  missing.push(`${n}@${r} (${dir})`);
35720
35728
  }
35721
35729
  }
35722
- return missing.length === 0 ? null : { lockfile, missing };
35730
+ return { lockfile, missing, rootRead: rootListing !== null };
35723
35731
  }
35724
35732
  function wantedDependencies(pkg) {
35725
35733
  const wanted = [];
@@ -38220,7 +38228,9 @@ function refusedRun(fields) {
38220
38228
  venue: null,
38221
38229
  refusalOrigin: fields.origin,
38222
38230
  // No test step ran, so there is no output to read a step from.
38223
- stoppedAt: null
38231
+ stoppedAt: null,
38232
+ // Refused before any install step ran.
38233
+ installFrozen: null
38224
38234
  };
38225
38235
  }
38226
38236
  function refusalOriginOf(refusal2) {
@@ -38304,7 +38314,29 @@ function installUnfrozenNote(spec) {
38304
38314
  const u = spec.installUnfrozen;
38305
38315
  if (u === void 0)
38306
38316
  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.`;
38317
+ 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.`;
38318
+ }
38319
+ function isFrozenInstallCommand(command) {
38320
+ const lead = /^(?:(?:sudo|time)\s+|env(?:\s+[A-Za-z_]\w*=\S*)*\s+)*/.exec(command)?.[0] ?? "";
38321
+ const install = command.slice(lead.length).split(/;|&&|\|\|/)[0]?.trim() ?? "";
38322
+ if (/^npm ci(\s|$)/.test(install))
38323
+ return true;
38324
+ if (/^(yarn|pnpm|bun)\b/.test(install) && /\s--frozen-lockfile(?=\s|$)/.test(install)) {
38325
+ return true;
38326
+ }
38327
+ return /^yarn\b/.test(install) && /\s--immutable(?=\s|$)/.test(install);
38328
+ }
38329
+ function installFrozenFor(spec, install) {
38330
+ if (install === null)
38331
+ return null;
38332
+ const launched = install.exitCode !== INSTALL_NEVER_STARTED && (install.exitCode !== null || install.timedOut);
38333
+ if (!launched)
38334
+ return null;
38335
+ if (spec.runtime !== "node")
38336
+ return null;
38337
+ if (spec.installUnfrozen !== void 0)
38338
+ return false;
38339
+ return isFrozenInstallCommand(install.command);
38308
38340
  }
38309
38341
  async function verifyWorkingDiff(req) {
38310
38342
  const ctx = {
@@ -38673,7 +38705,8 @@ async function runVerification(req, ctx) {
38673
38705
  // A run that reached a verdict was not refused, so neither side refused it.
38674
38706
  refusalOrigin: null,
38675
38707
  // Read from the full test output, not `outputTail`, which is an excerpt.
38676
- stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? "")
38708
+ stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? ""),
38709
+ installFrozen: installFrozenFor(spec, verdict.install)
38677
38710
  };
38678
38711
  let later = null;
38679
38712
  if (req.screenshots !== void 0) {
@@ -38872,7 +38905,7 @@ async function prepareBaseTree(o) {
38872
38905
  throw err;
38873
38906
  }
38874
38907
  }
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;
38908
+ 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
38909
  var init_thrun = __esm({
38877
38910
  "../../packages/envrun/dist/thrun.js"() {
38878
38911
  "use strict";
@@ -38961,6 +38994,7 @@ var init_thrun = __esm({
38961
38994
  FAILURE_LINE = /^(?:[ \t]*(?:not ok |FAILED |FAIL )|E {3}|# fail [1-9])/m;
38962
38995
  REDACTED_TARGET_REPO = "(refused before the target was accepted)";
38963
38996
  REDACTED_TARGET_SHA = "(refused)";
38997
+ INSTALL_NEVER_STARTED = 125;
38964
38998
  }
38965
38999
  });
38966
39000
 
@@ -39862,6 +39896,7 @@ __export(dist_exports, {
39862
39896
  imageRepo: () => imageRepo,
39863
39897
  imageVariantFor: () => imageVariantFor,
39864
39898
  installCommandFor: () => installCommandFor,
39899
+ installFrozenFor: () => installFrozenFor,
39865
39900
  installLocalMigrationTooling: () => installLocalMigrationTooling,
39866
39901
  installUnfrozenNote: () => installUnfrozenNote,
39867
39902
  isBookkeepingTable: () => isBookkeepingTable,
@@ -44195,6 +44230,57 @@ async function uploadClaimScreenshots(intake, uploads) {
44195
44230
  );
44196
44231
  }
44197
44232
  }
44233
+ async function packageJsonAt(wt, rev, path5) {
44234
+ let text;
44235
+ try {
44236
+ ({ stdout: text } = await pExecFile("git", ["-C", wt, "show", `${rev}:${path5}`], {
44237
+ shell: false,
44238
+ maxBuffer: 16 * 1024 * 1024
44239
+ }));
44240
+ } catch {
44241
+ return {};
44242
+ }
44243
+ try {
44244
+ const parsed = JSON.parse(String(text));
44245
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
44246
+ } catch {
44247
+ return null;
44248
+ }
44249
+ }
44250
+ function sectionKey(pkg, section) {
44251
+ const value = pkg[section];
44252
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
44253
+ return JSON.stringify(value ?? null);
44254
+ }
44255
+ return JSON.stringify(Object.keys(value).sort().map((k) => [k, value[k]]));
44256
+ }
44257
+ async function patchChangesDependencies(wt, base, touched) {
44258
+ for (const path5 of touched) {
44259
+ if (path5.split("/").pop() !== "package.json") continue;
44260
+ const before = await packageJsonAt(wt, base, path5);
44261
+ const after = await packageJsonAt(wt, "HEAD", path5);
44262
+ if (before === null || after === null) continue;
44263
+ if (DEPENDENCY_SECTIONS2.some((s) => sectionKey(before, s) !== sectionKey(after, s))) return true;
44264
+ }
44265
+ return false;
44266
+ }
44267
+ async function dependencyLockfileWarnings(wt, base, touched) {
44268
+ if (!await patchChangesDependencies(wt, base, touched)) return [];
44269
+ const warnings = [];
44270
+ for (const [lockfile, cmd] of SENDABLE_LOCKFILES) {
44271
+ let present;
44272
+ try {
44273
+ present = (await sh("git", ["-C", wt, "ls-tree", "--name-only", base, "--", lockfile])).trim() === lockfile;
44274
+ } catch {
44275
+ present = false;
44276
+ }
44277
+ if (!present || touched.includes(lockfile)) continue;
44278
+ warnings.push(
44279
+ ` \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.`
44280
+ );
44281
+ }
44282
+ return warnings;
44283
+ }
44198
44284
  async function submitFounderPatch({ claims, claim, id, wt, flags }) {
44199
44285
  let roots;
44200
44286
  try {
@@ -44245,6 +44331,9 @@ async function submitFounderPatch({ claims, claim, id, wt, flags }) {
44245
44331
  );
44246
44332
  process.exit(1);
44247
44333
  }
44334
+ const lockfileWarnings = await dependencyLockfileWarnings(wt, base, touched);
44335
+ if (lockfileWarnings.length > 0) console.log(`
44336
+ ${lockfileWarnings.join("\n")}`);
44248
44337
  const authorName = await sh("git", ["-C", wt, "log", "-1", "--format=%an"]);
44249
44338
  const authorEmail = await sh("git", ["-C", wt, "log", "-1", "--format=%ae"]);
44250
44339
  const shots = flags.screenshots === void 0 ? null : loadClaimScreenshots(flags.screenshots);
@@ -45754,7 +45843,7 @@ async function run() {
45754
45843
  process.exit(1);
45755
45844
  }
45756
45845
  }
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;
45846
+ 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
45847
  var init_jpi_claim = __esm({
45759
45848
  "bin/jpi-claim.js"() {
45760
45849
  "use strict";
@@ -45887,6 +45976,16 @@ var init_jpi_claim = __esm({
45887
45976
  CONTROL_CHARS3 = /[\u0000-\u001F\u007F-\u009F]/g;
45888
45977
  FOUNDER_POSTING_ID = /^fb_[A-Za-z0-9_-]{1,80}$/;
45889
45978
  CLAIM_SCREENSHOT_PUT_TIMEOUT_MS = 2e4;
45979
+ DEPENDENCY_SECTIONS2 = [
45980
+ "dependencies",
45981
+ "devDependencies",
45982
+ "optionalDependencies",
45983
+ "peerDependencies"
45984
+ ];
45985
+ SENDABLE_LOCKFILES = [
45986
+ ["package-lock.json", "npm install"],
45987
+ ["yarn.lock", "yarn install"]
45988
+ ];
45890
45989
  CLAIM_RESOLUTION_REASONS = [
45891
45990
  "not-my-stack",
45892
45991
  "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,
@@ -44188,6 +44223,57 @@ async function uploadClaimScreenshots(intake, uploads) {
44188
44223
  );
44189
44224
  }
44190
44225
  }
44226
+ async function packageJsonAt(wt, rev, path5) {
44227
+ let text;
44228
+ try {
44229
+ ({ stdout: text } = await pExecFile("git", ["-C", wt, "show", `${rev}:${path5}`], {
44230
+ shell: false,
44231
+ maxBuffer: 16 * 1024 * 1024
44232
+ }));
44233
+ } catch {
44234
+ return {};
44235
+ }
44236
+ try {
44237
+ const parsed = JSON.parse(String(text));
44238
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
44239
+ } catch {
44240
+ return null;
44241
+ }
44242
+ }
44243
+ function sectionKey(pkg, section) {
44244
+ const value = pkg[section];
44245
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
44246
+ return JSON.stringify(value ?? null);
44247
+ }
44248
+ return JSON.stringify(Object.keys(value).sort().map((k) => [k, value[k]]));
44249
+ }
44250
+ async function patchChangesDependencies(wt, base, touched) {
44251
+ for (const path5 of touched) {
44252
+ if (path5.split("/").pop() !== "package.json") continue;
44253
+ const before = await packageJsonAt(wt, base, path5);
44254
+ const after = await packageJsonAt(wt, "HEAD", path5);
44255
+ if (before === null || after === null) continue;
44256
+ if (DEPENDENCY_SECTIONS2.some((s) => sectionKey(before, s) !== sectionKey(after, s))) return true;
44257
+ }
44258
+ return false;
44259
+ }
44260
+ async function dependencyLockfileWarnings(wt, base, touched) {
44261
+ if (!await patchChangesDependencies(wt, base, touched)) return [];
44262
+ const warnings = [];
44263
+ for (const [lockfile, cmd] of SENDABLE_LOCKFILES) {
44264
+ let present;
44265
+ try {
44266
+ present = (await sh("git", ["-C", wt, "ls-tree", "--name-only", base, "--", lockfile])).trim() === lockfile;
44267
+ } catch {
44268
+ present = false;
44269
+ }
44270
+ if (!present || touched.includes(lockfile)) continue;
44271
+ warnings.push(
44272
+ ` \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.`
44273
+ );
44274
+ }
44275
+ return warnings;
44276
+ }
44191
44277
  async function submitFounderPatch({ claims, claim, id, wt, flags }) {
44192
44278
  let roots;
44193
44279
  try {
@@ -44238,6 +44324,9 @@ async function submitFounderPatch({ claims, claim, id, wt, flags }) {
44238
44324
  );
44239
44325
  process.exit(1);
44240
44326
  }
44327
+ const lockfileWarnings = await dependencyLockfileWarnings(wt, base, touched);
44328
+ if (lockfileWarnings.length > 0) console.log(`
44329
+ ${lockfileWarnings.join("\n")}`);
44241
44330
  const authorName = await sh("git", ["-C", wt, "log", "-1", "--format=%an"]);
44242
44331
  const authorEmail = await sh("git", ["-C", wt, "log", "-1", "--format=%ae"]);
44243
44332
  const shots = flags.screenshots === void 0 ? null : loadClaimScreenshots(flags.screenshots);
@@ -45747,7 +45836,7 @@ async function run() {
45747
45836
  process.exit(1);
45748
45837
  }
45749
45838
  }
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;
45839
+ 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
45840
  var init_jpi_claim = __esm({
45752
45841
  "bin/jpi-claim.js"() {
45753
45842
  "use strict";
@@ -45880,6 +45969,16 @@ var init_jpi_claim = __esm({
45880
45969
  CONTROL_CHARS3 = /[\u0000-\u001F\u007F-\u009F]/g;
45881
45970
  FOUNDER_POSTING_ID = /^fb_[A-Za-z0-9_-]{1,80}$/;
45882
45971
  CLAIM_SCREENSHOT_PUT_TIMEOUT_MS = 2e4;
45972
+ DEPENDENCY_SECTIONS2 = [
45973
+ "dependencies",
45974
+ "devDependencies",
45975
+ "optionalDependencies",
45976
+ "peerDependencies"
45977
+ ];
45978
+ SENDABLE_LOCKFILES = [
45979
+ ["package-lock.json", "npm install"],
45980
+ ["yarn.lock", "yarn install"]
45981
+ ];
45883
45982
  CLAIM_RESOLUTION_REASONS = [
45884
45983
  "not-my-stack",
45885
45984
  "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.42",
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",