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