@beastmode-develeap/beastmode 0.1.151 → 0.1.152

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.
package/dist/index.js CHANGED
@@ -2652,7 +2652,7 @@ var init_migrator = __esm({
2652
2652
 
2653
2653
  // src/engine/bridge.ts
2654
2654
  function generateDaemonConfig(factoryConfig, projectConfig, factoryPath) {
2655
- const pipeline = factoryConfig.pipeline;
2655
+ const pipeline2 = factoryConfig.pipeline;
2656
2656
  const models = factoryConfig.models;
2657
2657
  const resilience = factoryConfig.resilience;
2658
2658
  const cost = factoryConfig.cost;
@@ -2720,9 +2720,9 @@ function generateDaemonConfig(factoryConfig, projectConfig, factoryPath) {
2720
2720
  },
2721
2721
  verification: {
2722
2722
  enabled: true,
2723
- satisfaction_threshold: pipeline.satisfaction_threshold,
2723
+ satisfaction_threshold: pipeline2.satisfaction_threshold,
2724
2724
  timeout_minutes: 30,
2725
- prod_accept_floor: pipeline.prod_accept_floor
2725
+ prod_accept_floor: pipeline2.prod_accept_floor
2726
2726
  },
2727
2727
  review: {
2728
2728
  enabled: humanGates.pr_review !== "disabled",
@@ -2745,8 +2745,8 @@ function generateDaemonConfig(factoryConfig, projectConfig, factoryPath) {
2745
2745
  build_timeout_seconds: 600
2746
2746
  },
2747
2747
  convergence: {
2748
- max_iterations: pipeline.max_iterations,
2749
- fail_fast_threshold: pipeline.fail_fast_threshold,
2748
+ max_iterations: pipeline2.max_iterations,
2749
+ fail_fast_threshold: pipeline2.fail_fast_threshold,
2750
2750
  infra_retry_max: 3,
2751
2751
  max_precheck_retries: 2
2752
2752
  },
@@ -4343,7 +4343,7 @@ You are currently scoped to project "${scope}". Focus your answers on this proje
4343
4343
  async function runViaCli(session, content, scope = "factory") {
4344
4344
  session.busy = true;
4345
4345
  try {
4346
- const { spawn: spawn3 } = await import("child_process");
4346
+ const { spawn: spawn4 } = await import("child_process");
4347
4347
  let boardContext = "";
4348
4348
  try {
4349
4349
  const boardUrl = getBoardUrl(session.factoryPath);
@@ -4435,7 +4435,7 @@ Respond concisely. Continue the conversation naturally.`;
4435
4435
  spawnCmd = "claude";
4436
4436
  spawnArgs = claudeArgs;
4437
4437
  }
4438
- const child = spawn3(spawnCmd, spawnArgs, {
4438
+ const child = spawn4(spawnCmd, spawnArgs, {
4439
4439
  cwd: session.factoryPath,
4440
4440
  env: {
4441
4441
  ...process.env,
@@ -11401,8 +11401,8 @@ async function runMigrate(opts) {
11401
11401
  }
11402
11402
  let worktreeOutput = "";
11403
11403
  try {
11404
- const { execSync: execSync11 } = await import("child_process");
11405
- worktreeOutput = execSync11("git worktree list", {
11404
+ const { execSync: execSync12 } = await import("child_process");
11405
+ worktreeOutput = execSync12("git worktree list", {
11406
11406
  cwd,
11407
11407
  encoding: "utf-8",
11408
11408
  timeout: 5e3
@@ -11575,14 +11575,14 @@ async function runPipeline(projectName, opts) {
11575
11575
  const daemonConfig = generateDaemonConfig(factoryConfig, projectConfig, factoryDir);
11576
11576
  writeFileSync22(daemonConfigPath, JSON.stringify(daemonConfig, null, 2), "utf-8");
11577
11577
  info(`Generated daemon config at: ${daemonConfigPath}`);
11578
- const { execSync: execSync11 } = await import("child_process");
11578
+ const { execSync: execSync12 } = await import("child_process");
11579
11579
  let pythonAvailable = false;
11580
11580
  try {
11581
- execSync11("python --version", { timeout: 5e3, encoding: "utf-8" });
11581
+ execSync12("python --version", { timeout: 5e3, encoding: "utf-8" });
11582
11582
  pythonAvailable = true;
11583
11583
  } catch {
11584
11584
  try {
11585
- execSync11("python3 --version", { timeout: 5e3, encoding: "utf-8" });
11585
+ execSync12("python3 --version", { timeout: 5e3, encoding: "utf-8" });
11586
11586
  pythonAvailable = true;
11587
11587
  } catch {
11588
11588
  }
@@ -11610,8 +11610,8 @@ async function runPipeline(projectName, opts) {
11610
11610
  }
11611
11611
  const cmd = buildDaemonCommand(null, daemonConfigPath);
11612
11612
  info(`Spawning daemon: ${cmd.command} ${cmd.args.join(" ")}`);
11613
- const { spawn: spawn3 } = await import("child_process");
11614
- const child = spawn3(cmd.command, cmd.args, {
11613
+ const { spawn: spawn4 } = await import("child_process");
11614
+ const child = spawn4(cmd.command, cmd.args, {
11615
11615
  stdio: "inherit",
11616
11616
  cwd: factoryDir,
11617
11617
  env: {
@@ -11735,16 +11735,16 @@ async function runDaemon(opts) {
11735
11735
  console.log(JSON.stringify(daemonConfig, null, 2));
11736
11736
  return;
11737
11737
  }
11738
- const { execSync: execSync11 } = await import("child_process");
11738
+ const { execSync: execSync12 } = await import("child_process");
11739
11739
  let pythonCmd = "python";
11740
11740
  let pythonAvailable = false;
11741
11741
  try {
11742
- execSync11("python --version", { timeout: 5e3, encoding: "utf-8" });
11742
+ execSync12("python --version", { timeout: 5e3, encoding: "utf-8" });
11743
11743
  pythonAvailable = true;
11744
11744
  pythonCmd = "python";
11745
11745
  } catch {
11746
11746
  try {
11747
- execSync11("python3 --version", { timeout: 5e3, encoding: "utf-8" });
11747
+ execSync12("python3 --version", { timeout: 5e3, encoding: "utf-8" });
11748
11748
  pythonAvailable = true;
11749
11749
  pythonCmd = "python3";
11750
11750
  } catch {
@@ -11762,8 +11762,8 @@ async function runDaemon(opts) {
11762
11762
  info(`Starting daemon: ${pythonCmd} ${cmd.args.join(" ")}`);
11763
11763
  console.log();
11764
11764
  const pidFile = join28(bmDir, "daemon.pid");
11765
- const { spawn: spawn3 } = await import("child_process");
11766
- const child = spawn3(pythonCmd, cmd.args, {
11765
+ const { spawn: spawn4 } = await import("child_process");
11766
+ const child = spawn4(pythonCmd, cmd.args, {
11767
11767
  stdio: "inherit",
11768
11768
  cwd: factoryDir,
11769
11769
  env: {
@@ -11783,8 +11783,8 @@ async function runDaemon(opts) {
11783
11783
  const exitCode = await new Promise((resolvePromise) => {
11784
11784
  child.on("exit", (code) => {
11785
11785
  try {
11786
- const { unlinkSync: unlinkSync6 } = __require("fs");
11787
- unlinkSync6(pidFile);
11786
+ const { unlinkSync: unlinkSync7 } = __require("fs");
11787
+ unlinkSync7(pidFile);
11788
11788
  } catch {
11789
11789
  }
11790
11790
  resolvePromise(code ?? 1);
@@ -12422,7 +12422,7 @@ var updateCommand = new Command22("update").description("Pull latest BeastMode i
12422
12422
  // src/cli/commands/runner-cmd.ts
12423
12423
  init_display();
12424
12424
  import { Command as Command23 } from "commander";
12425
- import { spawn as spawn2 } from "child_process";
12425
+ import { spawn as spawn3 } from "child_process";
12426
12426
  import { resolve as resolve20 } from "path";
12427
12427
 
12428
12428
  // src/cli/github-runners.ts
@@ -12497,7 +12497,13 @@ function resolveGitHubConfig() {
12497
12497
  // src/cli/runner-helpers.ts
12498
12498
  import { execSync as execSync10, spawn, spawnSync as spawnSync5 } from "child_process";
12499
12499
  import { promises as fs } from "fs";
12500
+ import { join as join31 } from "path";
12500
12501
  init_display();
12502
+ async function writeRunnerMeta(dir, meta) {
12503
+ await fs.mkdir(dir, { recursive: true });
12504
+ const path = join31(dir, "runner-meta.json");
12505
+ await fs.writeFile(path, JSON.stringify(meta, null, 2) + "\n", "utf-8");
12506
+ }
12501
12507
  function resolveRepoSlug() {
12502
12508
  let rawUrl;
12503
12509
  try {
@@ -12687,15 +12693,326 @@ function setupStep(text) {
12687
12693
  info(text);
12688
12694
  }
12689
12695
 
12690
- // src/cli/workflow-switcher.ts
12696
+ // src/cli/native-runner.ts
12697
+ import {
12698
+ execSync as execSync11,
12699
+ spawn as spawn2,
12700
+ spawnSync as spawnSync6
12701
+ } from "child_process";
12691
12702
  import {
12703
+ createWriteStream,
12692
12704
  existsSync as existsSync33,
12693
12705
  mkdirSync as mkdirSync20,
12694
- readFileSync as readFileSync31,
12695
12706
  unlinkSync as unlinkSync5,
12696
12707
  writeFileSync as writeFileSync27
12697
12708
  } from "fs";
12698
- import { dirname as dirname8, join as join31 } from "path";
12709
+ import { homedir as homedir4 } from "os";
12710
+ import { join as join32, dirname as dirname8 } from "path";
12711
+ import { Readable } from "stream";
12712
+ import { pipeline } from "stream/promises";
12713
+ init_display();
12714
+ var RUNNER_VERSION = "2.322.0";
12715
+ function detectPlatform() {
12716
+ const platform3 = process.platform;
12717
+ if (platform3 === "win32") {
12718
+ throw new Error(
12719
+ "--native is not supported on Windows. Use Docker or WSL."
12720
+ );
12721
+ }
12722
+ if (platform3 !== "linux" && platform3 !== "darwin") {
12723
+ throw new Error(
12724
+ `--native is not supported on ${platform3}. Use Docker instead.`
12725
+ );
12726
+ }
12727
+ const arch = process.arch;
12728
+ if (arch !== "x64" && arch !== "arm64") {
12729
+ throw new Error(
12730
+ `Unsupported architecture: ${arch}. GitHub runners support x64 and arm64.`
12731
+ );
12732
+ }
12733
+ return { os: platform3, arch };
12734
+ }
12735
+ function runnerDownloadUrl(os, arch) {
12736
+ const ghOs = os === "darwin" ? "osx" : "linux";
12737
+ return `https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-${ghOs}-${arch}-${RUNNER_VERSION}.tar.gz`;
12738
+ }
12739
+ async function downloadAndExtractRunner(installDir, os, arch) {
12740
+ const runShPath = join32(installDir, "run.sh");
12741
+ if (existsSync33(runShPath)) {
12742
+ info(
12743
+ `Runner binary already present at ${runShPath} \u2014 skipping download.`
12744
+ );
12745
+ if (os === "darwin") {
12746
+ spawnSync6("xattr", ["-c", "-r", installDir], { stdio: "ignore" });
12747
+ }
12748
+ return;
12749
+ }
12750
+ const url = runnerDownloadUrl(os, arch);
12751
+ const tarball = join32(installDir, "runner.tar.gz");
12752
+ setupStep(
12753
+ `Downloading GitHub Actions runner v${RUNNER_VERSION} (${os}/${arch})...`
12754
+ );
12755
+ const resp = await fetch(url);
12756
+ if (!resp.ok || !resp.body) {
12757
+ throw new Error(
12758
+ `Failed to download runner: HTTP ${resp.status} from ${url}`
12759
+ );
12760
+ }
12761
+ await pipeline(
12762
+ Readable.fromWeb(resp.body),
12763
+ createWriteStream(tarball)
12764
+ );
12765
+ setupStep("Extracting runner...");
12766
+ try {
12767
+ execSync11("tar -xzf runner.tar.gz", { cwd: installDir, stdio: "ignore" });
12768
+ } catch (err) {
12769
+ throw new Error(
12770
+ `Failed to extract runner archive: ${err?.message ?? String(err)}`
12771
+ );
12772
+ }
12773
+ try {
12774
+ unlinkSync5(tarball);
12775
+ } catch {
12776
+ }
12777
+ if (os === "darwin") {
12778
+ setupStep("Removing macOS Gatekeeper quarantine attribute...");
12779
+ warn(
12780
+ "macOS Gatekeeper quarantine: running xattr -c -r on runner binaries."
12781
+ );
12782
+ warn(
12783
+ "This is required because macOS quarantines downloaded executables."
12784
+ );
12785
+ spawnSync6("xattr", ["-c", "-r", installDir], { stdio: "ignore" });
12786
+ }
12787
+ }
12788
+ async function configureRunner(installDir, repoUrl, token, name, labels) {
12789
+ setupStep("Configuring runner...");
12790
+ const args = [
12791
+ "--url",
12792
+ repoUrl,
12793
+ "--token",
12794
+ token,
12795
+ "--name",
12796
+ name,
12797
+ "--labels",
12798
+ labels.join(","),
12799
+ "--unattended",
12800
+ "--replace"
12801
+ ];
12802
+ const result = spawnSync6(join32(installDir, "config.sh"), args, {
12803
+ cwd: installDir,
12804
+ stdio: ["ignore", "inherit", "pipe"],
12805
+ encoding: "utf-8"
12806
+ });
12807
+ if (result.error && result.error.code === "ENOENT") {
12808
+ throw new Error(
12809
+ `Runner configuration failed: config.sh not found at ${installDir}/config.sh.`
12810
+ );
12811
+ }
12812
+ if (result.status !== 0) {
12813
+ const stderr = (result.stderr || "").replace(token, "<REDACTED>");
12814
+ throw new Error(
12815
+ `Runner configuration failed (exit ${result.status}): ${stderr}`
12816
+ );
12817
+ }
12818
+ }
12819
+ function startRunnerForeground(installDir) {
12820
+ setupStep("Starting runner in foreground mode (Ctrl+C to stop)...");
12821
+ info("The runner will stop when this terminal closes.");
12822
+ if (process.platform === "darwin") {
12823
+ warn(
12824
+ "macOS pitfall: if the terminal session disconnects, the runner dies."
12825
+ );
12826
+ warn(
12827
+ "Use --service to install as a launchd agent for persistent operation."
12828
+ );
12829
+ }
12830
+ spawn2(join32(installDir, "run.sh"), [], {
12831
+ cwd: installDir,
12832
+ stdio: "inherit"
12833
+ });
12834
+ }
12835
+ function systemdUnitContent(installDir, name) {
12836
+ return `[Unit]
12837
+ Description=GitHub Actions Runner (${name})
12838
+ After=network.target
12839
+
12840
+ [Service]
12841
+ Type=simple
12842
+ WorkingDirectory=${installDir}
12843
+ ExecStart=${installDir}/run.sh
12844
+ Restart=on-failure
12845
+ RestartSec=5
12846
+ KillSignal=SIGTERM
12847
+ TimeoutStopSec=30
12848
+
12849
+ [Install]
12850
+ WantedBy=default.target
12851
+ `;
12852
+ }
12853
+ async function installSystemdService(installDir, name) {
12854
+ const unitName = `beastmode-runner-${name}.service`;
12855
+ const unitDir = join32(homedir4(), ".config", "systemd", "user");
12856
+ mkdirSync20(unitDir, { recursive: true });
12857
+ writeFileSync27(
12858
+ join32(unitDir, unitName),
12859
+ systemdUnitContent(installDir, name),
12860
+ "utf-8"
12861
+ );
12862
+ try {
12863
+ execSync11("systemctl --user daemon-reload", { stdio: "inherit" });
12864
+ execSync11(`systemctl --user enable --now ${unitName}`, {
12865
+ stdio: "inherit"
12866
+ });
12867
+ } catch (err) {
12868
+ throw new Error(
12869
+ `systemd user-service install failed: ${err?.message ?? String(err)}. Ensure systemd --user is available (some minimal Linux containers don't ship it).`
12870
+ );
12871
+ }
12872
+ success(`Installed and started systemd user service: ${unitName}`);
12873
+ info(` Check status: systemctl --user status ${unitName}`);
12874
+ info(` View logs: journalctl --user -u ${unitName} -f`);
12875
+ }
12876
+ function launchdPlistContent(installDir, name) {
12877
+ const label = `com.beastmode.runner.${name}`;
12878
+ return `<?xml version="1.0" encoding="UTF-8"?>
12879
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
12880
+ <plist version="1.0">
12881
+ <dict>
12882
+ <key>Label</key>
12883
+ <string>${label}</string>
12884
+ <key>WorkingDirectory</key>
12885
+ <string>${installDir}</string>
12886
+ <key>ProgramArguments</key>
12887
+ <array>
12888
+ <string>${installDir}/run.sh</string>
12889
+ </array>
12890
+ <key>RunAtLoad</key>
12891
+ <true/>
12892
+ <key>KeepAlive</key>
12893
+ <dict>
12894
+ <key>SuccessfulExit</key>
12895
+ <false/>
12896
+ </dict>
12897
+ <key>StandardOutPath</key>
12898
+ <string>${installDir}/logs/stdout.log</string>
12899
+ <key>StandardErrorPath</key>
12900
+ <string>${installDir}/logs/stderr.log</string>
12901
+ <key>EnvironmentVariables</key>
12902
+ <dict>
12903
+ <key>PATH</key>
12904
+ <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin</string>
12905
+ </dict>
12906
+ </dict>
12907
+ </plist>
12908
+ `;
12909
+ }
12910
+ async function installLaunchdService(installDir, name) {
12911
+ const label = `com.beastmode.runner.${name}`;
12912
+ const plistPath2 = join32(
12913
+ homedir4(),
12914
+ "Library",
12915
+ "LaunchAgents",
12916
+ `${label}.plist`
12917
+ );
12918
+ mkdirSync20(dirname8(plistPath2), { recursive: true });
12919
+ mkdirSync20(join32(installDir, "logs"), { recursive: true });
12920
+ writeFileSync27(plistPath2, launchdPlistContent(installDir, name), "utf-8");
12921
+ try {
12922
+ execSync11(`launchctl load ${plistPath2}`, { stdio: "inherit" });
12923
+ } catch (err) {
12924
+ throw new Error(
12925
+ `launchd agent install failed: ${err?.message ?? String(err)}.`
12926
+ );
12927
+ }
12928
+ success(`Installed and loaded launchd agent: ${label}`);
12929
+ info(` Check status: launchctl list | grep ${label}`);
12930
+ info(` View logs: tail -f ${installDir}/logs/stdout.log`);
12931
+ warn("macOS pitfall: launchd agents only run while the user is logged in.");
12932
+ warn(
12933
+ "If you reboot without auto-login, the runner won't start until you log in."
12934
+ );
12935
+ warn("For headless servers, use a LaunchDaemon (requires root) instead.");
12936
+ }
12937
+ async function installRunnerService(installDir, name, os) {
12938
+ if (os === "linux") {
12939
+ await installSystemdService(installDir, name);
12940
+ } else {
12941
+ await installLaunchdService(installDir, name);
12942
+ }
12943
+ }
12944
+ async function nativeRunnerSetup(opts) {
12945
+ if (opts.service !== void 0 && !opts.native) {
12946
+ throw new Error(
12947
+ "--service requires --native. The Docker runner always runs as a container service."
12948
+ );
12949
+ }
12950
+ const { os: platformOs, arch } = detectPlatform();
12951
+ if (platformOs === "darwin") {
12952
+ header("Native Runner Setup (macOS)");
12953
+ warn(
12954
+ "macOS has several pitfalls for self-hosted runners. Read the warnings below carefully."
12955
+ );
12956
+ } else {
12957
+ header("Native Runner Setup (Linux)");
12958
+ }
12959
+ const ghConfig = resolveGitHubConfig();
12960
+ const repoSlug = opts.repo ?? resolveRepoSlug();
12961
+ const installDir = join32(homedir4(), ".beastmode", "runners", opts.name);
12962
+ if (opts.dryRun) {
12963
+ info(
12964
+ `[dry-run] Would install native runner '${opts.name}' for repo ${repoSlug}`
12965
+ );
12966
+ info(`[dry-run] Platform: ${platformOs}/${arch}`);
12967
+ info(`[dry-run] Install dir: ${installDir}`);
12968
+ info(`[dry-run] Mode: ${opts.service ? "service" : "foreground"}`);
12969
+ return;
12970
+ }
12971
+ mkdirSync20(installDir, { recursive: true });
12972
+ setupStep("Generating registration token via GitHub API...");
12973
+ const { token: regToken } = await createRegistrationToken(ghConfig);
12974
+ await downloadAndExtractRunner(installDir, platformOs, arch);
12975
+ const repoUrl = `https://github.com/${ghConfig.owner}/${ghConfig.repo}`;
12976
+ await configureRunner(installDir, repoUrl, regToken, opts.name, [
12977
+ "self-hosted",
12978
+ opts.label
12979
+ ]);
12980
+ await writeEnvEntries({
12981
+ RUNNER_REPO_URL: repoUrl,
12982
+ RUNNER_TOKEN: regToken,
12983
+ RUNNER_NAME: opts.name,
12984
+ RUNNER_MODE: "native"
12985
+ });
12986
+ await writeRunnerMeta(installDir, {
12987
+ name: opts.name,
12988
+ mode: "native",
12989
+ platform: platformOs,
12990
+ arch,
12991
+ installDir,
12992
+ service: opts.service ?? false,
12993
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
12994
+ });
12995
+ if (opts.service) {
12996
+ await installRunnerService(installDir, opts.name, platformOs);
12997
+ } else {
12998
+ startRunnerForeground(installDir);
12999
+ }
13000
+ setupStep("Waiting for runner to appear online on GitHub (timeout 60s)...");
13001
+ await pollUntilOnline(ghConfig, opts.name, 6e4);
13002
+ success(
13003
+ `Runner '${opts.name}' registered and online (native/${platformOs}).`
13004
+ );
13005
+ }
13006
+
13007
+ // src/cli/workflow-switcher.ts
13008
+ import {
13009
+ existsSync as existsSync34,
13010
+ mkdirSync as mkdirSync21,
13011
+ readFileSync as readFileSync31,
13012
+ unlinkSync as unlinkSync6,
13013
+ writeFileSync as writeFileSync28
13014
+ } from "fs";
13015
+ import { dirname as dirname9, join as join33 } from "path";
12699
13016
  var TARGET_LABEL = "[self-hosted, beastmode]";
12700
13017
  var TARGET_WORKFLOWS = [
12701
13018
  ".github/workflows/test.yml",
@@ -12734,8 +13051,8 @@ function restoreRunsOn(content, originals) {
12734
13051
  return newLines.join("\n");
12735
13052
  }
12736
13053
  async function switchWorkflows(projectDir) {
12737
- const statePath = join31(projectDir, STATE_FILE);
12738
- if (existsSync33(statePath)) {
13054
+ const statePath = join33(projectDir, STATE_FILE);
13055
+ if (existsSync34(statePath)) {
12739
13056
  return { alreadySwitched: true, files: [] };
12740
13057
  }
12741
13058
  const state = {
@@ -12745,8 +13062,8 @@ async function switchWorkflows(projectDir) {
12745
13062
  };
12746
13063
  const resultFiles = [];
12747
13064
  for (const relPath of TARGET_WORKFLOWS) {
12748
- const absPath = join31(projectDir, relPath);
12749
- if (!existsSync33(absPath)) {
13065
+ const absPath = join33(projectDir, relPath);
13066
+ if (!existsSync34(absPath)) {
12750
13067
  throw new Error(`Workflow file not found: ${relPath}`);
12751
13068
  }
12752
13069
  const content = readFileSync31(absPath, "utf-8");
@@ -12754,17 +13071,17 @@ async function switchWorkflows(projectDir) {
12754
13071
  if (originals.length === 0) {
12755
13072
  throw new Error(`No runs-on found in ${relPath}`);
12756
13073
  }
12757
- writeFileSync27(absPath, newContent, "utf-8");
13074
+ writeFileSync28(absPath, newContent, "utf-8");
12758
13075
  state.files.push({ relativePath: relPath, originals });
12759
13076
  resultFiles.push({ relativePath: relPath, jobCount: originals.length });
12760
13077
  }
12761
- mkdirSync20(dirname8(statePath), { recursive: true });
12762
- writeFileSync27(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
13078
+ mkdirSync21(dirname9(statePath), { recursive: true });
13079
+ writeFileSync28(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
12763
13080
  return { alreadySwitched: false, files: resultFiles };
12764
13081
  }
12765
13082
  async function restoreWorkflows(projectDir) {
12766
- const statePath = join31(projectDir, STATE_FILE);
12767
- if (!existsSync33(statePath)) {
13083
+ const statePath = join33(projectDir, STATE_FILE);
13084
+ if (!existsSync34(statePath)) {
12768
13085
  return { nothingToRestore: true, files: [] };
12769
13086
  }
12770
13087
  const state = JSON.parse(
@@ -12772,27 +13089,32 @@ async function restoreWorkflows(projectDir) {
12772
13089
  );
12773
13090
  const resultFiles = [];
12774
13091
  for (const fileState of state.files) {
12775
- const absPath = join31(projectDir, fileState.relativePath);
12776
- if (!existsSync33(absPath)) {
13092
+ const absPath = join33(projectDir, fileState.relativePath);
13093
+ if (!existsSync34(absPath)) {
12777
13094
  throw new Error(`Workflow file not found: ${fileState.relativePath}`);
12778
13095
  }
12779
13096
  const content = readFileSync31(absPath, "utf-8");
12780
13097
  const newContent = restoreRunsOn(content, fileState.originals);
12781
- writeFileSync27(absPath, newContent, "utf-8");
13098
+ writeFileSync28(absPath, newContent, "utf-8");
12782
13099
  resultFiles.push({
12783
13100
  relativePath: fileState.relativePath,
12784
13101
  jobCount: fileState.originals.length
12785
13102
  });
12786
13103
  }
12787
- unlinkSync5(statePath);
13104
+ unlinkSync6(statePath);
12788
13105
  return { nothingToRestore: false, files: resultFiles };
12789
13106
  }
12790
13107
 
12791
13108
  // src/cli/commands/runner-cmd.ts
12792
13109
  var runnerCommand = new Command23("runner").description("Manage self-hosted GitHub Actions runners");
12793
13110
  async function runnerSetupAction(opts) {
13111
+ if (opts.service !== void 0 && !opts.native) {
13112
+ throw new Error(
13113
+ "--service requires --native. The Docker runner always runs as a container service."
13114
+ );
13115
+ }
12794
13116
  if (opts.native) {
12795
- warn("--native is not implemented yet (Story 5)");
13117
+ await nativeRunnerSetup(opts);
12796
13118
  return;
12797
13119
  }
12798
13120
  const ghConfig = resolveGitHubConfig();
@@ -12833,11 +13155,19 @@ async function runnerSetupAction(opts) {
12833
13155
  await pollUntilOnline(ghConfig, opts.name, 6e4);
12834
13156
  setupStep("Adding runner to docker-compose...");
12835
13157
  await composeUpRunner();
13158
+ await writeRunnerMeta(".beastmode", {
13159
+ name: opts.name,
13160
+ mode: "docker",
13161
+ platform: process.platform,
13162
+ arch: process.arch,
13163
+ service: true,
13164
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
13165
+ });
12836
13166
  success(`Runner '${opts.name}' registered and online.`);
12837
13167
  }
12838
13168
  async function composeUpRunner() {
12839
13169
  await new Promise((resolve21, reject) => {
12840
- const child = spawn2(
13170
+ const child = spawn3(
12841
13171
  "docker",
12842
13172
  ["compose", "--profile", "runner", "up", "-d", "runner"],
12843
13173
  { stdio: ["ignore", "inherit", "inherit"] }
@@ -12854,8 +13184,19 @@ async function composeUpRunner() {
12854
13184
  });
12855
13185
  });
12856
13186
  }
12857
- runnerCommand.command("setup").description("Set up a self-hosted GitHub Actions runner").option("--repo <owner/repo>", "GitHub repo for runner registration").option("--name <name>", "Container + runner name", "beastmode-runner").option("--label <label>", "Additional runner label", "beastmode").option("--dry-run", "Print what would happen without mutating state").option("--native", "Use native install instead of Docker").action(async (opts) => {
12858
- await runnerSetupAction(opts);
13187
+ runnerCommand.command("setup").description("Set up a self-hosted GitHub Actions runner").option("--repo <owner/repo>", "GitHub repo for runner registration").option("--name <name>", "Container + runner name", "beastmode-runner").option("--label <label>", "Additional runner label", "beastmode").option("--dry-run", "Print what would happen without mutating state").option(
13188
+ "--native",
13189
+ "Use native runner binary instead of Docker (downloads actions/runner)"
13190
+ ).option(
13191
+ "--service",
13192
+ "Install as background service (systemd/launchd). Requires --native."
13193
+ ).action(async (opts) => {
13194
+ try {
13195
+ await runnerSetupAction(opts);
13196
+ } catch (err) {
13197
+ error(err?.message ? String(err.message) : String(err));
13198
+ process.exitCode = 1;
13199
+ }
12859
13200
  });
12860
13201
  async function runnerStatusAction(opts) {
12861
13202
  const container = await findContainerByName(opts.name);