@intent-systems/nexus 2026.1.5-3 → 2026.1.5-5

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 (144) hide show
  1. package/dist/agents/agent-id.js +41 -0
  2. package/dist/agents/auth-profiles.js +114 -25
  3. package/dist/agents/identity-state.js +79 -0
  4. package/dist/agents/model-auth.js +1 -0
  5. package/dist/agents/model-fallback.js +15 -9
  6. package/dist/agents/model-selection.js +1 -1
  7. package/dist/agents/models-config.js +17 -11
  8. package/dist/agents/pi-embedded-runner.js +101 -9
  9. package/dist/agents/sandbox.js +12 -3
  10. package/dist/agents/skill-runner.js +29 -4
  11. package/dist/agents/skill-usage.js +114 -11
  12. package/dist/agents/skills-status.js +4 -4
  13. package/dist/agents/skills.js +18 -7
  14. package/dist/agents/subagent-registry.js +25 -11
  15. package/dist/agents/system-prompt.js +16 -0
  16. package/dist/agents/tool-policy.js +19 -3
  17. package/dist/agents/tools/browser-tool.js +5 -2
  18. package/dist/agents/tools/image-tool.js +93 -8
  19. package/dist/agents/tools/sessions-announce-target.js +5 -1
  20. package/dist/agents/workspace.js +55 -46
  21. package/dist/auto-reply/command-detection.js +2 -1
  22. package/dist/auto-reply/reply/directive-handling.js +153 -28
  23. package/dist/auto-reply/reply/directives.js +17 -2
  24. package/dist/auto-reply/reply/model-selection.js +8 -3
  25. package/dist/auto-reply/reply/queue.js +2 -2
  26. package/dist/auto-reply/reply.js +1 -1
  27. package/dist/auto-reply/thinking.js +15 -0
  28. package/dist/browser/chrome.js +1 -1
  29. package/dist/browser/client.js +2 -0
  30. package/dist/browser/config.js +6 -2
  31. package/dist/browser/pw-tools-core.js +3 -0
  32. package/dist/browser/routes/agent.js +14 -0
  33. package/dist/canvas-host/server.js +1 -1
  34. package/dist/capabilities/detector.js +245 -0
  35. package/dist/capabilities/registry.js +99 -0
  36. package/dist/channels/location.js +44 -0
  37. package/dist/channels/web/index.js +2 -0
  38. package/dist/cli/cloud-cli.js +12 -7
  39. package/dist/cli/credential-cli.js +139 -17
  40. package/dist/cli/gateway-cli.js +1 -1
  41. package/dist/cli/log-cli.js +25 -0
  42. package/dist/cli/pairing-cli.js +1 -1
  43. package/dist/cli/program.js +58 -6
  44. package/dist/cli/run-main.js +1 -1
  45. package/dist/cli/skills-cli.js +144 -21
  46. package/dist/cli/skills-hub-cli.js +59 -29
  47. package/dist/cli/tool-connector-cli.js +99 -24
  48. package/dist/cli/upstream-sync-cli.js +253 -96
  49. package/dist/cli/usage-cli.js +14 -0
  50. package/dist/commands/auth-choice-options.js +6 -1
  51. package/dist/commands/auth-choice.js +157 -5
  52. package/dist/commands/bootstrap-preset.js +10 -6
  53. package/dist/commands/capabilities.js +33 -6
  54. package/dist/commands/claude-md.js +3 -2
  55. package/dist/commands/config-view.js +1 -1
  56. package/dist/commands/configure.js +4 -4
  57. package/dist/commands/credential.js +497 -36
  58. package/dist/commands/cursor-rules.js +39 -19
  59. package/dist/commands/doctor.js +5 -4
  60. package/dist/commands/identity.js +28 -31
  61. package/dist/commands/init.js +15 -18
  62. package/dist/commands/log.js +134 -0
  63. package/dist/commands/models/fallbacks.js +1 -1
  64. package/dist/commands/models/image-fallbacks.js +1 -1
  65. package/dist/commands/models/list.js +1 -1
  66. package/dist/commands/models/scan.js +1 -1
  67. package/dist/commands/onboard-auth.js +27 -2
  68. package/dist/commands/onboard-eve-identity.js +7 -8
  69. package/dist/commands/onboard-non-interactive.js +4 -2
  70. package/dist/commands/onboard-quickstart.js +18 -11
  71. package/dist/commands/quest-state.js +271 -0
  72. package/dist/commands/quest.js +53 -13
  73. package/dist/commands/reset.js +1 -1
  74. package/dist/commands/sessions-ingest.js +5 -4
  75. package/dist/commands/setup.js +4 -2
  76. package/dist/commands/skills-manifest.js +2 -2
  77. package/dist/commands/status.js +179 -61
  78. package/dist/commands/suggestions.js +1 -1
  79. package/dist/commands/usage-tracking.js +32 -0
  80. package/dist/commands/usage-upload.js +6 -1
  81. package/dist/config/defaults.js +1 -3
  82. package/dist/config/includes.js +5 -7
  83. package/dist/config/io.js +88 -16
  84. package/dist/config/legacy.js +4 -2
  85. package/dist/config/paths.js +16 -0
  86. package/dist/config/sessions.js +9 -5
  87. package/dist/config/zod-schema.js +4 -3
  88. package/dist/control-plane/broker/broker.js +1022 -0
  89. package/dist/control-plane/compaction.js +282 -0
  90. package/dist/control-plane/factory.js +31 -0
  91. package/dist/control-plane/index.js +10 -0
  92. package/dist/control-plane/odu/agents.js +192 -0
  93. package/dist/control-plane/odu/interaction-tools.js +208 -0
  94. package/dist/control-plane/odu/prompt-loader.js +95 -0
  95. package/dist/control-plane/odu/runtime.js +479 -0
  96. package/dist/control-plane/odu/types.js +6 -0
  97. package/dist/control-plane/odu-control-plane.js +316 -0
  98. package/dist/control-plane/single-agent.js +249 -0
  99. package/dist/control-plane/types.js +11 -0
  100. package/dist/credentials/store.js +449 -0
  101. package/dist/gateway/server-browser.js +5 -4
  102. package/dist/gateway/server-methods/cron.js +11 -1
  103. package/dist/gateway/server.js +14 -7
  104. package/dist/infra/bonjour.js +1 -1
  105. package/dist/infra/event-log.js +8 -2
  106. package/dist/infra/path-env.js +1 -2
  107. package/dist/infra/provider-usage.auth.js +5 -3
  108. package/dist/infra/provider-usage.fetch.claude.js +16 -6
  109. package/dist/infra/provider-usage.fetch.minimax.js +8 -3
  110. package/dist/infra/provider-usage.js +9 -5
  111. package/dist/infra/restart.js +2 -2
  112. package/dist/infra/usage-settings.js +78 -0
  113. package/dist/infra/usage-suggestions.js +17 -5
  114. package/dist/infra/usage-upload.js +38 -1
  115. package/dist/infra/voicewake.js +2 -2
  116. package/dist/logging/redact.js +109 -0
  117. package/dist/markdown/fences.js +58 -0
  118. package/dist/media/image-ops.js +3 -1
  119. package/dist/memory/embeddings.js +146 -0
  120. package/dist/memory/index.js +3 -0
  121. package/dist/memory/internal.js +163 -0
  122. package/dist/pairing/pairing-store.js +218 -0
  123. package/dist/plugins/cli.js +42 -0
  124. package/dist/plugins/discovery.js +253 -0
  125. package/dist/plugins/install.js +181 -0
  126. package/dist/plugins/loader.js +290 -0
  127. package/dist/plugins/registry.js +105 -0
  128. package/dist/plugins/status.js +29 -0
  129. package/dist/plugins/tools.js +39 -0
  130. package/dist/plugins/types.js +1 -0
  131. package/dist/providers/github-copilot-auth.js +1 -1
  132. package/dist/routing/resolve-route.js +144 -0
  133. package/dist/routing/session-key.js +65 -0
  134. package/dist/sessions/send-policy.js +5 -5
  135. package/dist/slack/monitor.js +22 -1
  136. package/dist/telegram/reaction-level.js +2 -1
  137. package/dist/utils/provider-utils.js +28 -0
  138. package/dist/utils.js +4 -3
  139. package/dist/wizard/onboarding.js +29 -7
  140. package/package.json +4 -29
  141. package/patches/@mariozechner__pi-ai.patch +215 -0
  142. package/patches/playwright-core@1.57.0.patch +13 -0
  143. package/patches/qrcode-terminal.patch +12 -0
  144. package/scripts/postinstall.js +202 -0
@@ -21,7 +21,7 @@
21
21
  * bundle-dispatch-oldest - Dispatch oldest pending bundles
22
22
  */
23
23
  import { execSync, spawn } from "node:child_process";
24
- import { existsSync, readFileSync, writeFileSync, mkdirSync, openSync, closeSync, statSync, } from "node:fs";
24
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, statSync, writeFileSync, } from "node:fs";
25
25
  import { dirname, join, resolve } from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
27
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -29,6 +29,14 @@ const PROJECT_ROOT = resolve(__dirname, "../..");
29
29
  const STATE_FILE = join(PROJECT_ROOT, ".upstream-sync/state.json");
30
30
  const TMUX_SOCKET = "/tmp/upstream-sync.sock";
31
31
  const RUNS_DIR = join(PROJECT_ROOT, ".upstream-sync/runs");
32
+ const DEFAULT_MAX_CONCURRENT = 6;
33
+ function getMaxConcurrent() {
34
+ const raw = process.env.NEXUS_UPSTREAM_SYNC_MAX_CONCURRENT;
35
+ const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
36
+ if (Number.isFinite(parsed) && parsed > 0)
37
+ return parsed;
38
+ return DEFAULT_MAX_CONCURRENT;
39
+ }
32
40
  // ============================================================================
33
41
  // State Management
34
42
  // ============================================================================
@@ -120,7 +128,11 @@ function getTypePriority(type, breaking) {
120
128
  function bundleDirectCommits(state) {
121
129
  const directCommits = Object.entries(state.merges)
122
130
  .filter(([_, e]) => e.isDirectCommit && e.status === "new" && !e.bundleId)
123
- .map(([sha, e]) => ({ sha, entry: e, parsed: parseConventionalCommit(e.title) }));
131
+ .map(([sha, e]) => ({
132
+ sha,
133
+ entry: e,
134
+ parsed: parseConventionalCommit(e.title),
135
+ }));
124
136
  if (directCommits.length === 0)
125
137
  return [];
126
138
  // Group by: date (day) + scope + type
@@ -135,7 +147,7 @@ function bundleDirectCommits(state) {
135
147
  if (!groups.has(key)) {
136
148
  groups.set(key, []);
137
149
  }
138
- groups.get(key).push(commit);
150
+ groups.get(key)?.push(commit);
139
151
  }
140
152
  // Convert groups to bundles
141
153
  const bundles = [];
@@ -177,8 +189,17 @@ function bundleDirectCommits(state) {
177
189
  return bundles.sort((a, b) => b.priority - a.priority);
178
190
  }
179
191
  // Types that should ALWAYS be consolidated into daily bundles (low priority, safe to batch)
180
- const LOW_PRIORITY_TYPES = new Set(["fix", "docs", "test", "chore", "style", "ci", "build"]);
181
- const BUNDLE_STRATEGY = (process.env.NEXUS_UPSTREAM_SYNC_BUNDLE_STRATEGY ?? "aggressive");
192
+ const LOW_PRIORITY_TYPES = new Set([
193
+ "fix",
194
+ "docs",
195
+ "test",
196
+ "chore",
197
+ "style",
198
+ "ci",
199
+ "build",
200
+ ]);
201
+ const BUNDLE_STRATEGY = (process.env.NEXUS_UPSTREAM_SYNC_BUNDLE_STRATEGY ??
202
+ "aggressive");
182
203
  const MIN_BUNDLE_LINES = (() => {
183
204
  const raw = process.env.NEXUS_UPSTREAM_SYNC_BUNDLE_MIN_LINES ?? "1000";
184
205
  const parsed = Number.parseInt(raw, 10);
@@ -314,7 +335,7 @@ function consolidateBundles(state, bundles) {
314
335
  if (!scopeBundles.has(key)) {
315
336
  scopeBundles.set(key, []);
316
337
  }
317
- scopeBundles.get(key).push(bundle);
338
+ scopeBundles.get(key)?.push(bundle);
318
339
  continue;
319
340
  }
320
341
  if (bundle.type && LOW_PRIORITY_TYPES.has(bundle.type)) {
@@ -351,7 +372,7 @@ function consolidateBundles(state, bundles) {
351
372
  if (!byDate.has(date)) {
352
373
  byDate.set(date, []);
353
374
  }
354
- byDate.get(date).push(bundle);
375
+ byDate.get(date)?.push(bundle);
355
376
  }
356
377
  for (const [date, group] of byDate) {
357
378
  if (group.length === 1) {
@@ -384,7 +405,7 @@ function consolidateBundles(state, bundles) {
384
405
  if (!byDateType.has(key)) {
385
406
  byDateType.set(key, []);
386
407
  }
387
- byDateType.get(key).push(bundle);
408
+ byDateType.get(key)?.push(bundle);
388
409
  }
389
410
  for (const [key, group] of byDateType) {
390
411
  if (group.length === 1) {
@@ -418,7 +439,7 @@ function printBundles(bundles) {
418
439
  console.log("No bundles to display.");
419
440
  return;
420
441
  }
421
- console.log("\n" + "═".repeat(60));
442
+ console.log(`\n${"═".repeat(60)}`);
422
443
  console.log(" DIRECT COMMIT BUNDLES");
423
444
  console.log("═".repeat(60));
424
445
  // Group by priority tier
@@ -442,13 +463,15 @@ function printBundles(bundles) {
442
463
  printTier("FIXES", "🐛", fixes);
443
464
  printTier("REFACTORS", "♻️", refactors);
444
465
  printTier("OTHER", "📦", other);
445
- console.log("\n" + "═".repeat(60));
466
+ console.log(`\n${"═".repeat(60)}`);
446
467
  console.log(` Total: ${bundles.length} bundles, ${bundles.reduce((sum, b) => sum + b.commits.length, 0)} commits`);
447
- console.log("═".repeat(60) + "\n");
468
+ console.log(`${"═".repeat(60)}\n`);
448
469
  }
449
470
  function categorizeSplitBucket(title, type) {
450
471
  const lower = title.toLowerCase();
451
- if (lower.includes("appcast") || lower.includes("release") || lower.includes("changelog")) {
472
+ if (lower.includes("appcast") ||
473
+ lower.includes("release") ||
474
+ lower.includes("changelog")) {
452
475
  return "release";
453
476
  }
454
477
  if (type === "docs" ||
@@ -483,7 +506,9 @@ function categorizeSplitBucket(title, type) {
483
506
  if (lower.includes("cron") || lower.includes("gateway")) {
484
507
  return "gateway";
485
508
  }
486
- if (lower.includes("voice") || lower.includes("plugin") || lower.includes("apply_patch")) {
509
+ if (lower.includes("voice") ||
510
+ lower.includes("plugin") ||
511
+ lower.includes("apply_patch")) {
487
512
  return "voice";
488
513
  }
489
514
  if (type && type !== "other")
@@ -495,7 +520,11 @@ function categorizeSplitBucket(title, type) {
495
520
  // ============================================================================
496
521
  function exec(cmd, cwd) {
497
522
  try {
498
- return execSync(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
523
+ return execSync(cmd, {
524
+ cwd,
525
+ encoding: "utf-8",
526
+ stdio: ["pipe", "pipe", "pipe"],
527
+ }).trim();
499
528
  }
500
529
  catch (e) {
501
530
  const error = e;
@@ -503,7 +532,8 @@ function exec(cmd, cwd) {
503
532
  }
504
533
  }
505
534
  function isDispatchDisabled(state) {
506
- return process.env.NEXUS_UPSTREAM_SYNC_DISABLE_DISPATCH === "1" || Boolean(state?.dispatchDisabled);
535
+ return (process.env.NEXUS_UPSTREAM_SYNC_DISABLE_DISPATCH === "1" ||
536
+ Boolean(state?.dispatchDisabled));
507
537
  }
508
538
  function assertDispatchEnabled(state) {
509
539
  if (isDispatchDisabled(state)) {
@@ -524,7 +554,10 @@ function branchExists(branchName) {
524
554
  }
525
555
  function isGitAncestor(ancestor, target, cwd) {
526
556
  try {
527
- execSync(`git merge-base --is-ancestor ${ancestor} ${target}`, { cwd, stdio: "ignore" });
557
+ execSync(`git merge-base --is-ancestor ${ancestor} ${target}`, {
558
+ cwd,
559
+ stdio: "ignore",
560
+ });
528
561
  return true;
529
562
  }
530
563
  catch {
@@ -539,7 +572,8 @@ function assertBranchBasedOnMain(branchName, worktreePath) {
539
572
  }
540
573
  return mainCommit;
541
574
  }
542
- if (branchExists(branchName) && !isGitAncestor(mainCommit, branchName, PROJECT_ROOT)) {
575
+ if (branchExists(branchName) &&
576
+ !isGitAncestor(mainCommit, branchName, PROJECT_ROOT)) {
543
577
  throw new Error(`Branch ${branchName} is not based on current main (${mainCommit}). Delete/recreate before dispatch.`);
544
578
  }
545
579
  return mainCommit;
@@ -559,7 +593,7 @@ function getRunPaths(runId) {
559
593
  }
560
594
  function bashSingleQuote(s) {
561
595
  // 'foo'"'"'bar' pattern
562
- return `'${s.replace(/'/g, `'\"'\"'`)}'`;
596
+ return `'${s.replace(/'/g, `'"'"'`)}'`;
563
597
  }
564
598
  function isPidRunning(pid) {
565
599
  try {
@@ -580,6 +614,19 @@ function readLastLines(filePath, maxLines) {
580
614
  return "";
581
615
  }
582
616
  }
617
+ function hasWorktreeCommitBeyondMain(worktreePath) {
618
+ try {
619
+ const log = execSync(`git log --oneline main..HEAD`, {
620
+ cwd: worktreePath,
621
+ encoding: "utf-8",
622
+ stdio: ["pipe", "pipe", "ignore"],
623
+ }).trim();
624
+ return Boolean(log);
625
+ }
626
+ catch {
627
+ return false;
628
+ }
629
+ }
583
630
  function isRunStalled(runLog, runStartedAt) {
584
631
  if (!runLog || !runStartedAt)
585
632
  return false;
@@ -605,13 +652,19 @@ function startDetachedAgentRun(opts) {
605
652
  const promptArg = bashSingleQuote(opts.prompt);
606
653
  const commitMsgArg = bashSingleQuote(opts.commitMessage);
607
654
  const exitPathArg = bashSingleQuote(exitPath);
655
+ const nodeMajor = Number((process.versions.node ?? "").split(".")[0] ?? 0);
656
+ const skipInstall = process.env.NEXUS_UPSTREAM_SYNC_SKIP_INSTALL === "1" ||
657
+ (nodeMajor > 0 && nodeMajor < 20);
658
+ const installCmd = skipInstall
659
+ ? `echo "SKIP_NPM_INSTALL"; `
660
+ : `npm install || echo "WARN: npm install failed; continuing"; `;
608
661
  const cmd = `rc=0; ` +
609
662
  `echo "== upstream-sync run ${opts.runId} =="; ` +
610
663
  `echo "startedAt=${startedAt}"; ` +
611
664
  `echo "cwd=${opts.cwd}"; ` +
612
665
  `echo ""; ` +
613
- `npm install || rc=$?; ` +
614
- `if [ $rc -eq 0 ]; then codex exec --dangerously-bypass-approvals-and-sandbox ${promptArg} || rc=$?; fi; ` +
666
+ installCmd +
667
+ `codex exec --dangerously-bypass-approvals-and-sandbox ${promptArg} || rc=$?; ` +
615
668
  `if [ $rc -eq 0 ]; then ` +
616
669
  ` git add -A || rc=$?; ` +
617
670
  ` git reset PORT_TASK.md package-lock.json 2>/dev/null || true; ` +
@@ -631,14 +684,25 @@ function startDetachedAgentRun(opts) {
631
684
  env: process.env,
632
685
  });
633
686
  try {
634
- writeFileSync(metaPath, JSON.stringify({ runId: opts.runId, pid: child.pid, startedAt, cwd: opts.cwd, logPath, exitPath }, null, 2) + "\n");
687
+ writeFileSync(metaPath, `${JSON.stringify({
688
+ runId: opts.runId,
689
+ pid: child.pid,
690
+ startedAt,
691
+ cwd: opts.cwd,
692
+ logPath,
693
+ exitPath,
694
+ }, null, 2)}\n`);
635
695
  }
636
696
  catch {
637
697
  // ignore
638
698
  }
639
699
  child.unref();
640
700
  closeSync(fd);
641
- return { pid: child.pid, logPath, exitPath, metaPath, startedAt };
701
+ const pid = child.pid;
702
+ if (!pid) {
703
+ throw new Error("Expected child process pid");
704
+ }
705
+ return { pid, logPath, exitPath, metaPath, startedAt };
642
706
  }
643
707
  function getUpstreamPath(state) {
644
708
  return resolve(PROJECT_ROOT, state.upstream.path);
@@ -682,7 +746,14 @@ function findNewCommits(state) {
682
746
  const prMatch = subject.match(/Merge pull request #(\d+) from (.+)/);
683
747
  const prNumber = prMatch ? parseInt(prMatch[1], 10) : undefined;
684
748
  const title = prMatch ? prMatch[2] : subject;
685
- commits.push({ sha, prNumber, title, date, isDirectCommit: false, author });
749
+ commits.push({
750
+ sha,
751
+ prNumber,
752
+ title,
753
+ date,
754
+ isDirectCommit: false,
755
+ author,
756
+ });
686
757
  }
687
758
  else {
688
759
  // Check if this references a PR anywhere in the subject:
@@ -698,18 +769,31 @@ function findNewCommits(state) {
698
769
  .replace(/\s*\(#\d+\)\s*/g, " ")
699
770
  .replace(/\s+/g, " ")
700
771
  .trim();
701
- commits.push({ sha, prNumber, title, date, isDirectCommit: false, author });
772
+ commits.push({
773
+ sha,
774
+ prNumber,
775
+ title,
776
+ date,
777
+ isDirectCommit: false,
778
+ author,
779
+ });
702
780
  }
703
781
  else {
704
782
  // True direct commit - no PR number reference
705
- commits.push({ sha, title: subject, date, isDirectCommit: true, author });
783
+ commits.push({
784
+ sha,
785
+ title: subject,
786
+ date,
787
+ isDirectCommit: true,
788
+ author,
789
+ });
706
790
  }
707
791
  }
708
792
  }
709
793
  return commits;
710
794
  }
711
795
  // Legacy alias for compatibility
712
- function findNewMerges(state) {
796
+ function _findNewMerges(state) {
713
797
  return findNewCommits(state);
714
798
  }
715
799
  // ============================================================================
@@ -720,7 +804,9 @@ function generatePortTask(state, sha, entry) {
720
804
  const baseCommit = entry.baseCommit || exec("git rev-parse main", PROJECT_ROOT);
721
805
  // Get the diff for this commit
722
806
  const diff = exec(`git show ${sha} --stat`, upstreamPath);
723
- const commitType = entry.isDirectCommit ? "Direct Commit" : `PR #${entry.prNumber || "unknown"}`;
807
+ const commitType = entry.isDirectCommit
808
+ ? "Direct Commit"
809
+ : `PR #${entry.prNumber || "unknown"}`;
724
810
  const commitLabel = entry.isDirectCommit
725
811
  ? `⭐ DIRECT COMMIT (HIGH PRIORITY)`
726
812
  : `PR #${entry.prNumber}`;
@@ -792,7 +878,7 @@ function dispatchAgent(state, sha) {
792
878
  exec(`git worktree add ${worktreePath} ${branchName}`, PROJECT_ROOT);
793
879
  }
794
880
  catch {
795
- throw new Error(`Failed to create worktree: ${e}`);
881
+ throw new Error(`Failed to create worktree: ${String(e)}`);
796
882
  }
797
883
  }
798
884
  }
@@ -804,7 +890,12 @@ function dispatchAgent(state, sha) {
804
890
  const runId = `pr-${identifier}-${Date.now()}`;
805
891
  const prompt = "Read PORT_TASK.md and complete the porting task. Commit your changes when done (a supervisor will also attempt to commit).";
806
892
  const commitMessage = "feat: port upstream changes";
807
- const run = startDetachedAgentRun({ runId, cwd: worktreePath, prompt, commitMessage });
893
+ const run = startDetachedAgentRun({
894
+ runId,
895
+ cwd: worktreePath,
896
+ prompt,
897
+ commitMessage,
898
+ });
808
899
  console.log(` Started detached runner pid=${run.pid}`);
809
900
  // Update state
810
901
  entry.status = "porting";
@@ -851,7 +942,8 @@ function checkAgentStatus(state, sha) {
851
942
  return "idle";
852
943
  // If log indicates codex finished and is waiting on commit, treat as idle.
853
944
  const tail = entry.runLog ? readLastLines(entry.runLog, 30) : "";
854
- if (tail.includes("Your task is complete") || tail.includes("Run these commands NOW"))
945
+ if (tail.includes("Your task is complete") ||
946
+ tail.includes("Run these commands NOW"))
855
947
  return "idle";
856
948
  return "working";
857
949
  }
@@ -884,7 +976,8 @@ function checkAgentStatus(state, sha) {
884
976
  try {
885
977
  const output = exec(`tmux -S "${TMUX_SOCKET}" capture-pane -p -t ${sessionName} -S -10`);
886
978
  // Check if codex is waiting for commit instructions
887
- if (output.includes("Your task is complete") || output.includes("Run these commands NOW")) {
979
+ if (output.includes("Your task is complete") ||
980
+ output.includes("Run these commands NOW")) {
888
981
  if (entry.portBranch) {
889
982
  try {
890
983
  const commits = exec(`git log main..${entry.portBranch} --oneline`, PROJECT_ROOT);
@@ -958,7 +1051,10 @@ function nudgeIdleAgent(worktreePath, sessionName) {
958
1051
  }
959
1052
  let hasChanges = false;
960
1053
  try {
961
- execSync("git diff --cached --quiet", { cwd: worktreePath, stdio: "ignore" });
1054
+ execSync("git diff --cached --quiet", {
1055
+ cwd: worktreePath,
1056
+ stdio: "ignore",
1057
+ });
962
1058
  }
963
1059
  catch {
964
1060
  hasChanges = true;
@@ -1014,7 +1110,9 @@ function updateAllStatuses(state) {
1014
1110
  if (entry.bundleId)
1015
1111
  continue;
1016
1112
  if (entry.status === "porting") {
1017
- const label = entry.prNumber ? `PR #${entry.prNumber}` : `Commit ${sha.slice(0, 8)}`;
1113
+ const label = entry.prNumber
1114
+ ? `PR #${entry.prNumber}`
1115
+ : `Commit ${sha.slice(0, 8)}`;
1018
1116
  const status = checkAgentStatus(state, sha);
1019
1117
  if (status === "done") {
1020
1118
  // Agent finished and committed - close session and mark complete
@@ -1098,7 +1196,7 @@ function checkBundleAgentStatus(bundleId) {
1098
1196
  encoding: "utf-8",
1099
1197
  stdio: ["pipe", "pipe", "ignore"],
1100
1198
  }).trim();
1101
- if (subject) {
1199
+ if (subject && hasWorktreeCommitBeyondMain(worktreePath)) {
1102
1200
  const files = execSync(`git show --name-only --pretty=format: -1`, {
1103
1201
  cwd: worktreePath,
1104
1202
  encoding: "utf-8",
@@ -1123,7 +1221,8 @@ function checkBundleAgentStatus(bundleId) {
1123
1221
  if (isRunStalled(bundle.runLog, bundle.runStartedAt))
1124
1222
  return "idle";
1125
1223
  const tail = bundle.runLog ? readLastLines(bundle.runLog, 40) : "";
1126
- if (tail.includes("Your task is complete") || tail.includes("Run these commands NOW"))
1224
+ if (tail.includes("Your task is complete") ||
1225
+ tail.includes("Run these commands NOW"))
1127
1226
  return "idle";
1128
1227
  return "working";
1129
1228
  }
@@ -1138,10 +1237,11 @@ function checkBundleAgentStatus(bundleId) {
1138
1237
  encoding: "utf-8",
1139
1238
  stdio: ["pipe", "pipe", "ignore"],
1140
1239
  }).trim();
1141
- if (log.includes("port") ||
1240
+ if ((log.includes("port") ||
1142
1241
  log.includes("feat:") ||
1143
1242
  log.includes("fix:") ||
1144
- log.includes("refactor")) {
1243
+ log.includes("refactor")) &&
1244
+ hasWorktreeCommitBeyondMain(worktreePath)) {
1145
1245
  // Ensure the commit includes actual changes beyond PORT_TASK/package-lock
1146
1246
  try {
1147
1247
  const files = execSync(`git show --name-only --pretty=format: -1`, {
@@ -1169,7 +1269,8 @@ function checkBundleAgentStatus(bundleId) {
1169
1269
  const pane = execSync(`tmux -S ${TMUX_SOCKET} capture-pane -t "${sessionName}" -p 2>/dev/null`, {
1170
1270
  encoding: "utf-8",
1171
1271
  });
1172
- if (pane.includes("Your task is complete") || pane.includes("Run these commands NOW")) {
1272
+ if (pane.includes("Your task is complete") ||
1273
+ pane.includes("Run these commands NOW")) {
1173
1274
  return "idle"; // Done but hasn't exited
1174
1275
  }
1175
1276
  return "working";
@@ -1246,7 +1347,14 @@ function formatDuration(seconds) {
1246
1347
  return `${mins}m`;
1247
1348
  }
1248
1349
  function printStatus(state) {
1249
- const counts = { new: 0, porting: 0, pending_review: 0, shelved: 0, merged: 0, ignored: 0 };
1350
+ const counts = {
1351
+ new: 0,
1352
+ porting: 0,
1353
+ pending_review: 0,
1354
+ shelved: 0,
1355
+ merged: 0,
1356
+ ignored: 0,
1357
+ };
1250
1358
  const byStatus = {
1251
1359
  pending_review: [],
1252
1360
  shelved: [],
@@ -1261,7 +1369,7 @@ function printStatus(state) {
1261
1369
  counts[statusKey]++;
1262
1370
  byStatus[statusKey].push({ sha, entry });
1263
1371
  }
1264
- console.log("\n" + "═".repeat(60));
1372
+ console.log(`\n${"═".repeat(60)}`);
1265
1373
  console.log(" UPSTREAM SYNC STATUS");
1266
1374
  console.log("═".repeat(60));
1267
1375
  console.log(` Last fetched: ${state.lastFetched}`);
@@ -1302,7 +1410,9 @@ function printStatus(state) {
1302
1410
  const sessionName = getBundleSessionName(bundle.id);
1303
1411
  const agentStatus = checkBundleAgentStatus(bundle.id);
1304
1412
  const session = tmuxSessions.get(sessionName);
1305
- const runtimeSec = session ? Math.floor(Date.now() / 1000) - session.created : 0;
1413
+ const runtimeSec = session
1414
+ ? Math.floor(Date.now() / 1000) - session.created
1415
+ : 0;
1306
1416
  const runtime = formatDuration(runtimeSec);
1307
1417
  const statusLabel = agentStatus === "idle"
1308
1418
  ? "stuck (idle)"
@@ -1359,7 +1469,9 @@ function printStatus(state) {
1359
1469
  if (byStatus.porting.length > 0) {
1360
1470
  console.log(" ⚙️ IN PROGRESS:");
1361
1471
  for (const { entry } of byStatus.porting) {
1362
- const status = entry.tmuxSession ? `tmux: ${entry.tmuxSession}` : "unknown";
1472
+ const status = entry.tmuxSession
1473
+ ? `tmux: ${entry.tmuxSession}`
1474
+ : "unknown";
1363
1475
  console.log(` PR #${entry.prNumber}: ${entry.title} [${status}]`);
1364
1476
  }
1365
1477
  console.log("");
@@ -1371,7 +1483,7 @@ function printStatus(state) {
1371
1483
  return -1;
1372
1484
  if (!a.entry.isDirectCommit && b.entry.isDirectCommit)
1373
1485
  return 1;
1374
- return new Date(b.entry.date).getTime() - new Date(a.entry.date).getTime();
1486
+ return (new Date(b.entry.date).getTime() - new Date(a.entry.date).getTime());
1375
1487
  });
1376
1488
  const directCount = sortedNew.filter((x) => x.entry.isDirectCommit).length;
1377
1489
  console.log(` 🆕 NEW (will dispatch agents): ${directCount} direct commits, ${byStatus.new.length - directCount} PRs`);
@@ -1380,7 +1492,9 @@ function printStatus(state) {
1380
1492
  console.log(` ⭐ DIRECT: ${entry.title} (${entry.author || "unknown"})`);
1381
1493
  }
1382
1494
  // Then PRs
1383
- for (const { entry } of sortedNew.filter((x) => !x.entry.isDirectCommit).slice(0, 10)) {
1495
+ for (const { entry } of sortedNew
1496
+ .filter((x) => !x.entry.isDirectCommit)
1497
+ .slice(0, 10)) {
1384
1498
  console.log(` PR #${entry.prNumber}: ${entry.title}`);
1385
1499
  }
1386
1500
  const remainingPRs = sortedNew.filter((x) => !x.entry.isDirectCommit).length - 10;
@@ -1410,7 +1524,7 @@ function printStatus(state) {
1410
1524
  console.log(" bundle-dispatch-oldest - Dispatch oldest pending bundles");
1411
1525
  console.log(" bundle-ignore <id> - Ignore a bundle");
1412
1526
  console.log(" bundle-auto - Auto-dispatch high-priority bundles");
1413
- console.log("═".repeat(60) + "\n");
1527
+ console.log(`${"═".repeat(60)}\n`);
1414
1528
  }
1415
1529
  // ============================================================================
1416
1530
  // Commands
@@ -1418,7 +1532,7 @@ function printStatus(state) {
1418
1532
  function findMergeBySha(state, prNumOrSha) {
1419
1533
  // Try PR number first
1420
1534
  const prNum = parseInt(prNumOrSha, 10);
1421
- if (!isNaN(prNum)) {
1535
+ if (!Number.isNaN(prNum)) {
1422
1536
  for (const [sha, entry] of Object.entries(state.merges)) {
1423
1537
  if (entry.prNumber === prNum) {
1424
1538
  return { sha, entry };
@@ -1437,13 +1551,13 @@ function findMergeBySha(state, prNumOrSha) {
1437
1551
  }
1438
1552
  return null;
1439
1553
  }
1440
- function cmdMerge(state, prNumOrSha) {
1554
+ function _cmdMerge(state, prNumOrSha) {
1441
1555
  const found = findMergeBySha(state, prNumOrSha);
1442
1556
  if (!found) {
1443
1557
  console.error(`❌ PR not found: ${prNumOrSha}`);
1444
1558
  return;
1445
1559
  }
1446
- const { sha, entry } = found;
1560
+ const { entry } = found;
1447
1561
  if (entry.status !== "pending_review") {
1448
1562
  console.error(`❌ PR #${entry.prNumber} is not ready for review (status: ${entry.status})`);
1449
1563
  return;
@@ -1466,7 +1580,7 @@ function cmdMerge(state, prNumOrSha) {
1466
1580
  }
1467
1581
  }
1468
1582
  catch (e) {
1469
- console.error(`❌ Merge failed: ${e}`);
1583
+ console.error(`❌ Merge failed: ${String(e)}`);
1470
1584
  }
1471
1585
  }
1472
1586
  function cmdIgnore(state, prNumOrSha, reason) {
@@ -1526,7 +1640,7 @@ function cmdShow(state, prNumOrSha) {
1526
1640
  console.log(diff);
1527
1641
  }
1528
1642
  catch (e) {
1529
- console.error(` Could not get diff: ${e}`);
1643
+ console.error(` Could not get diff: ${String(e)}`);
1530
1644
  }
1531
1645
  }
1532
1646
  function cmdShelve(state, prNumOrSha) {
@@ -1571,7 +1685,7 @@ function cmdUnshelve(state, prNumOrSha) {
1571
1685
  saveState(state);
1572
1686
  console.log(`👀 PR #${entry.prNumber} moved back to pending review`);
1573
1687
  }
1574
- function generatePrDescription(state, sha, entry) {
1688
+ function generatePrDescription(state, sha, _entry) {
1575
1689
  const upstreamPath = getUpstreamPath(state);
1576
1690
  try {
1577
1691
  // Get commit message body for description
@@ -1593,7 +1707,7 @@ function cmdTimeline(state) {
1593
1707
  const entries = Object.entries(state.merges);
1594
1708
  // Group by date
1595
1709
  const byDate = new Map();
1596
- for (const [sha, entry] of entries) {
1710
+ for (const [_sha, entry] of entries) {
1597
1711
  const date = entry.date.split("T")[0];
1598
1712
  if (!byDate.has(date)) {
1599
1713
  byDate.set(date, {
@@ -1607,6 +1721,8 @@ function cmdTimeline(state) {
1607
1721
  });
1608
1722
  }
1609
1723
  const day = byDate.get(date);
1724
+ if (!day)
1725
+ continue;
1610
1726
  if (entry.status === "merged")
1611
1727
  day.merged++;
1612
1728
  else if (entry.status === "porting")
@@ -1625,7 +1741,15 @@ function cmdTimeline(state) {
1625
1741
  // Sort dates
1626
1742
  const dates = [...byDate.keys()].sort();
1627
1743
  // Calculate totals
1628
- const totals = { merged: 0, porting: 0, pending: 0, new: 0, ignored: 0, prs: 0, direct: 0 };
1744
+ const totals = {
1745
+ merged: 0,
1746
+ porting: 0,
1747
+ pending: 0,
1748
+ new: 0,
1749
+ ignored: 0,
1750
+ prs: 0,
1751
+ direct: 0,
1752
+ };
1629
1753
  for (const day of byDate.values()) {
1630
1754
  totals.merged += day.merged;
1631
1755
  totals.porting += day.porting;
@@ -1635,11 +1759,15 @@ function cmdTimeline(state) {
1635
1759
  totals.prs += day.prs;
1636
1760
  totals.direct += day.direct;
1637
1761
  }
1638
- const totalCommits = totals.merged + totals.porting + totals.pending + totals.new + totals.ignored;
1762
+ const totalCommits = totals.merged +
1763
+ totals.porting +
1764
+ totals.pending +
1765
+ totals.new +
1766
+ totals.ignored;
1639
1767
  const ported = totals.merged + totals.porting + totals.pending;
1640
1768
  const portedPct = Math.round((ported / totalCommits) * 100);
1641
1769
  // Header
1642
- console.log("\n" + "═".repeat(80));
1770
+ console.log(`\n${"═".repeat(80)}`);
1643
1771
  console.log(" 📊 NEXUS ← UPSTREAM SYNC TIMELINE");
1644
1772
  console.log("═".repeat(80));
1645
1773
  console.log(` Tracking since: ${state.trackingStartDate}`);
@@ -1665,11 +1793,13 @@ function cmdTimeline(state) {
1665
1793
  console.log("");
1666
1794
  // Daily timeline
1667
1795
  console.log(" DAILY TIMELINE:");
1668
- console.log(" " + "─".repeat(76));
1796
+ console.log(` ${"─".repeat(76)}`);
1669
1797
  console.log(" Date │ Total │ Merged │ Porting │ Pending │ New │ Progress");
1670
- console.log(" " + "─".repeat(76));
1798
+ console.log(` ${"─".repeat(76)}`);
1671
1799
  for (const date of dates) {
1672
1800
  const day = byDate.get(date);
1801
+ if (!day)
1802
+ continue;
1673
1803
  const dayTotal = day.merged + day.porting + day.pending + day.new + day.ignored;
1674
1804
  const dayPorted = day.merged + day.porting + day.pending;
1675
1805
  const dayPct = dayTotal > 0 ? Math.round((dayPorted / dayTotal) * 100) : 0;
@@ -1685,7 +1815,7 @@ function cmdTimeline(state) {
1685
1815
  "·".repeat(Math.max(0, miniNew));
1686
1816
  console.log(` ${date} │ ${String(dayTotal).padStart(5)} │ ${String(day.merged).padStart(6)} │ ${String(day.porting).padStart(7)} │ ${String(day.pending).padStart(7)} │ ${String(day.new).padStart(5)} │ ${miniBar} ${dayPct}%`);
1687
1817
  }
1688
- console.log(" " + "─".repeat(76));
1818
+ console.log(` ${"─".repeat(76)}`);
1689
1819
  console.log(` TOTAL │ ${String(totalCommits).padStart(5)} │ ${String(totals.merged).padStart(6)} │ ${String(totals.porting).padStart(7)} │ ${String(totals.pending).padStart(7)} │ ${String(totals.new).padStart(5)} │`);
1690
1820
  console.log("");
1691
1821
  // Bundle summary
@@ -1704,7 +1834,7 @@ function cmdTimeline(state) {
1704
1834
  const portingEntries = entries.filter(([_, e]) => e.status === "porting");
1705
1835
  if (portingEntries.length > 0) {
1706
1836
  console.log(" 🔧 CURRENTLY PORTING:");
1707
- for (const [sha, entry] of portingEntries.slice(0, 8)) {
1837
+ for (const [_sha, entry] of portingEntries.slice(0, 8)) {
1708
1838
  const type = entry.isDirectCommit ? "DIRECT" : `PR #${entry.prNumber}`;
1709
1839
  const session = entry.tmuxSession ? ` [${entry.tmuxSession}]` : "";
1710
1840
  console.log(` ${type}: ${entry.title.slice(0, 50)}...${session}`);
@@ -1728,7 +1858,7 @@ function cmdTimeline(state) {
1728
1858
  console.log(` But Peter pushes ${totals.direct} commits directly to main (no PRs).`);
1729
1859
  console.log(` We just discovered these today - that's why there's so much "new" work.`);
1730
1860
  console.log("");
1731
- console.log("═".repeat(80) + "\n");
1861
+ console.log(`${"═".repeat(80)}\n`);
1732
1862
  }
1733
1863
  function cmdReview(state) {
1734
1864
  const reviewable = Object.entries(state.merges)
@@ -1853,7 +1983,7 @@ function cmdRebundle(state) {
1853
1983
  }
1854
1984
  // 1. Clear bundleIds from all "new" direct commits (not porting ones)
1855
1985
  let clearedCommits = 0;
1856
- for (const [sha, entry] of Object.entries(state.merges)) {
1986
+ for (const [_sha, entry] of Object.entries(state.merges)) {
1857
1987
  if (entry.isDirectCommit && entry.bundleId && entry.status === "new") {
1858
1988
  delete entry.bundleId;
1859
1989
  clearedCommits++;
@@ -1997,7 +2127,7 @@ function dispatchBundleAgent(state, bundleId) {
1997
2127
  exec(`git worktree add ${worktreePath} ${branchName}`, PROJECT_ROOT);
1998
2128
  }
1999
2129
  catch {
2000
- throw new Error(`Failed to create worktree: ${e}`);
2130
+ throw new Error(`Failed to create worktree: ${String(e)}`);
2001
2131
  }
2002
2132
  }
2003
2133
  }
@@ -2009,7 +2139,12 @@ function dispatchBundleAgent(state, bundleId) {
2009
2139
  const runId = `bundle-${shortId}-${Date.now()}`;
2010
2140
  const prompt = "Read PORT_TASK.md and complete the porting task. This is a BUNDLED port - apply all changes as one cohesive update. Commit your changes when done (a supervisor will also attempt to commit).";
2011
2141
  const commitMessage = `feat: port ${bundle.name}`;
2012
- const run = startDetachedAgentRun({ runId, cwd: worktreePath, prompt, commitMessage });
2142
+ const run = startDetachedAgentRun({
2143
+ runId,
2144
+ cwd: worktreePath,
2145
+ prompt,
2146
+ commitMessage,
2147
+ });
2013
2148
  console.log(` Started detached runner pid=${run.pid}`);
2014
2149
  // Update bundle status
2015
2150
  bundle.status = "porting";
@@ -2054,7 +2189,7 @@ function cmdBundleDispatch(state, bundleId) {
2054
2189
  dispatchBundleAgent(state, found);
2055
2190
  }
2056
2191
  catch (e) {
2057
- console.error(`❌ Failed to dispatch: ${e}`);
2192
+ console.error(`❌ Failed to dispatch: ${String(e)}`);
2058
2193
  }
2059
2194
  }
2060
2195
  function cmdBundleIgnore(state, bundleId, reason) {
@@ -2143,7 +2278,9 @@ function cmdBundleSplit(state, bundleId) {
2143
2278
  const types = new Set(sorted.map((c) => c.parsed.type));
2144
2279
  const scopes = new Set(sorted.map((c) => c.parsed.scope).filter(Boolean));
2145
2280
  const authors = new Set(sorted.map((c) => c.entry.author).filter(Boolean));
2146
- const author = authors.size === 1 ? Array.from(authors)[0] : bundle.author || "multiple";
2281
+ const author = authors.size === 1
2282
+ ? Array.from(authors)[0]
2283
+ : bundle.author || "multiple";
2147
2284
  const dateLabel = firstEntry?.date?.split("T")[0] ?? "unknown";
2148
2285
  const name = sorted.length === 1
2149
2286
  ? sorted[0]?.entry.title
@@ -2233,7 +2370,8 @@ function cmdBundleAuto(state, minPriority = 70) {
2233
2370
  console.log("🔍 Analyzing direct commits first...\n");
2234
2371
  cmdBundles(state);
2235
2372
  }
2236
- const pending = Object.values(state.bundles)
2373
+ const bundles = state.bundles ?? {};
2374
+ const pending = Object.values(bundles)
2237
2375
  .filter((b) => b.status === "pending" && b.priority >= minPriority)
2238
2376
  .sort((a, b) => b.priority - a.priority);
2239
2377
  if (pending.length === 0) {
@@ -2241,8 +2379,8 @@ function cmdBundleAuto(state, minPriority = 70) {
2241
2379
  return;
2242
2380
  }
2243
2381
  // Check how many agents are already running
2244
- const currentlyRunning = Object.values(state.bundles).filter((b) => b.status === "porting").length;
2245
- const maxConcurrent = 6;
2382
+ const currentlyRunning = Object.values(bundles).filter((b) => b.status === "porting").length;
2383
+ const maxConcurrent = getMaxConcurrent();
2246
2384
  const canDispatch = Math.max(0, maxConcurrent - currentlyRunning);
2247
2385
  if (canDispatch === 0) {
2248
2386
  console.log(`⏳ Max agents (${maxConcurrent}) already running. Wait for some to complete.`);
@@ -2251,13 +2389,17 @@ function cmdBundleAuto(state, minPriority = 70) {
2251
2389
  const toDispatch = pending.slice(0, canDispatch);
2252
2390
  console.log(`🚀 Auto-dispatching ${toDispatch.length} high-priority bundle(s)...\n`);
2253
2391
  for (const bundle of toDispatch) {
2254
- const priorityLabel = bundle.priority >= 100 ? "🔥 BREAKING" : bundle.priority >= 70 ? "✨ FEATURE" : "🐛 FIX";
2392
+ const priorityLabel = bundle.priority >= 100
2393
+ ? "🔥 BREAKING"
2394
+ : bundle.priority >= 70
2395
+ ? "✨ FEATURE"
2396
+ : "🐛 FIX";
2255
2397
  console.log(`\n${priorityLabel} ${bundle.name}`);
2256
2398
  try {
2257
2399
  dispatchBundleAgent(state, bundle.id);
2258
2400
  }
2259
2401
  catch (e) {
2260
- console.error(` ❌ Failed: ${e}`);
2402
+ console.error(` ❌ Failed: ${String(e)}`);
2261
2403
  }
2262
2404
  }
2263
2405
  console.log(`\n✅ Dispatched ${toDispatch.length} bundle(s).`);
@@ -2279,13 +2421,14 @@ function cmdBundleDispatchOldest(state, requested) {
2279
2421
  : 0;
2280
2422
  const portingIndividual = Object.values(state.merges).filter((e) => e.status === "porting" && !e.bundleId).length;
2281
2423
  const currentlyPorting = portingBundles + portingIndividual;
2282
- const maxConcurrent = 6;
2424
+ const maxConcurrent = getMaxConcurrent();
2283
2425
  const available = Math.max(0, maxConcurrent - currentlyPorting);
2284
2426
  if (available === 0) {
2285
2427
  console.log(`⏳ All ${maxConcurrent} slots occupied. Wait for current bundles to finish.`);
2286
2428
  return;
2287
2429
  }
2288
- const pending = Object.values(state.bundles)
2430
+ const bundles = state.bundles ?? {};
2431
+ const pending = Object.values(bundles)
2289
2432
  .filter((b) => b.status === "pending")
2290
2433
  .sort((a, b) => {
2291
2434
  const dateDiff = a.dateRange.start.localeCompare(b.dateRange.start);
@@ -2308,7 +2451,7 @@ function cmdBundleDispatchOldest(state, requested) {
2308
2451
  console.log(` ✓ ${bundle.name} (${bundle.commits.length} commits)`);
2309
2452
  }
2310
2453
  catch (e) {
2311
- console.error(` ✗ ${bundle.id}: ${e}`);
2454
+ console.error(` ✗ ${bundle.id}: ${String(e)}`);
2312
2455
  }
2313
2456
  }
2314
2457
  }
@@ -2326,7 +2469,7 @@ function checkMergeConflicts(state, prNums) {
2326
2469
  exec(`git merge --no-commit --no-ff ${found.entry.portBranch}`, PROJECT_ROOT);
2327
2470
  exec(`git reset --hard HEAD`, PROJECT_ROOT);
2328
2471
  }
2329
- catch (e) {
2472
+ catch {
2330
2473
  // Get conflicting files
2331
2474
  try {
2332
2475
  const conflictFiles = exec(`git diff --name-only --diff-filter=U`, PROJECT_ROOT);
@@ -2355,7 +2498,9 @@ function checkMergeConflicts(state, prNums) {
2355
2498
  return { canMerge: conflicts.length === 0, conflicts };
2356
2499
  }
2357
2500
  function cmdMergeMultiple(state, prNums) {
2358
- const nums = prNums.map((p) => parseInt(p, 10)).filter((n) => !isNaN(n));
2501
+ const nums = prNums
2502
+ .map((p) => parseInt(p, 10))
2503
+ .filter((n) => !Number.isNaN(n));
2359
2504
  if (nums.length === 0) {
2360
2505
  console.error("Usage: upstream-sync merge <pr#> [pr#] [pr#]...");
2361
2506
  return;
@@ -2368,7 +2513,8 @@ function cmdMergeMultiple(state, prNums) {
2368
2513
  console.error(`❌ PR #${prNum} not found`);
2369
2514
  return;
2370
2515
  }
2371
- if (found.entry.status !== "pending_review" && found.entry.status !== "shelved") {
2516
+ if (found.entry.status !== "pending_review" &&
2517
+ found.entry.status !== "shelved") {
2372
2518
  console.error(`❌ PR #${prNum} is not ready for merge (status: ${found.entry.status})`);
2373
2519
  return;
2374
2520
  }
@@ -2415,7 +2561,7 @@ function cmdMergeMultiple(state, prNums) {
2415
2561
  }
2416
2562
  }
2417
2563
  catch (e) {
2418
- console.error(` ❌ Merge failed: ${e}`);
2564
+ console.error(` ❌ Merge failed: ${String(e)}`);
2419
2565
  console.log(` Stopping merge sequence. Please resolve manually.`);
2420
2566
  return;
2421
2567
  }
@@ -2433,7 +2579,7 @@ async function runDaemon(state, intervalMinutes = 2) {
2433
2579
  console.log(` Interval: ${intervalMinutes} minute(s)`);
2434
2580
  console.log(` Max concurrent agents: 6`);
2435
2581
  console.log(` Press Ctrl+C to stop\n`);
2436
- console.log("═".repeat(60) + "\n");
2582
+ console.log(`${"═".repeat(60)}\n`);
2437
2583
  let iteration = 0;
2438
2584
  while (true) {
2439
2585
  iteration++;
@@ -2445,7 +2591,7 @@ async function runDaemon(state, intervalMinutes = 2) {
2445
2591
  state = loadState();
2446
2592
  }
2447
2593
  catch (e) {
2448
- console.error(` ❌ Failed to load state: ${e}`);
2594
+ console.error(` ❌ Failed to load state: ${String(e)}`);
2449
2595
  await sleep(intervalMinutes * 60 * 1000);
2450
2596
  continue;
2451
2597
  }
@@ -2454,7 +2600,7 @@ async function runDaemon(state, intervalMinutes = 2) {
2454
2600
  fetchUpstream(state);
2455
2601
  }
2456
2602
  catch (e) {
2457
- console.error(` ⚠️ Fetch failed: ${e}`);
2603
+ console.error(` ⚠️ Fetch failed: ${String(e)}`);
2458
2604
  }
2459
2605
  // 2. Find new commits (both merges and direct commits)
2460
2606
  const newCommits = findNewCommits(state);
@@ -2498,7 +2644,13 @@ async function runDaemon(state, intervalMinutes = 2) {
2498
2644
  for (const entry of Object.values(state.merges)) {
2499
2645
  mergeCounts[entry.status]++;
2500
2646
  }
2501
- const bundleCounts = { pending: 0, porting: 0, pending_review: 0, merged: 0, ignored: 0 };
2647
+ const bundleCounts = {
2648
+ pending: 0,
2649
+ porting: 0,
2650
+ pending_review: 0,
2651
+ merged: 0,
2652
+ ignored: 0,
2653
+ };
2502
2654
  if (state.bundles) {
2503
2655
  for (const bundle of Object.values(state.bundles)) {
2504
2656
  if (bundle.status in bundleCounts) {
@@ -2514,22 +2666,23 @@ async function runDaemon(state, intervalMinutes = 2) {
2514
2666
  console.log(` 📊 Status: ${totalPorting} porting | ${totalReady} ready | ${totalQueued} queued (${bundleCounts.pending} bundles + ${totalQueued - bundleCounts.pending} PRs)`);
2515
2667
  // 5. Check if we're done
2516
2668
  if (totalQueued === 0 && totalPorting === 0) {
2517
- console.log("\n" + "═".repeat(60));
2669
+ console.log(`\n${"═".repeat(60)}`);
2518
2670
  console.log("🎉 ALL DONE! No more work to process.");
2519
2671
  console.log(` ✅ Merged: ${mergeCounts.merged}`);
2520
2672
  console.log(` 👀 Ready for review: ${totalReady}`);
2521
2673
  console.log(` 📦 Shelved: ${mergeCounts.shelved}`);
2522
2674
  console.log(` ⏭️ Ignored: ${mergeCounts.ignored}`);
2523
- console.log("═".repeat(60) + "\n");
2675
+ console.log(`${"═".repeat(60)}\n`);
2524
2676
  console.log("Daemon exiting. Run 'upstream-sync review' to see pending PRs.");
2525
2677
  break;
2526
2678
  }
2527
2679
  // 6. Dispatch agents for new work (up to max concurrent)
2528
2680
  // Handle both bundles (for direct commits) and individual PRs
2529
- const maxConcurrent = 6;
2681
+ const maxConcurrent = getMaxConcurrent();
2530
2682
  // Count currently porting (both bundle and individual)
2531
2683
  const portingBundles = state.bundles
2532
- ? Object.values(state.bundles).filter((b) => b.status === "porting").length
2684
+ ? Object.values(state.bundles).filter((b) => b.status === "porting")
2685
+ .length
2533
2686
  : 0;
2534
2687
  const portingIndividual = Object.values(state.merges).filter((e) => e.status === "porting" && !e.bundleId).length;
2535
2688
  const currentlyPorting = portingBundles + portingIndividual;
@@ -2545,7 +2698,8 @@ async function runDaemon(state, intervalMinutes = 2) {
2545
2698
  .filter(([_, b]) => b.status === "pending")
2546
2699
  .sort((a, b) => {
2547
2700
  // Oldest first, then higher priority
2548
- const dateDiff = new Date(a[1].dateRange.start).getTime() - new Date(b[1].dateRange.start).getTime();
2701
+ const dateDiff = new Date(a[1].dateRange.start).getTime() -
2702
+ new Date(b[1].dateRange.start).getTime();
2549
2703
  if (dateDiff !== 0)
2550
2704
  return dateDiff;
2551
2705
  return b[1].priority - a[1].priority;
@@ -2571,7 +2725,7 @@ async function runDaemon(state, intervalMinutes = 2) {
2571
2725
  dispatched++;
2572
2726
  }
2573
2727
  catch (e) {
2574
- console.error(` ✗ Bundle ${bundleId}: ${e}`);
2728
+ console.error(` ✗ Bundle ${bundleId}: ${String(e)}`);
2575
2729
  }
2576
2730
  }
2577
2731
  }
@@ -2581,7 +2735,7 @@ async function runDaemon(state, intervalMinutes = 2) {
2581
2735
  .filter(([_, e]) => e.status === "new" && !e.isDirectCommit && !e.bundleId)
2582
2736
  .sort((a, b) => {
2583
2737
  // Oldest first
2584
- return new Date(a[1].date).getTime() - new Date(b[1].date).getTime();
2738
+ return (new Date(a[1].date).getTime() - new Date(b[1].date).getTime());
2585
2739
  });
2586
2740
  for (const [sha, entry] of newPRs) {
2587
2741
  if (dispatched >= canDispatch)
@@ -2592,7 +2746,7 @@ async function runDaemon(state, intervalMinutes = 2) {
2592
2746
  dispatched++;
2593
2747
  }
2594
2748
  catch (e) {
2595
- console.error(` ✗ PR #${entry.prNumber}: ${e}`);
2749
+ console.error(` ✗ PR #${entry.prNumber}: ${String(e)}`);
2596
2750
  }
2597
2751
  }
2598
2752
  }
@@ -2625,7 +2779,7 @@ async function main() {
2625
2779
  state = loadState();
2626
2780
  }
2627
2781
  catch (e) {
2628
- console.error(`❌ ${e}`);
2782
+ console.error(`❌ ${String(e)}`);
2629
2783
  process.exit(1);
2630
2784
  }
2631
2785
  switch (command) {
@@ -2726,11 +2880,12 @@ async function main() {
2726
2880
  cmdBundleDispatchOldest(state, Number.isFinite(rawCount) ? rawCount : undefined);
2727
2881
  break;
2728
2882
  }
2729
- case "bundle-auto":
2883
+ case "bundle-auto": {
2730
2884
  // Optional: specify minimum priority (default 70 = features+)
2731
2885
  const minPriority = args[1] ? parseInt(args[1], 10) : 70;
2732
2886
  cmdBundleAuto(state, minPriority);
2733
2887
  break;
2888
+ }
2734
2889
  case "pause":
2735
2890
  state.dispatchDisabled = true;
2736
2891
  saveState(state);
@@ -2741,8 +2896,7 @@ async function main() {
2741
2896
  saveState(state);
2742
2897
  console.log("✅ Dispatch resumed. New agents can be started.");
2743
2898
  break;
2744
- case "sync":
2745
- default:
2899
+ default: {
2746
2900
  // Full sync flow
2747
2901
  console.log("🔄 UPSTREAM SYNC\n");
2748
2902
  // 1. Fetch
@@ -2793,7 +2947,7 @@ async function main() {
2793
2947
  updateAllStatuses(state);
2794
2948
  // 4. Dispatch agents - PRIORITIZE direct commits
2795
2949
  const currentlyPorting = Object.values(state.merges).filter((e) => e.status === "porting").length;
2796
- const maxConcurrentSync = 6;
2950
+ const maxConcurrentSync = getMaxConcurrent();
2797
2951
  const canDispatchSync = Math.max(0, maxConcurrentSync - currentlyPorting);
2798
2952
  // Sort: direct commits first, then by date
2799
2953
  const newEntries = Object.entries(state.merges)
@@ -2811,11 +2965,13 @@ async function main() {
2811
2965
  for (const [sha, entry] of toDispatchSync) {
2812
2966
  try {
2813
2967
  dispatchAgent(state, sha);
2814
- const label = entry.isDirectCommit ? "⭐ DIRECT" : `PR #${entry.prNumber}`;
2968
+ const label = entry.isDirectCommit
2969
+ ? "⭐ DIRECT"
2970
+ : `PR #${entry.prNumber}`;
2815
2971
  console.log(` ✓ ${label}: ${entry.title}`);
2816
2972
  }
2817
2973
  catch (e) {
2818
- console.error(` Failed to dispatch: ${e}`);
2974
+ console.error(` Failed to dispatch: ${String(e)}`);
2819
2975
  }
2820
2976
  }
2821
2977
  }
@@ -2825,6 +2981,7 @@ async function main() {
2825
2981
  // 5. Show status
2826
2982
  printStatus(state);
2827
2983
  break;
2984
+ }
2828
2985
  }
2829
2986
  }
2830
2987
  main().catch((e) => {