@gethmy/agent 1.49.16 → 1.49.18

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.
Files changed (3) hide show
  1. package/dist/cli.js +357 -237
  2. package/dist/index.js +330 -234
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1718,6 +1718,169 @@ var init_contract_phase = __esm(() => {
1718
1718
  CONTRACT_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + CONTRACT_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
1719
1719
  });
1720
1720
 
1721
+ // src/human-paths.ts
1722
+ import { execFile } from "node:child_process";
1723
+ import { promisify } from "node:util";
1724
+ import { log as log4 } from "@gethmy/harness";
1725
+ async function readPrChangedFiles(prUrl, cwd, run = defaultRun) {
1726
+ const match = PR_URL_RE.exec(prUrl);
1727
+ if (!match)
1728
+ return { ok: false, reason: "the pull request url is not valid" };
1729
+ const [, owner, repo, number] = match;
1730
+ try {
1731
+ const countOut = await run(["pr", "view", prUrl, "--json", "changedFiles"], cwd);
1732
+ const expected = JSON.parse(countOut.trim()).changedFiles;
1733
+ if (typeof expected !== "number" || !Number.isInteger(expected)) {
1734
+ return { ok: false, reason: "gh did not report a changed-file count" };
1735
+ }
1736
+ const out = await run([
1737
+ "api",
1738
+ `repos/${owner}/${repo}/pulls/${number}/files?per_page=100`,
1739
+ "--paginate",
1740
+ "--slurp"
1741
+ ], cwd);
1742
+ const parsed = JSON.parse(out.trim());
1743
+ const entries = (Array.isArray(parsed) ? parsed : [parsed]).flatMap((page) => Array.isArray(page) ? page : [page]);
1744
+ const files = new Set;
1745
+ const deleted = new Set;
1746
+ for (const entry of entries) {
1747
+ if (typeof entry?.filename !== "string" || entry.filename === "") {
1748
+ return { ok: false, reason: "the file list holds an unreadable entry" };
1749
+ }
1750
+ files.add(entry.filename);
1751
+ if (entry.status === "removed")
1752
+ deleted.add(entry.filename);
1753
+ if (typeof entry.previous_filename === "string") {
1754
+ files.add(entry.previous_filename);
1755
+ }
1756
+ }
1757
+ if (entries.length !== expected) {
1758
+ return {
1759
+ ok: false,
1760
+ reason: `read ${entries.length} files but the pull request changes ${expected}`
1761
+ };
1762
+ }
1763
+ return { ok: true, files: [...files], deleted: [...deleted] };
1764
+ } catch (err) {
1765
+ return {
1766
+ ok: false,
1767
+ reason: `gh failed: ${err instanceof Error ? err.message : String(err)}`
1768
+ };
1769
+ }
1770
+ }
1771
+ function tokenize(glob) {
1772
+ const tokens = [];
1773
+ for (let i = 0;i < glob.length; i++) {
1774
+ const c = glob[i];
1775
+ if (c === "*" && glob[i + 1] === "*") {
1776
+ while (glob[i + 1] === "*")
1777
+ i++;
1778
+ if (glob[i + 1] === "/") {
1779
+ i++;
1780
+ tokens.push({ kind: "dirs" });
1781
+ } else {
1782
+ tokens.push({ kind: "globstar" });
1783
+ }
1784
+ } else if (c === "*") {
1785
+ tokens.push({ kind: "star" });
1786
+ } else if (c === "?") {
1787
+ tokens.push({ kind: "one" });
1788
+ } else {
1789
+ tokens.push({ kind: "lit", char: c });
1790
+ }
1791
+ }
1792
+ return tokens;
1793
+ }
1794
+ function matchesGlob(glob, path) {
1795
+ const text = path.toLowerCase();
1796
+ const pattern = glob.toLowerCase();
1797
+ if (pattern.endsWith("/**") && text === pattern.slice(0, -3))
1798
+ return true;
1799
+ const tokens = tokenize(pattern);
1800
+ const n = text.length;
1801
+ let next = new Uint8Array(n + 2);
1802
+ next[n] = 1;
1803
+ for (let ti = tokens.length - 1;ti >= 0; ti--) {
1804
+ const token = tokens[ti];
1805
+ const cur = new Uint8Array(n + 2);
1806
+ let inner = 0;
1807
+ for (let pi = n;pi >= 0; pi--) {
1808
+ const ch = pi < n ? text[pi] : "";
1809
+ if (token.kind === "dirs") {
1810
+ if (pi < n)
1811
+ inner = ch === "/" && next[pi + 1] ? 1 : inner;
1812
+ cur[pi] = next[pi] || inner ? 1 : 0;
1813
+ continue;
1814
+ }
1815
+ switch (token.kind) {
1816
+ case "lit":
1817
+ cur[pi] = pi < n && ch === token.char && next[pi + 1] ? 1 : 0;
1818
+ break;
1819
+ case "one":
1820
+ cur[pi] = pi < n && ch !== "/" && next[pi + 1] ? 1 : 0;
1821
+ break;
1822
+ case "star":
1823
+ cur[pi] = next[pi] || pi < n && ch !== "/" && cur[pi + 1] ? 1 : 0;
1824
+ break;
1825
+ case "globstar":
1826
+ cur[pi] = next[pi] || pi < n && cur[pi + 1] ? 1 : 0;
1827
+ break;
1828
+ }
1829
+ }
1830
+ next = cur;
1831
+ }
1832
+ return next[0] === 1;
1833
+ }
1834
+ function checkHumanPaths(read, globs) {
1835
+ if (!read.ok)
1836
+ return { kind: "unreadable", reason: read.reason };
1837
+ const pathGlobs = globs.filter((g) => g !== DELETED_FILE_ENTRY);
1838
+ const holdDeletions = pathGlobs.length !== globs.length;
1839
+ const held = [];
1840
+ const seen = new Set;
1841
+ for (const path of read.files) {
1842
+ const glob = pathGlobs.find((g) => matchesGlob(g, path));
1843
+ if (glob === undefined)
1844
+ continue;
1845
+ held.push({ path, glob });
1846
+ seen.add(path);
1847
+ }
1848
+ if (holdDeletions) {
1849
+ for (const path of read.deleted ?? []) {
1850
+ if (!seen.has(path))
1851
+ held.push({ path, glob: DELETED_FILE_ENTRY });
1852
+ }
1853
+ }
1854
+ const first = held[0];
1855
+ if (first === undefined)
1856
+ return { kind: "clear" };
1857
+ return {
1858
+ kind: "held",
1859
+ path: first.path,
1860
+ glob: first.glob,
1861
+ paths: held.map((h) => h.path)
1862
+ };
1863
+ }
1864
+ async function evaluateHumanPaths(prUrl, cwd, globs, read = readPrChangedFiles) {
1865
+ const check = checkHumanPaths(await read(prUrl, cwd), globs);
1866
+ if (check.kind === "unreadable") {
1867
+ log4.warn(TAG4, `${prUrl}: ${check.reason}, so the sensitive-path check cannot run`);
1868
+ }
1869
+ return check;
1870
+ }
1871
+ var TAG4 = "human-paths", PR_URL_RE, DELETED_FILE_ENTRY = ":deleted-file", defaultRun = async (args, cwd) => {
1872
+ const { stdout } = await promisify(execFile)("gh", args, {
1873
+ cwd,
1874
+ encoding: "utf-8",
1875
+ timeout: 60000,
1876
+ maxBuffer: 16 * 1024 * 1024
1877
+ });
1878
+ return stdout;
1879
+ };
1880
+ var init_human_paths = __esm(() => {
1881
+ PR_URL_RE = /^https:\/\/github\.com\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)\/pull\/(\d+)$/;
1882
+ });
1883
+
1721
1884
  // src/plan-phase.ts
1722
1885
  function scoreComplexity(enriched) {
1723
1886
  const { card, labels, subtasks } = enriched;
@@ -1888,6 +2051,7 @@ var init_types = __esm(() => {
1888
2051
  init_src();
1889
2052
  init_board_review();
1890
2053
  init_contract_phase();
2054
+ init_human_paths();
1891
2055
  init_plan_phase();
1892
2056
  RESEARCH_RULES = ["shadow", "armed"];
1893
2057
  DEFAULT_FINDING_AUTHORS = [
@@ -1895,6 +2059,22 @@ var init_types = __esm(() => {
1895
2059
  "github-actions[bot]"
1896
2060
  ];
1897
2061
  DEFAULT_HUMAN_PATHS = [
2062
+ DELETED_FILE_ENTRY,
2063
+ "**/billing/**",
2064
+ "**/payment/**",
2065
+ "**/payments/**",
2066
+ "**/stripe/**",
2067
+ "src/contexts/AuthContext.tsx",
2068
+ "src/lib/auth*.ts",
2069
+ "src/pages/*Auth*.tsx",
2070
+ "src/hooks/use*OAuth*.ts",
2071
+ "src/components/settings/OAuth*.tsx",
2072
+ "packages/mobile/app/(auth)/**",
2073
+ "packages/mobile/src/components/auth/**",
2074
+ "packages/mobile/src/lib/auth-gate.ts",
2075
+ "packages/mobile/src/lib/biometric*.ts",
2076
+ "packages/mobile/src/stores/auth-store.ts",
2077
+ "packages/quickbar/src/renderer/src/auth-failure.ts",
1898
2078
  ".github/workflows/**",
1899
2079
  ".github/scripts/**",
1900
2080
  "supabase/migrations/**",
@@ -2262,7 +2442,7 @@ __export(exports_board_helpers, {
2262
2442
  buildLabelMap: () => buildLabelMap,
2263
2443
  addLabelByName: () => addLabelByName
2264
2444
  });
2265
- import { log as log4 } from "@gethmy/harness";
2445
+ import { log as log5 } from "@gethmy/harness";
2266
2446
  function buildLabelMap(boardLabels) {
2267
2447
  const map = new Map;
2268
2448
  for (const label of boardLabels) {
@@ -2285,18 +2465,18 @@ async function moveCardToColumn(client, card, targetColumnName) {
2285
2465
  const board = await client.getBoard(card.project_id);
2286
2466
  const targetColumn = board.columns.find((c) => c.name.toLowerCase() === targetColumnName.toLowerCase());
2287
2467
  if (!targetColumn) {
2288
- log4.warn(TAG4, `Column "${targetColumnName}" not found, skipping move`);
2468
+ log5.warn(TAG5, `Column "${targetColumnName}" not found, skipping move`);
2289
2469
  return;
2290
2470
  }
2291
2471
  if (card.column_id === targetColumn.id) {
2292
- log4.debug(TAG4, `Card already in "${targetColumnName}"`);
2472
+ log5.debug(TAG5, `Card already in "${targetColumnName}"`);
2293
2473
  return;
2294
2474
  }
2295
2475
  await client.moveCard(card.id, targetColumn.id);
2296
2476
  card.column_id = targetColumn.id;
2297
- log4.info(TAG4, `Moved #${card.short_id} to "${targetColumnName}"`);
2477
+ log5.info(TAG5, `Moved #${card.short_id} to "${targetColumnName}"`);
2298
2478
  } catch (err) {
2299
- log4.error(TAG4, `Failed to move card: ${err instanceof Error ? err.message : err}`);
2479
+ log5.error(TAG5, `Failed to move card: ${err instanceof Error ? err.message : err}`);
2300
2480
  }
2301
2481
  }
2302
2482
  async function findOrCreateLabel(client, projectId, labelName, color = "#22c55e") {
@@ -2312,13 +2492,13 @@ async function findOrCreateLabel(client, projectId, labelName, color = "#22c55e"
2312
2492
  });
2313
2493
  const labelId = result?.label?.id;
2314
2494
  if (labelId) {
2315
- log4.info(TAG4, `Created label "${labelName}"`);
2495
+ log5.info(TAG5, `Created label "${labelName}"`);
2316
2496
  return labelId;
2317
2497
  }
2318
- log4.warn(TAG4, `createLabel succeeded but returned no id`);
2498
+ log5.warn(TAG5, `createLabel succeeded but returned no id`);
2319
2499
  return null;
2320
2500
  } catch (err) {
2321
- log4.error(TAG4, `Failed to find/create label "${labelName}": ${err instanceof Error ? err.message : err}`);
2501
+ log5.error(TAG5, `Failed to find/create label "${labelName}": ${err instanceof Error ? err.message : err}`);
2322
2502
  return null;
2323
2503
  }
2324
2504
  }
@@ -2328,9 +2508,9 @@ async function addLabelByName(client, card, labelName, color) {
2328
2508
  return;
2329
2509
  try {
2330
2510
  await client.addLabelToCard(card.id, labelId);
2331
- log4.info(TAG4, `Added label "${labelName}" to #${card.short_id}`);
2511
+ log5.info(TAG5, `Added label "${labelName}" to #${card.short_id}`);
2332
2512
  } catch (err) {
2333
- log4.error(TAG4, `Failed to add label to card: ${err instanceof Error ? err.message : err}`);
2513
+ log5.error(TAG5, `Failed to add label to card: ${err instanceof Error ? err.message : err}`);
2334
2514
  }
2335
2515
  }
2336
2516
  async function moveCardAndAddLabel(client, card, targetColumnName, labelName, labelColor = "#8b5cf6") {
@@ -2341,18 +2521,18 @@ async function moveCardAndAddLabel(client, card, targetColumnName, labelName, la
2341
2521
  const labels = board.labels ?? [];
2342
2522
  const targetColumn = columns.find((c) => c.name.toLowerCase() === targetColumnName.toLowerCase());
2343
2523
  if (!targetColumn) {
2344
- log4.warn(TAG4, `Column "${targetColumnName}" not found on board (available: ${columns.map((c) => c.name).join(", ")})`);
2524
+ log5.warn(TAG5, `Column "${targetColumnName}" not found on board (available: ${columns.map((c) => c.name).join(", ")})`);
2345
2525
  } else if (card.column_id === targetColumn.id) {
2346
- log4.debug(TAG4, `Card #${card.short_id} already in "${targetColumnName}"`);
2526
+ log5.debug(TAG5, `Card #${card.short_id} already in "${targetColumnName}"`);
2347
2527
  moved = true;
2348
2528
  } else {
2349
2529
  try {
2350
2530
  await client.moveCard(card.id, targetColumn.id);
2351
2531
  card.column_id = targetColumn.id;
2352
- log4.info(TAG4, `Moved #${card.short_id} to "${targetColumnName}"`);
2532
+ log5.info(TAG5, `Moved #${card.short_id} to "${targetColumnName}"`);
2353
2533
  moved = true;
2354
2534
  } catch (err) {
2355
- log4.error(TAG4, `Failed to move #${card.short_id} to "${targetColumnName}": ${err instanceof Error ? err.message : err}`);
2535
+ log5.error(TAG5, `Failed to move #${card.short_id} to "${targetColumnName}": ${err instanceof Error ? err.message : err}`);
2356
2536
  }
2357
2537
  }
2358
2538
  let labelId;
@@ -2366,26 +2546,26 @@ async function moveCardAndAddLabel(client, card, targetColumnName, labelName, la
2366
2546
  });
2367
2547
  labelId = result?.label?.id;
2368
2548
  if (labelId)
2369
- log4.info(TAG4, `Created label "${labelName}"`);
2549
+ log5.info(TAG5, `Created label "${labelName}"`);
2370
2550
  }
2371
2551
  if (labelId) {
2372
2552
  try {
2373
2553
  await client.addLabelToCard(card.id, labelId);
2374
- log4.info(TAG4, `Added label "${labelName}" to #${card.short_id}`);
2554
+ log5.info(TAG5, `Added label "${labelName}" to #${card.short_id}`);
2375
2555
  } catch (err) {
2376
- log4.error(TAG4, `Failed to add label "${labelName}" to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
2556
+ log5.error(TAG5, `Failed to add label "${labelName}" to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
2377
2557
  }
2378
2558
  }
2379
2559
  } catch (err) {
2380
- log4.error(TAG4, `Failed to prepare move/label for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
2560
+ log5.error(TAG5, `Failed to prepare move/label for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
2381
2561
  }
2382
2562
  return moved;
2383
2563
  }
2384
- var TAG4 = "board";
2564
+ var TAG5 = "board";
2385
2565
  var init_board_helpers = () => {};
2386
2566
 
2387
2567
  // src/board-reviewer.ts
2388
- import { log as log5 } from "@gethmy/harness";
2568
+ import { log as log6 } from "@gethmy/harness";
2389
2569
 
2390
2570
  class BoardReviewer {
2391
2571
  client;
@@ -2411,7 +2591,7 @@ class BoardReviewer {
2411
2591
  this.running = true;
2412
2592
  const { runAtHour, runAtMinute } = this.config.boardReview;
2413
2593
  const delay = msUntilNextRun(this.now(), runAtHour, runAtMinute);
2414
- log5.info(TAG5, `Board review scheduled daily at ${pad(runAtHour)}:${pad(runAtMinute)} (next run in ${Math.round(delay / 60000)}m)`);
2594
+ log6.info(TAG6, `Board review scheduled daily at ${pad(runAtHour)}:${pad(runAtMinute)} (next run in ${Math.round(delay / 60000)}m)`);
2415
2595
  this.scheduleNext(delay);
2416
2596
  }
2417
2597
  stop() {
@@ -2420,7 +2600,7 @@ class BoardReviewer {
2420
2600
  clearTimeout(this.timer);
2421
2601
  this.timer = null;
2422
2602
  }
2423
- log5.info(TAG5, "Board review stopped");
2603
+ log6.info(TAG6, "Board review stopped");
2424
2604
  }
2425
2605
  async runOnce() {
2426
2606
  await this.tick();
@@ -2451,18 +2631,18 @@ class BoardReviewer {
2451
2631
  config: cfg
2452
2632
  });
2453
2633
  if (digest.totalFindings === 0) {
2454
- log5.info(TAG5, "Board review: no findings — skipping digest");
2634
+ log6.info(TAG6, "Board review: no findings — skipping digest");
2455
2635
  return;
2456
2636
  }
2457
2637
  const title = digestTitleForDate(new Date(this.now()));
2458
2638
  const existing = cards.find((c) => !c.archived_at && c.title === title);
2459
2639
  if (existing) {
2460
- log5.info(TAG5, `Digest for today already exists (#${existing.short_id}) — skipping`);
2640
+ log6.info(TAG6, `Digest for today already exists (#${existing.short_id}) — skipping`);
2461
2641
  return;
2462
2642
  }
2463
2643
  const column = this.resolveDigestColumn(columns);
2464
2644
  if (!column) {
2465
- log5.warn(TAG5, "No board column resolved for the digest — skipping");
2645
+ log6.warn(TAG6, "No board column resolved for the digest — skipping");
2466
2646
  return;
2467
2647
  }
2468
2648
  const body = renderBoardReviewDigest(digest, { config: cfg });
@@ -2472,9 +2652,9 @@ class BoardReviewer {
2472
2652
  description: body,
2473
2653
  priority: cfg.digestPriority
2474
2654
  });
2475
- log5.info(TAG5, `Posted board-review digest to "${column.name}": ${digest.totalFindings} finding(s) across ${digest.flaggedCardCount} card(s)`);
2655
+ log6.info(TAG6, `Posted board-review digest to "${column.name}": ${digest.totalFindings} finding(s) across ${digest.flaggedCardCount} card(s)`);
2476
2656
  } catch (err) {
2477
- log5.error(TAG5, `Board review tick failed: ${err instanceof Error ? err.message : err}`);
2657
+ log6.error(TAG6, `Board review tick failed: ${err instanceof Error ? err.message : err}`);
2478
2658
  }
2479
2659
  }
2480
2660
  resolveDigestColumn(columns) {
@@ -2489,7 +2669,7 @@ class BoardReviewer {
2489
2669
  const found = byName(configured);
2490
2670
  if (found)
2491
2671
  return found;
2492
- log5.warn(TAG5, `Configured digestColumn "${configured}" not found — falling back`);
2672
+ log6.warn(TAG6, `Configured digestColumn "${configured}" not found — falling back`);
2493
2673
  }
2494
2674
  const firstPickup = this.config.pickupColumns[0];
2495
2675
  if (firstPickup) {
@@ -2503,7 +2683,7 @@ class BoardReviewer {
2503
2683
  function pad(n) {
2504
2684
  return String(n).padStart(2, "0");
2505
2685
  }
2506
- var TAG5 = "board-review";
2686
+ var TAG6 = "board-review";
2507
2687
  var init_board_reviewer = __esm(() => {
2508
2688
  init_board_review();
2509
2689
  });
@@ -3839,7 +4019,7 @@ var init_stream_drafts = __esm(() => {
3839
4019
 
3840
4020
  // src/stream-parser.ts
3841
4021
  import { EventEmitter } from "node:events";
3842
- import { log as log6 } from "@gethmy/harness";
4022
+ import { log as log7 } from "@gethmy/harness";
3843
4023
  function normalizeToolResultContent(raw) {
3844
4024
  if (raw == null)
3845
4025
  return;
@@ -3860,7 +4040,7 @@ function normalizeToolResultContent(raw) {
3860
4040
  return String(raw);
3861
4041
  }
3862
4042
  }
3863
- var TAG6 = "stream-parser", StreamParser;
4043
+ var TAG7 = "stream-parser", StreamParser;
3864
4044
  var init_stream_parser = __esm(() => {
3865
4045
  StreamParser = class StreamParser extends EventEmitter {
3866
4046
  buffer = "";
@@ -3907,14 +4087,14 @@ var init_stream_parser = __esm(() => {
3907
4087
  try {
3908
4088
  msg = JSON.parse(line);
3909
4089
  } catch {
3910
- log6.debug(TAG6, `Non-JSON line: ${line.slice(0, 100)}`);
4090
+ log7.debug(TAG7, `Non-JSON line: ${line.slice(0, 100)}`);
3911
4091
  return;
3912
4092
  }
3913
4093
  try {
3914
4094
  this.handleMessage(msg);
3915
4095
  } catch (err) {
3916
4096
  const errMsg = err instanceof Error ? err.message : String(err);
3917
- log6.warn(TAG6, `Error handling stream event: ${errMsg}`);
4097
+ log7.warn(TAG7, `Error handling stream event: ${errMsg}`);
3918
4098
  this.emit("parse_error", errMsg);
3919
4099
  }
3920
4100
  }
@@ -4705,7 +4885,7 @@ __export(exports_config, {
4705
4885
  });
4706
4886
  import { execSync } from "node:child_process";
4707
4887
  import { readFileSync as readFileSync2 } from "node:fs";
4708
- import { log as log7 } from "@gethmy/harness";
4888
+ import { log as log8 } from "@gethmy/harness";
4709
4889
  import { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
4710
4890
  import {
4711
4891
  findLocalConfigPath as findLocalConfigPath2,
@@ -4727,7 +4907,7 @@ function readConfigFile() {
4727
4907
  const raw = readFileSync2(legacyConfigPath(), "utf-8");
4728
4908
  if (!warnedLegacyConfigDir) {
4729
4909
  warnedLegacyConfigDir = true;
4730
- log7.warn(TAG7, `Read the pre-#1082 config at ${legacyConfigPath()}. The current ` + `location is ${configPath()} — the daemon migrates it on its next ` + `\`run\`, and this fallback is temporary.`);
4910
+ log8.warn(TAG8, `Read the pre-#1082 config at ${legacyConfigPath()}. The current ` + `location is ${configPath()} — the daemon migrates it on its next ` + `\`run\`, and this fallback is temporary.`);
4731
4911
  }
4732
4912
  return raw;
4733
4913
  }
@@ -4876,7 +5056,7 @@ function createApiClient(config) {
4876
5056
  refreshCredential: refreshOAuthToken
4877
5057
  });
4878
5058
  }
4879
- var TAG7 = "config", NOT_A_REPOSITORY_EXIT = 128, warnedLegacyConfigDir = false;
5059
+ var TAG8 = "config", NOT_A_REPOSITORY_EXIT = 128, warnedLegacyConfigDir = false;
4880
5060
  var init_config = __esm(() => {
4881
5061
  init_src();
4882
5062
  init_board_port();
@@ -4898,7 +5078,7 @@ import {
4898
5078
  statSync
4899
5079
  } from "node:fs";
4900
5080
  import { join as join2 } from "node:path";
4901
- import { log as log8 } from "@gethmy/harness";
5081
+ import { log as log9 } from "@gethmy/harness";
4902
5082
  function migrateConfigDir(opts = {}) {
4903
5083
  const from = opts.from ?? legacyConfigDir();
4904
5084
  const to = opts.to ?? configDir();
@@ -4909,12 +5089,12 @@ function migrateConfigDir(opts = {}) {
4909
5089
  try {
4910
5090
  if (!statSync(from).isDirectory()) {
4911
5091
  const error = `${from} is not a directory`;
4912
- log8.warn(TAG8, `Refusing to migrate: ${error}. The daemon keeps reading the new ` + `location; move or delete that file by hand.`);
5092
+ log9.warn(TAG9, `Refusing to migrate: ${error}. The daemon keeps reading the new ` + `location; move or delete that file by hand.`);
4913
5093
  return { outcome: "failed", ...empty, error };
4914
5094
  }
4915
5095
  } catch (err) {
4916
5096
  const error = err instanceof Error ? err.message : String(err);
4917
- log8.warn(TAG8, `Could not inspect ${from}: ${error}`);
5097
+ log9.warn(TAG9, `Could not inspect ${from}: ${error}`);
4918
5098
  return { outcome: "failed", ...empty, error };
4919
5099
  }
4920
5100
  const moved = [];
@@ -4923,18 +5103,18 @@ function migrateConfigDir(opts = {}) {
4923
5103
  if (!existsSync(to)) {
4924
5104
  mkdirSync2(join2(to, ".."), { recursive: true });
4925
5105
  renameSync(from, to);
4926
- log8.info(TAG8, `Moved ${from} to ${to}`);
5106
+ log9.info(TAG9, `Moved ${from} to ${to}`);
4927
5107
  return { outcome: "moved", ...empty };
4928
5108
  }
4929
5109
  mergeDirInto(from, to, "", moved, skipped, 0);
4930
5110
  if (moved.length === 0 && skipped.length === 0) {
4931
5111
  return { outcome: "nothing-to-do", moved, skipped };
4932
5112
  }
4933
- log8.info(TAG8, `Merged ${from} into ${to}: ${moved.length} moved` + (skipped.length > 0 ? `, ${skipped.length} left in place (already present at the target): ${nameSample(skipped)}` : ""));
5113
+ log9.info(TAG9, `Merged ${from} into ${to}: ${moved.length} moved` + (skipped.length > 0 ? `, ${skipped.length} left in place (already present at the target): ${nameSample(skipped)}` : ""));
4934
5114
  return { outcome: "merged", moved, skipped };
4935
5115
  } catch (err) {
4936
5116
  const error = err instanceof Error ? err.message : String(err);
4937
- log8.warn(TAG8, `Could not migrate ${from} to ${to}: ${error}.` + (moved.length > 0 ? ` ${moved.length} entr${moved.length === 1 ? "y" : "ies"} already moved: ${nameSample(moved)}. Re-run to finish; nothing is deleted.` : ` Nothing was moved; the daemon reads the old config through the fallback.`));
5117
+ log9.warn(TAG9, `Could not migrate ${from} to ${to}: ${error}.` + (moved.length > 0 ? ` ${moved.length} entr${moved.length === 1 ? "y" : "ies"} already moved: ${nameSample(moved)}. Re-run to finish; nothing is deleted.` : ` Nothing was moved; the daemon reads the old config through the fallback.`));
4938
5118
  return { outcome: "failed", moved, skipped, error };
4939
5119
  }
4940
5120
  }
@@ -5003,7 +5183,7 @@ function nameSample(paths, limit = 10) {
5003
5183
  return paths.join(", ");
5004
5184
  return `${paths.slice(0, limit).join(", ")} … and ${paths.length - limit} more`;
5005
5185
  }
5006
- var TAG8 = "config-migration", MAX_MERGE_DEPTH = 32;
5186
+ var TAG9 = "config-migration", MAX_MERGE_DEPTH = 32;
5007
5187
  var init_config_dir_migration = __esm(() => {
5008
5188
  init_paths();
5009
5189
  });
@@ -5099,7 +5279,7 @@ var init_control_origin = __esm(() => {
5099
5279
  import {
5100
5280
  createServer
5101
5281
  } from "node:http";
5102
- import { log as log9 } from "@gethmy/harness";
5282
+ import { log as log10 } from "@gethmy/harness";
5103
5283
 
5104
5284
  class HttpServer {
5105
5285
  opts;
@@ -5118,7 +5298,7 @@ class HttpServer {
5118
5298
  }
5119
5299
  this.server = createServer((req, res) => {
5120
5300
  this.route(req, res).catch((err) => {
5121
- log9.error(TAG9, `unhandled: ${err instanceof Error ? err.message : err}`);
5301
+ log10.error(TAG10, `unhandled: ${err instanceof Error ? err.message : err}`);
5122
5302
  if (!res.headersSent) {
5123
5303
  res.writeHead(500, { "content-type": "application/json" });
5124
5304
  res.end(JSON.stringify({ error: "internal_error" }));
@@ -5133,13 +5313,13 @@ class HttpServer {
5133
5313
  await this.listenOnce(port);
5134
5314
  this.boundPort = port;
5135
5315
  if (port !== startPort) {
5136
- log9.info(TAG9, `port ${startPort} busy — bound to ${port} instead`);
5316
+ log10.info(TAG10, `port ${startPort} busy — bound to ${port} instead`);
5137
5317
  }
5138
5318
  return port;
5139
5319
  } catch (err) {
5140
5320
  const lastAttempt = i === attempts - 1;
5141
5321
  if (isAddrInUse(err) && !lastAttempt) {
5142
- log9.debug(TAG9, `port ${port} in use, trying ${port + 1}`);
5322
+ log10.debug(TAG10, `port ${port} in use, trying ${port + 1}`);
5143
5323
  continue;
5144
5324
  }
5145
5325
  throw err;
@@ -5226,11 +5406,11 @@ class HttpServer {
5226
5406
  logRefusal(reason, detail, method, path) {
5227
5407
  const line = `refused ${method} ${path}: ${detail} (#1071)`;
5228
5408
  if (this.lastRefusalReason === reason) {
5229
- log9.debug(TAG9, line);
5409
+ log10.debug(TAG10, line);
5230
5410
  return;
5231
5411
  }
5232
5412
  this.lastRefusalReason = reason;
5233
- log9.warn(TAG9, line);
5413
+ log10.warn(TAG10, line);
5234
5414
  }
5235
5415
  respondHealth(res) {
5236
5416
  const health = this.opts.getHealth();
@@ -5355,7 +5535,7 @@ function parseSweepCommand(path) {
5355
5535
  function busy(w) {
5356
5536
  return w.cardId !== null;
5357
5537
  }
5358
- var TAG9 = "http";
5538
+ var TAG10 = "http";
5359
5539
  var init_http_server = __esm(() => {
5360
5540
  init_config_validation();
5361
5541
  init_control_origin();
@@ -5393,7 +5573,7 @@ import {
5393
5573
  } from "node:fs";
5394
5574
  import { tmpdir } from "node:os";
5395
5575
  import { dirname, join as join3, relative, sep } from "node:path";
5396
- import { GIT_NO_HOOKS as GIT_NO_HOOKS2, log as log10 } from "@gethmy/harness";
5576
+ import { GIT_NO_HOOKS as GIT_NO_HOOKS2, log as log11 } from "@gethmy/harness";
5397
5577
  function extractScratchTrees(cleanWorktree, commitish) {
5398
5578
  const base = mkdtempSync(join3(tmpdir(), "harmony-repair-"));
5399
5579
  const scratch = join3(base, "scratch");
@@ -5497,7 +5677,7 @@ function applyChanges(args) {
5497
5677
  try {
5498
5678
  unlinkSync(safeDestination(clean, path));
5499
5679
  } catch {
5500
- log10.warn(TAG10, `could not remove ${path} from the clean tree`);
5680
+ log11.warn(TAG11, `could not remove ${path} from the clean tree`);
5501
5681
  }
5502
5682
  }
5503
5683
  }
@@ -5524,7 +5704,7 @@ function safeDestination(clean, path) {
5524
5704
  }
5525
5705
  return to;
5526
5706
  }
5527
- var TAG10 = "ci-patch-trees", MAX_CHANGED_FILES = 200, MAX_FILE_BYTES;
5707
+ var TAG11 = "ci-patch-trees", MAX_CHANGED_FILES = 200, MAX_FILE_BYTES;
5528
5708
  var init_ci_patch_trees = __esm(() => {
5529
5709
  MAX_FILE_BYTES = 8 * 1024 * 1024;
5530
5710
  });
@@ -5540,7 +5720,7 @@ import {
5540
5720
  GIT_NO_HOOKS as GIT_NO_HOOKS3,
5541
5721
  installCommand,
5542
5722
  isPrUrlOnOrigin,
5543
- log as log11,
5723
+ log as log12,
5544
5724
  removeWorktreeHoldingBranch,
5545
5725
  resolvePrHeadBranch,
5546
5726
  reviewWorktreePath,
@@ -5562,7 +5742,7 @@ function checkoutExistingBranch(basePath, branchName, opts = {}) {
5562
5742
  }).trim();
5563
5743
  const worktreeDir = reviewWorktreePath(repoRoot, basePath, branchName);
5564
5744
  if (existsSync2(worktreeDir)) {
5565
- log11.warn(TAG11, `Review worktree already exists at ${worktreeDir}, cleaning up`);
5745
+ log12.warn(TAG12, `Review worktree already exists at ${worktreeDir}, cleaning up`);
5566
5746
  cleanupWorktree(worktreeDir);
5567
5747
  }
5568
5748
  try {
@@ -5586,7 +5766,7 @@ function checkoutExistingBranch(basePath, branchName, opts = {}) {
5586
5766
  stdio: "pipe"
5587
5767
  });
5588
5768
  } catch {}
5589
- log11.info(TAG11, `Creating review worktree: ${worktreeDir} (branch: ${branchName})`);
5769
+ log12.info(TAG12, `Creating review worktree: ${worktreeDir} (branch: ${branchName})`);
5590
5770
  try {
5591
5771
  execFileSync3("git", [
5592
5772
  ...GIT_NO_HOOKS3,
@@ -5603,7 +5783,7 @@ function checkoutExistingBranch(basePath, branchName, opts = {}) {
5603
5783
  }
5604
5784
  if (!shouldInstall(worktreeDir, opts.commands))
5605
5785
  return worktreeDir;
5606
- log11.info(TAG11, "Installing dependencies in review worktree...");
5786
+ log12.info(TAG12, "Installing dependencies in review worktree...");
5607
5787
  try {
5608
5788
  execSync2(installCommand(opts.ignoreScripts !== false), {
5609
5789
  cwd: worktreeDir,
@@ -5612,14 +5792,14 @@ function checkoutExistingBranch(basePath, branchName, opts = {}) {
5612
5792
  env: containedEnv()
5613
5793
  });
5614
5794
  } catch {
5615
- log11.warn(TAG11, "Install failed (may be fine if deps are hoisted)");
5795
+ log12.warn(TAG12, "Install failed (may be fine if deps are hoisted)");
5616
5796
  }
5617
5797
  return worktreeDir;
5618
5798
  }
5619
5799
  function extractBranchFromDescription(description) {
5620
5800
  const branch = extractBranchRef(description);
5621
5801
  if (!branch && hasUnsafeDaemonBranchLine(description)) {
5622
- log11.warn(TAG11, "Daemon Branch: line contains unsafe characters; ignoring it");
5802
+ log12.warn(TAG12, "Daemon Branch: line contains unsafe characters; ignoring it");
5623
5803
  }
5624
5804
  return branch;
5625
5805
  }
@@ -5711,7 +5891,7 @@ function describeReviewTreeChange(worktree, before) {
5711
5891
  `).filter((line) => line && !beforeLines.has(line)).slice(0, 10);
5712
5892
  return `The review run left the worktree changed (${added.join(", ") || "a change it had was undone"}).`;
5713
5893
  }
5714
- var TAG11 = "review-worktree";
5894
+ var TAG12 = "review-worktree";
5715
5895
  var init_review_worktree = __esm(() => {
5716
5896
  init_src();
5717
5897
  });
@@ -5728,7 +5908,7 @@ import {
5728
5908
  confineToRepo,
5729
5909
  GIT_NO_HOOKS as GIT_NO_HOOKS4,
5730
5910
  HARMONY_CREDENTIAL_KEYS,
5731
- log as log12,
5911
+ log as log13,
5732
5912
  REPO_CONFIG_FILENAMES,
5733
5913
  runInSandbox,
5734
5914
  SdkAgentRunner as SdkAgentRunner2,
@@ -5807,7 +5987,7 @@ async function runRepairSpawn(args) {
5807
5987
  for await (const ev of runner.start(input)) {
5808
5988
  if (ev.kind === "error") {
5809
5989
  errored = true;
5810
- log12.warn(TAG12, `repair spawn error: ${ev.payload.message}`);
5990
+ log13.warn(TAG13, `repair spawn error: ${ev.payload.message}`);
5811
5991
  }
5812
5992
  }
5813
5993
  return { errored };
@@ -5817,7 +5997,7 @@ async function verifyInSandbox(worktree, config) {
5817
5997
  if (!patch.sandboxImage)
5818
5998
  return null;
5819
5999
  if (!await sandboxAvailable()) {
5820
- log12.info(TAG12, `sandboxImage is set to "${patch.sandboxImage}" but no container runtime answered — falling back to CI as the verifier`);
6000
+ log13.info(TAG13, `sandboxImage is set to "${patch.sandboxImage}" but no container runtime answered — falling back to CI as the verifier`);
5821
6001
  return null;
5822
6002
  }
5823
6003
  let ran = null;
@@ -5838,7 +6018,7 @@ async function verifyInSandbox(worktree, config) {
5838
6018
  return { result, command };
5839
6019
  }
5840
6020
  if (!ran) {
5841
- log12.info(TAG12, "no build or test command resolves in this repo — nothing to verify in the sandbox, falling back to CI as the verifier");
6021
+ log13.info(TAG13, "no build or test command resolves in this repo — nothing to verify in the sandbox, falling back to CI as the verifier");
5842
6022
  return null;
5843
6023
  }
5844
6024
  return { result: { passed: true, output: "" }, command: ran };
@@ -5910,7 +6090,7 @@ async function repairBranch(deps) {
5910
6090
  config
5911
6091
  });
5912
6092
  if (errored) {
5913
- log12.warn(TAG12, `#${card.short_id} repair spawn reported an error frame`);
6093
+ log13.warn(TAG13, `#${card.short_id} repair spawn reported an error frame`);
5914
6094
  }
5915
6095
  const changes = diffTrees(trees.pristine, trees.scratch);
5916
6096
  const changed = changedPaths(changes);
@@ -5922,12 +6102,12 @@ async function repairBranch(deps) {
5922
6102
  }
5923
6103
  const rejected = rejectChangeSet(changes);
5924
6104
  if (rejected) {
5925
- log12.warn(TAG12, `#${card.short_id} abandoning repair: ${rejected}`);
6105
+ log13.warn(TAG13, `#${card.short_id} abandoning repair: ${rejected}`);
5926
6106
  return { kind: "no_change", detail: `${rejected} — nothing pushed` };
5927
6107
  }
5928
6108
  const touched = buildExecutedChanges(changed);
5929
6109
  if (touched.length > 0) {
5930
- log12.warn(TAG12, `#${card.short_id} abandoning repair: it modified build-executed file(s) ${touched.join(", ")}`);
6110
+ log13.warn(TAG13, `#${card.short_id} abandoning repair: it modified build-executed file(s) ${touched.join(", ")}`);
5931
6111
  return {
5932
6112
  kind: "no_change",
5933
6113
  detail: `the repair modified files that decide what gets executed (${touched.join(", ")}) — ` + "abandoned without copying, verifying or pushing, because verification and CI would have run them"
@@ -5943,7 +6123,7 @@ async function repairBranch(deps) {
5943
6123
  const gitPointer = readGitPointer(worktree);
5944
6124
  const verified = await verifyInSandbox(worktree, config);
5945
6125
  if (readGitPointer(worktree) !== gitPointer) {
5946
- log12.warn(TAG12, `#${card.short_id} the sandbox altered the worktree's .git pointer — abandoning without running git in it`);
6126
+ log13.warn(TAG13, `#${card.short_id} the sandbox altered the worktree's .git pointer — abandoning without running git in it`);
5947
6127
  return {
5948
6128
  kind: "no_change",
5949
6129
  detail: "the sandboxed verification rewrote the worktree's git metadata, which would have " + "redirected the next git command — the repair was abandoned and nothing was pushed"
@@ -5988,7 +6168,7 @@ async function repairBranch(deps) {
5988
6168
  }
5989
6169
  }
5990
6170
  }
5991
- var TAG12 = "ci-patch", MAX_BRANCH_IN_PROMPT = 200, MAX_PR_URL_IN_PROMPT = 300, MAX_CHECK_NAME_IN_PROMPT = 200, MAX_CONCLUSION_IN_PROMPT = 40, MAX_LOG_IN_PROMPT = 64000, REPAIR_TOOLS, REPAIR_DENY, BUILD_EXECUTED_PATHS;
6171
+ var TAG13 = "ci-patch", MAX_BRANCH_IN_PROMPT = 200, MAX_PR_URL_IN_PROMPT = 300, MAX_CHECK_NAME_IN_PROMPT = 200, MAX_CONCLUSION_IN_PROMPT = 40, MAX_LOG_IN_PROMPT = 64000, REPAIR_TOOLS, REPAIR_DENY, BUILD_EXECUTED_PATHS;
5992
6172
  var init_ci_patch = __esm(() => {
5993
6173
  init_ci_patch_trees();
5994
6174
  init_review_worktree();
@@ -6034,7 +6214,7 @@ var init_ci_patch = __esm(() => {
6034
6214
  });
6035
6215
 
6036
6216
  // src/handback.ts
6037
- import { log as log13 } from "@gethmy/harness";
6217
+ import { log as log14 } from "@gethmy/harness";
6038
6218
  function assessHandback(card, expect) {
6039
6219
  if (card.archived_at) {
6040
6220
  return {
@@ -6088,7 +6268,7 @@ async function guardedHandback(client, cardId, expect) {
6088
6268
  ({ card } = await client.getCard(cardId));
6089
6269
  } catch (err) {
6090
6270
  const detail = err instanceof Error ? err.message : String(err);
6091
- log13.warn(TAG13, `could not re-read ${cardId} before handing it back: ${detail} — leaving the board alone`);
6271
+ log14.warn(TAG14, `could not re-read ${cardId} before handing it back: ${detail} — leaving the board alone`);
6092
6272
  return {
6093
6273
  verdict: { proceed: false, reason: "unreadable", detail },
6094
6274
  card: null
@@ -6096,11 +6276,11 @@ async function guardedHandback(client, cardId, expect) {
6096
6276
  }
6097
6277
  const verdict = assessHandback(card, expect);
6098
6278
  if (!verdict.proceed) {
6099
- log13.info(TAG13, `#${card.short_id}: not handing the card back — ${verdict.detail} (${verdict.reason})`);
6279
+ log14.info(TAG14, `#${card.short_id}: not handing the card back — ${verdict.detail} (${verdict.reason})`);
6100
6280
  }
6101
6281
  return { verdict, card };
6102
6282
  }
6103
- var TAG13 = "handback";
6283
+ var TAG14 = "handback";
6104
6284
  var init_handback = () => {};
6105
6285
 
6106
6286
  // src/ci-repair.ts
@@ -6110,7 +6290,7 @@ import {
6110
6290
  getFailedRunLog,
6111
6291
  getPrFailedChecks,
6112
6292
  isPrOnOrigin,
6113
- log as log14,
6293
+ log as log15,
6114
6294
  rerunFailedJobs,
6115
6295
  resolvePrHeadBranch as resolvePrHeadBranch2
6116
6296
  } from "@gethmy/harness";
@@ -6175,7 +6355,7 @@ async function attemptCiRepair(deps) {
6175
6355
  };
6176
6356
  }
6177
6357
  if (!isPrOnOrigin(prUrl, cwd)) {
6178
- log14.warn(TAG14, `#${card.short_id} refusing: ${prUrl} is not a pull request on this daemon's own origin`);
6358
+ log15.warn(TAG15, `#${card.short_id} refusing: ${prUrl} is not a pull request on this daemon's own origin`);
6179
6359
  return {
6180
6360
  kind: "skipped",
6181
6361
  detail: "the card's PR link does not point at this repository's origin — refusing to read CI output from a foreign pull request",
@@ -6205,11 +6385,11 @@ async function attemptCiRepair(deps) {
6205
6385
  patchEnabled: repairConfig.patch?.enabled === true
6206
6386
  });
6207
6387
  if (decision.action === "park") {
6208
- log14.warn(TAG14, `#${card.short_id} ${decision.reason}`);
6388
+ log15.warn(TAG15, `#${card.short_id} ${decision.reason}`);
6209
6389
  try {
6210
6390
  await stateStore.markCiRepairParked(card.id);
6211
6391
  } catch (err) {
6212
- log14.warn(TAG14, `#${card.short_id} could not latch the park (retrying next tick): ${errText(err)}`);
6392
+ log15.warn(TAG15, `#${card.short_id} could not latch the park (retrying next tick): ${errText(err)}`);
6213
6393
  }
6214
6394
  await recordAttempt(deps, {
6215
6395
  outcome: "parked",
@@ -6226,7 +6406,7 @@ async function attemptCiRepair(deps) {
6226
6406
  } catch (err) {
6227
6407
  return skip(card, `could not record the attempt: ${errText(err)}`);
6228
6408
  }
6229
- log14.info(TAG14, `#${card.short_id} attempt ${attempt}/${repairConfig.maxAttempts}: ${decision.reason}`);
6409
+ log15.info(TAG15, `#${card.short_id} attempt ${attempt}/${repairConfig.maxAttempts}: ${decision.reason}`);
6230
6410
  let outcome;
6231
6411
  switch (decision.action) {
6232
6412
  case "rerun":
@@ -6331,7 +6511,7 @@ async function recordAttempt(deps, input) {
6331
6511
  ]
6332
6512
  });
6333
6513
  } catch (err) {
6334
- log14.warn(TAG14, `#${card.short_id} could not record the attempt on the board: ${errText(err)}`);
6514
+ log15.warn(TAG15, `#${card.short_id} could not record the attempt on the board: ${errText(err)}`);
6335
6515
  } finally {
6336
6516
  if (createdSession) {
6337
6517
  try {
@@ -6341,158 +6521,18 @@ async function recordAttempt(deps, input) {
6341
6521
  }
6342
6522
  }
6343
6523
  function skip(card, detail) {
6344
- log14.info(TAG14, `#${card.short_id} not acting — ${detail}`);
6524
+ log15.info(TAG15, `#${card.short_id} not acting — ${detail}`);
6345
6525
  return { kind: "skipped", detail };
6346
6526
  }
6347
6527
  function errText(err) {
6348
6528
  return err instanceof Error ? err.message : String(err);
6349
6529
  }
6350
- var TAG14 = "ci-repair", MAX_CLASSIFIED_RUNS = 3;
6530
+ var TAG15 = "ci-repair", MAX_CLASSIFIED_RUNS = 3;
6351
6531
  var init_ci_repair = __esm(() => {
6352
6532
  init_ci_patch();
6353
6533
  init_handback();
6354
6534
  });
6355
6535
 
6356
- // src/human-paths.ts
6357
- import { execFile } from "node:child_process";
6358
- import { promisify } from "node:util";
6359
- import { log as log15 } from "@gethmy/harness";
6360
- async function readPrChangedFiles(prUrl, cwd, run = defaultRun) {
6361
- const match = PR_URL_RE.exec(prUrl);
6362
- if (!match)
6363
- return { ok: false, reason: "the pull request url is not valid" };
6364
- const [, owner, repo, number] = match;
6365
- try {
6366
- const countOut = await run(["pr", "view", prUrl, "--json", "changedFiles"], cwd);
6367
- const expected = JSON.parse(countOut.trim()).changedFiles;
6368
- if (typeof expected !== "number" || !Number.isInteger(expected)) {
6369
- return { ok: false, reason: "gh did not report a changed-file count" };
6370
- }
6371
- const out = await run([
6372
- "api",
6373
- `repos/${owner}/${repo}/pulls/${number}/files?per_page=100`,
6374
- "--paginate",
6375
- "--slurp"
6376
- ], cwd);
6377
- const parsed = JSON.parse(out.trim());
6378
- const entries = (Array.isArray(parsed) ? parsed : [parsed]).flatMap((page) => Array.isArray(page) ? page : [page]);
6379
- const files = new Set;
6380
- for (const entry of entries) {
6381
- if (typeof entry?.filename !== "string" || entry.filename === "") {
6382
- return { ok: false, reason: "the file list holds an unreadable entry" };
6383
- }
6384
- files.add(entry.filename);
6385
- if (typeof entry.previous_filename === "string") {
6386
- files.add(entry.previous_filename);
6387
- }
6388
- }
6389
- if (entries.length !== expected) {
6390
- return {
6391
- ok: false,
6392
- reason: `read ${entries.length} files but the pull request changes ${expected}`
6393
- };
6394
- }
6395
- return { ok: true, files: [...files] };
6396
- } catch (err) {
6397
- return {
6398
- ok: false,
6399
- reason: `gh failed: ${err instanceof Error ? err.message : String(err)}`
6400
- };
6401
- }
6402
- }
6403
- function tokenize(glob) {
6404
- const tokens2 = [];
6405
- for (let i = 0;i < glob.length; i++) {
6406
- const c = glob[i];
6407
- if (c === "*" && glob[i + 1] === "*") {
6408
- while (glob[i + 1] === "*")
6409
- i++;
6410
- if (glob[i + 1] === "/") {
6411
- i++;
6412
- tokens2.push({ kind: "dirs" });
6413
- } else {
6414
- tokens2.push({ kind: "globstar" });
6415
- }
6416
- } else if (c === "*") {
6417
- tokens2.push({ kind: "star" });
6418
- } else if (c === "?") {
6419
- tokens2.push({ kind: "one" });
6420
- } else {
6421
- tokens2.push({ kind: "lit", char: c });
6422
- }
6423
- }
6424
- return tokens2;
6425
- }
6426
- function matchesGlob(glob, path) {
6427
- const text = path.toLowerCase();
6428
- const pattern = glob.toLowerCase();
6429
- if (pattern.endsWith("/**") && text === pattern.slice(0, -3))
6430
- return true;
6431
- const tokens2 = tokenize(pattern);
6432
- const n = text.length;
6433
- let next = new Uint8Array(n + 2);
6434
- next[n] = 1;
6435
- for (let ti = tokens2.length - 1;ti >= 0; ti--) {
6436
- const token = tokens2[ti];
6437
- const cur = new Uint8Array(n + 2);
6438
- let inner = 0;
6439
- for (let pi = n;pi >= 0; pi--) {
6440
- const ch = pi < n ? text[pi] : "";
6441
- if (token.kind === "dirs") {
6442
- if (pi < n)
6443
- inner = ch === "/" && next[pi + 1] ? 1 : inner;
6444
- cur[pi] = next[pi] || inner ? 1 : 0;
6445
- continue;
6446
- }
6447
- switch (token.kind) {
6448
- case "lit":
6449
- cur[pi] = pi < n && ch === token.char && next[pi + 1] ? 1 : 0;
6450
- break;
6451
- case "one":
6452
- cur[pi] = pi < n && ch !== "/" && next[pi + 1] ? 1 : 0;
6453
- break;
6454
- case "star":
6455
- cur[pi] = next[pi] || pi < n && ch !== "/" && cur[pi + 1] ? 1 : 0;
6456
- break;
6457
- case "globstar":
6458
- cur[pi] = next[pi] || pi < n && cur[pi + 1] ? 1 : 0;
6459
- break;
6460
- }
6461
- }
6462
- next = cur;
6463
- }
6464
- return next[0] === 1;
6465
- }
6466
- function checkHumanPaths(read, globs) {
6467
- if (!read.ok)
6468
- return { kind: "unreadable", reason: read.reason };
6469
- for (const path of read.files) {
6470
- const glob = globs.find((g) => matchesGlob(g, path));
6471
- if (glob !== undefined)
6472
- return { kind: "held", path, glob };
6473
- }
6474
- return { kind: "clear" };
6475
- }
6476
- async function evaluateHumanPaths(prUrl, cwd, globs, read = readPrChangedFiles) {
6477
- const check = checkHumanPaths(await read(prUrl, cwd), globs);
6478
- if (check.kind === "unreadable") {
6479
- log15.warn(TAG15, `${prUrl}: ${check.reason}, so the sensitive-path check cannot run`);
6480
- }
6481
- return check;
6482
- }
6483
- var TAG15 = "human-paths", PR_URL_RE, defaultRun = async (args, cwd) => {
6484
- const { stdout } = await promisify(execFile)("gh", args, {
6485
- cwd,
6486
- encoding: "utf-8",
6487
- timeout: 60000,
6488
- maxBuffer: 16 * 1024 * 1024
6489
- });
6490
- return stdout;
6491
- };
6492
- var init_human_paths = __esm(() => {
6493
- PR_URL_RE = /^https:\/\/github\.com\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)\/pull\/(\d+)$/;
6494
- });
6495
-
6496
6536
  // src/park-reasons.ts
6497
6537
  import { log as log16 } from "@gethmy/harness";
6498
6538
  function stampablePath(path) {
@@ -6627,6 +6667,7 @@ import {
6627
6667
  isPrOnOrigin as isPrOnOrigin2,
6628
6668
  log as log17,
6629
6669
  mergePullRequest,
6670
+ PullRequestHeadMovedError,
6630
6671
  readBlockingRoundCount,
6631
6672
  readHeadCommitTime,
6632
6673
  readPrFindings,
@@ -6790,7 +6831,17 @@ async function attemptAutoMerge(deps) {
6790
6831
  blockingVerdictRounds = await countBlockingReviewVerdicts(prUrl, cwd, provider, independent.verdict.checkName);
6791
6832
  }
6792
6833
  const headCommitTime = countRounds ? await readHeadCommitTime(prUrl, cwd, provider) : null;
6793
- const humanPathsCheck = decisionConfig.humanPaths.length > 0 ? await evaluateHumanPaths(prUrl, cwd, decisionConfig.humanPaths) : undefined;
6834
+ let humanPathsCheck = decisionConfig.humanPaths.length > 0 ? await evaluateHumanPaths(prUrl, cwd, decisionConfig.humanPaths) : undefined;
6835
+ if (humanPathsCheck?.kind === "held" && driver === "interactive" && deps.closedLaneApproval !== undefined) {
6836
+ const approved = deps.closedLaneApproval.toLowerCase();
6837
+ if (headSha !== null && headSha.toLowerCase() === approved && headSha === deps.localHeadSha) {
6838
+ log17.info(TAG17, `#${card.short_id} closed-lane approval for ${headSha} — the person who ran this command read the change, so the hold on ${humanPathsCheck.path} is lifted for this head only`);
6839
+ deps.onClosedLaneApproved?.(headSha, humanPathsCheck.paths ?? [humanPathsCheck.path]);
6840
+ humanPathsCheck = { kind: "clear" };
6841
+ } else {
6842
+ log17.info(TAG17, `#${card.short_id} closed-lane approval is for ${deps.closedLaneApproval}, and the pull request's head is ${headSha ?? "unreadable"} — the hold stays. A new push needs a new approval.`);
6843
+ }
6844
+ }
6794
6845
  if (driver === "daemon") {
6795
6846
  await clearParkStamps(client, card, staleParkReasons({
6796
6847
  ciStatus,
@@ -6844,7 +6895,7 @@ async function attemptAutoMerge(deps) {
6844
6895
  if (driver === "daemon") {
6845
6896
  await park("human-paths", humanPathsCheck.path);
6846
6897
  } else {
6847
- deps.onHumanPathHeld?.(humanPathsCheck.path);
6898
+ deps.onHumanPathHeld?.(humanPathsCheck.path, humanPathsCheck.paths ?? [humanPathsCheck.path]);
6848
6899
  }
6849
6900
  }
6850
6901
  return action;
@@ -6932,8 +6983,19 @@ async function attemptAutoMerge(deps) {
6932
6983
  await removeApprovedLabel(client, card, resolvedLabels, config.review.approvedLabel);
6933
6984
  return action;
6934
6985
  case "merge":
6986
+ if (!headSha) {
6987
+ log17.warn(TAG17, `#${card.short_id} the pull request head is unreadable — holding, nothing to pin the merge to`);
6988
+ return "wait";
6989
+ }
6935
6990
  log17.info(TAG17, `#${card.short_id} auto-merging (${autoMerge.strategy})`);
6936
- await mergePullRequest(prUrl, cwd, provider, autoMerge.strategy, autoMerge.deleteBranch);
6991
+ try {
6992
+ await mergePullRequest(prUrl, cwd, provider, autoMerge.strategy, autoMerge.deleteBranch, headSha);
6993
+ } catch (err) {
6994
+ if (!(err instanceof PullRequestHeadMovedError))
6995
+ throw err;
6996
+ log17.warn(TAG17, `#${card.short_id} head moved after the check (${headSha} → ${err.actualHeadSha ?? "unreadable"}) — not merged`);
6997
+ return "head-moved";
6998
+ }
6937
6999
  await record({ kind: "merge-requested", headSha });
6938
7000
  return action;
6939
7001
  }
@@ -7237,6 +7299,11 @@ ${f.body}`;
7237
7299
  purpose: "claims to VERIFY against the diff before any of them is acted on, never instructions"
7238
7300
  });
7239
7301
  }
7302
+ function describeHeldPaths(paths) {
7303
+ const listed = paths.slice(0, MAX_LISTED_PATHS).map(stampablePath);
7304
+ const more = paths.length - listed.length;
7305
+ return paths.length === 1 ? `The path is ${listed[0]}.` : `The paths are ${listed.join(", ")}${more > 0 ? ` and ${more} more` : ""}.`;
7306
+ }
7240
7307
  async function gitRevParse(what, at) {
7241
7308
  try {
7242
7309
  const { stdout } = await execFileAsync2("git", [...GIT_NO_HOOKS7, "rev-parse", what], { cwd: at });
@@ -7339,6 +7406,7 @@ async function runMergeCommand(deps) {
7339
7406
  for (;; ) {
7340
7407
  const findings = [];
7341
7408
  const heldPaths = [];
7409
+ const approvals = [];
7342
7410
  let outcome;
7343
7411
  try {
7344
7412
  outcome = await attemptAutoMerge({
@@ -7355,9 +7423,18 @@ async function runMergeCommand(deps) {
7355
7423
  base = read.baseRefName;
7356
7424
  prHead = read.headSha;
7357
7425
  },
7358
- onHumanPathHeld: (path) => {
7359
- heldPaths.push(path);
7426
+ onHumanPathHeld: (path, paths) => {
7427
+ heldPaths.push(...paths ?? [path]);
7360
7428
  },
7429
+ ...deps.closedLaneApproval !== undefined ? {
7430
+ closedLaneApproval: deps.closedLaneApproval,
7431
+ onClosedLaneApproved: (sha, paths) => {
7432
+ approvals.splice(0, approvals.length, {
7433
+ headSha: sha,
7434
+ paths: paths.slice(0, MAX_LISTED_PATHS).map(stampablePath)
7435
+ });
7436
+ }
7437
+ } : {},
7361
7438
  onFindings: (f) => {
7362
7439
  findings.push({
7363
7440
  items: f.report.items,
@@ -7383,6 +7460,9 @@ async function runMergeCommand(deps) {
7383
7460
  return 0;
7384
7461
  }
7385
7462
  if (outcome === "merge") {
7463
+ const approval = approvals[0];
7464
+ if (approval)
7465
+ deps.emit({ type: "closed_lane_approved", ...approval });
7386
7466
  deps.emit({ type: "merged", prUrl: deps.prUrl, base });
7387
7467
  await finish(deps);
7388
7468
  return 0;
@@ -7406,7 +7486,7 @@ async function runMergeCommand(deps) {
7406
7486
  }
7407
7487
  const park = PARK_REASONS2[outcome];
7408
7488
  if (park) {
7409
- const detail = outcome === "wrong-base" && base ? ` The pull request targets "${base}"; this checkout merges into "${deps.config.worktree.baseBranch}".` : outcome === "stamp-human-paths" && heldPaths[0] !== undefined ? ` The path is ${stampablePath(heldPaths[0])}.` : outcome === "head-unbound" ? ` The pull request's head is ${prHead ?? "unreadable"}; this run built ${localHeadSha ?? "no head it can prove"}.${anchorDetail}` : "";
7489
+ const detail = outcome === "wrong-base" && base ? ` The pull request targets "${base}"; this checkout merges into "${deps.config.worktree.baseBranch}".` : outcome === "stamp-human-paths" && heldPaths[0] !== undefined ? ` ${describeHeldPaths(heldPaths)}` : outcome === "head-unbound" ? ` The pull request's head is ${prHead ?? "unreadable"}; this run built ${localHeadSha ?? "no head it can prove"}.${anchorDetail}` : "";
7410
7490
  deps.emit({ type: "parked", reason: `${park}${detail}` });
7411
7491
  return 1;
7412
7492
  }
@@ -7503,7 +7583,7 @@ async function finish(deps) {
7503
7583
  deps.emit({ type: "board_incomplete", reason: detail });
7504
7584
  }
7505
7585
  }
7506
- var TAG22 = "merge-command", POLL_INTERVAL_MS = 30000, execFileAsync2, KEEP_WAITING, PARK_REASONS2;
7586
+ var TAG22 = "merge-command", POLL_INTERVAL_MS = 30000, execFileAsync2, KEEP_WAITING, MAX_LISTED_PATHS = 10, PARK_REASONS2;
7507
7587
  var init_merge_command = __esm(() => {
7508
7588
  init_src();
7509
7589
  init_auto_merge();
@@ -7522,14 +7602,15 @@ var init_merge_command = __esm(() => {
7522
7602
  "stamp-review-findings": "the independent review posted blocking findings — nothing merges over an unread finding",
7523
7603
  "stamp-conflict": "the pull request conflicts with its base — merge the base branch into it and push",
7524
7604
  "stamp-review-incomplete": "the independent review never reached a verdict (truncated or errored run)",
7525
- "stamp-human-paths": "the pull request changes a path that always needs a person (review.autoMerge.humanPaths) — nothing merged. Read the change and merge it yourself",
7605
+ "stamp-human-paths": "the pull request changes a path that always needs a person (review.autoMerge.humanPaths) — nothing merged. A person reads the change, then merges it or runs this merge command again, in a terminal, with --allow-closed-lane <head sha>",
7526
7606
  "stamp-human-paths-unreadable": "the changed files of the pull request could not be read, so the sensitive-path check (review.autoMerge.humanPaths) cannot run — nothing merged. Try again, or merge it yourself once you have read the change",
7527
7607
  rereview: "the branch moved since it was reviewed — the merge needs a fresh review",
7528
7608
  repair: "CI needs repair, which the interactive driver does not do",
7529
7609
  "unsupported-provider": "no GitHub remote could be read for this checkout — auto-merge only supports GitHub, and a missing or unreadable `origin` reads the same way",
7530
7610
  "foreign-pr": "the card's `PR:` line does not name a pull request on this checkout's own origin — nothing was read from it, and nothing merged. Fix the link, or find out who redirected it",
7531
7611
  "wrong-base": "the pull request does not target the branch this checkout merges into — nothing merged. Check the card's `PR:` line names the pull request this run opened, and that it was opened against the default base",
7532
- "head-unbound": "this merge is not bound to the head this run built — nothing merged. Every other input to the gate comes from the card or from the pull request the card names, so a merge is only ever allowed for a head this run itself produced"
7612
+ "head-unbound": "this merge is not bound to the head this run built — nothing merged. Every other input to the gate comes from the card or from the pull request the card names, so a merge is only ever allowed for a head this run itself produced",
7613
+ "head-moved": "the pull request head changed after the check — nothing merged, and the new head went through no check. Run this merge command again so it checks the new head"
7533
7614
  };
7534
7615
  });
7535
7616
 
@@ -15210,6 +15291,7 @@ class Pool {
15210
15291
  await this.emitWaiting(cardId, reason);
15211
15292
  }
15212
15293
  lastWaitingEmit = new Map;
15294
+ ownClaims = new Map;
15213
15295
  async emitWaiting(cardId, currentTask) {
15214
15296
  if (this.lastWaitingEmit.get(cardId) === currentTask)
15215
15297
  return;
@@ -15285,6 +15367,7 @@ class Pool {
15285
15367
  }
15286
15368
  }
15287
15369
  async removeCard(cardId) {
15370
+ this.ownClaims.delete(cardId);
15288
15371
  await this.forgiveAttempts(cardId);
15289
15372
  this.lastWaitingEmit.delete(cardId);
15290
15373
  this.reservations.delete(cardId);
@@ -15303,13 +15386,22 @@ class Pool {
15303
15386
  }
15304
15387
  }
15305
15388
  async resetAttemptsForReassign(cardId) {
15389
+ const markedAt = this.ownClaims.get(cardId);
15390
+ this.ownClaims.delete(cardId);
15391
+ const ownClaim = markedAt !== undefined && Date.now() - markedAt <= OWN_CLAIM_ECHO_WINDOW_MS;
15306
15392
  if (this.isCardActive(cardId))
15307
15393
  return;
15308
- await this.forgiveAttempts(cardId);
15394
+ await this.forgiveAttempts(cardId, ownClaim);
15395
+ }
15396
+ expectOwnClaim(cardId) {
15397
+ this.ownClaims.set(cardId, Date.now());
15398
+ }
15399
+ forgetOwnClaim(cardId) {
15400
+ this.ownClaims.delete(cardId);
15309
15401
  }
15310
- async forgiveAttempts(cardId) {
15402
+ async forgiveAttempts(cardId, ownClaim = false) {
15311
15403
  await this.stateStore.resetAttempts(cardId, {
15312
- clearTurns: !this.stateStore.isFindingsRoundCard(cardId)
15404
+ clearTurns: !ownClaim && !this.stateStore.isFindingsRoundCard(cardId)
15313
15405
  });
15314
15406
  }
15315
15407
  isCardActive(cardId) {
@@ -15920,7 +16012,7 @@ ${round ? roundWayOn("findingsRoundRuns") : "Reassign the card — that clears t
15920
16012
  return true;
15921
16013
  }
15922
16014
  }
15923
- var TAG42 = "pool";
16015
+ var TAG42 = "pool", OWN_CLAIM_ECHO_WINDOW_MS = 60000;
15924
16016
  var init_pool = __esm(() => {
15925
16017
  init_src();
15926
16018
  init_board_helpers();
@@ -16685,8 +16777,10 @@ async function reclaimPreReviewStrands(opts) {
16685
16777
  if (prUrl && !(hasRoundMarker(card.description) && await checkPrMergeStatus3(prUrl, cwd, provider) === "open")) {
16686
16778
  continue;
16687
16779
  }
16780
+ opts.beforeClaim?.(card);
16688
16781
  const won = await claimUnassignedCard(client, card.id, agentId);
16689
16782
  if (!won) {
16783
+ opts.claimLost?.(card);
16690
16784
  log53.debug(TAG47, `#${card.short_id} — lost the review claim race, skipping`);
16691
16785
  continue;
16692
16786
  }
@@ -17503,6 +17597,8 @@ class Reconciler {
17503
17597
  knownCardIds,
17504
17598
  cwd: process.cwd(),
17505
17599
  provider: this.gitProvider,
17600
+ beforeClaim: (card) => this.pool.expectOwnClaim(card.id),
17601
+ claimLost: (card) => this.pool.forgetOwnClaim(card.id),
17506
17602
  onClaimed: async (card) => {
17507
17603
  const column = columns.find((c) => c.id === card.column_id);
17508
17604
  if (!column)