agent-control-plane 0.2.0 → 0.4.9

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 (59) hide show
  1. package/README.md +69 -19
  2. package/assets/workflow-catalog.json +1 -1
  3. package/bin/pr-risk.sh +22 -7
  4. package/bin/sync-pr-labels.sh +1 -1
  5. package/hooks/heartbeat-hooks.sh +125 -12
  6. package/hooks/issue-reconcile-hooks.sh +1 -1
  7. package/hooks/pr-reconcile-hooks.sh +1 -1
  8. package/npm/bin/agent-control-plane.js +296 -61
  9. package/package.json +11 -7
  10. package/tools/bin/agent-github-update-labels +36 -2
  11. package/tools/bin/agent-project-catch-up-merged-prs +4 -2
  12. package/tools/bin/agent-project-cleanup-session +49 -5
  13. package/tools/bin/agent-project-heartbeat-loop +119 -1471
  14. package/tools/bin/agent-project-publish-issue-pr +6 -3
  15. package/tools/bin/agent-project-reconcile-issue-session +78 -106
  16. package/tools/bin/agent-project-reconcile-pr-session +166 -143
  17. package/tools/bin/agent-project-retry-state +18 -7
  18. package/tools/bin/agent-project-run-claude-session +10 -0
  19. package/tools/bin/agent-project-run-codex-resilient +99 -14
  20. package/tools/bin/agent-project-run-codex-session +16 -5
  21. package/tools/bin/agent-project-run-kilo-session +10 -0
  22. package/tools/bin/agent-project-run-openclaw-session +10 -0
  23. package/tools/bin/agent-project-run-opencode-session +10 -0
  24. package/tools/bin/agent-project-sync-source-repo-main +163 -0
  25. package/tools/bin/agent-project-worker-status +10 -7
  26. package/tools/bin/cleanup-worktree.sh +6 -1
  27. package/tools/bin/flow-config-lib.sh +1257 -34
  28. package/tools/bin/flow-resident-worker-lib.sh +119 -1
  29. package/tools/bin/flow-shell-lib.sh +56 -0
  30. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  31. package/tools/bin/github-write-outbox.sh +470 -0
  32. package/tools/bin/heartbeat-loop-cache-lib.sh +164 -0
  33. package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
  34. package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
  35. package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
  36. package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
  37. package/tools/bin/heartbeat-recovery-preflight.sh +12 -1
  38. package/tools/bin/heartbeat-safe-auto.sh +56 -3
  39. package/tools/bin/install-project-launchd.sh +17 -2
  40. package/tools/bin/project-init.sh +21 -1
  41. package/tools/bin/project-launchd-bootstrap.sh +16 -9
  42. package/tools/bin/project-runtimectl.sh +46 -2
  43. package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
  44. package/tools/bin/resident-issue-controller-lib.sh +448 -0
  45. package/tools/bin/scaffold-profile.sh +61 -3
  46. package/tools/bin/start-pr-fix-worker.sh +47 -10
  47. package/tools/bin/start-resident-issue-loop.sh +28 -439
  48. package/tools/dashboard/app.js +37 -1
  49. package/tools/dashboard/dashboard_snapshot.py +65 -26
  50. package/tools/templates/pr-fix-template.md +3 -1
  51. package/tools/templates/pr-merge-repair-template.md +2 -1
  52. package/SKILL.md +0 -149
  53. package/references/architecture.md +0 -217
  54. package/references/commands.md +0 -128
  55. package/references/control-plane-map.md +0 -124
  56. package/references/docs-map.md +0 -73
  57. package/references/release-checklist.md +0 -65
  58. package/references/repo-map.md +0 -36
  59. package/tools/bin/split-retained-slice.sh +0 -124
@@ -45,11 +45,17 @@ Usage:
45
45
 
46
46
  Guided install/bootstrap flow for one repo profile. It can detect the current
47
47
  repo, suggest sane runtime paths, sync ACP into ~/.agent-runtime, scaffold one
48
- profile, run doctor checks, and optionally start the runtime.
48
+ profile, run doctor checks, and optionally start the runtime. ACP can run
49
+ GitHub-first or local-first with Gitea as the working forge.
49
50
 
50
51
  Options:
51
52
  --profile-id <id> Profile id to create or refresh
52
- --repo-slug <owner/repo> GitHub repo slug
53
+ --repo-slug <owner/repo> Forge repo slug
54
+ --forge-provider <provider> One of: github, gitea
55
+ --gitea-base-url <url> Base URL for a local/self-hosted Gitea instance
56
+ --gitea-token <token> Gitea API token written to profile runtime.env
57
+ --gitea-username <user> Gitea username written to profile runtime.env
58
+ --gitea-password <pass> Gitea password written to profile runtime.env
53
59
  --repo-root <path> Local checkout to manage (defaults to current git root)
54
60
  --agent-root <path> ACP-managed runtime root for this profile
55
61
  --agent-repo-root <path> Clean ACP-managed anchor repo root
@@ -380,6 +386,11 @@ function parseSetupArgs(args) {
380
386
  const options = {
381
387
  profileId: "",
382
388
  repoSlug: "",
389
+ forgeProvider: "",
390
+ giteaBaseUrl: "",
391
+ giteaToken: "",
392
+ giteaUsername: "",
393
+ giteaPassword: "",
383
394
  repoRoot: "",
384
395
  agentRoot: "",
385
396
  agentRepoRoot: "",
@@ -415,6 +426,21 @@ function parseSetupArgs(args) {
415
426
  case "--repo-slug":
416
427
  options.repoSlug = args[++index] || "";
417
428
  break;
429
+ case "--forge-provider":
430
+ options.forgeProvider = args[++index] || "";
431
+ break;
432
+ case "--gitea-base-url":
433
+ options.giteaBaseUrl = args[++index] || "";
434
+ break;
435
+ case "--gitea-token":
436
+ options.giteaToken = args[++index] || "";
437
+ break;
438
+ case "--gitea-username":
439
+ options.giteaUsername = args[++index] || "";
440
+ break;
441
+ case "--gitea-password":
442
+ options.giteaPassword = args[++index] || "";
443
+ break;
418
444
  case "--repo-root":
419
445
  options.repoRoot = args[++index] || "";
420
446
  break;
@@ -581,7 +607,7 @@ Agent schedule: every 4h
581
607
 
582
608
  - Run \`pnpm typecheck\` (or the repo-equivalent lint/typecheck) after code changes and record verification.
583
609
  `,
584
- labels: ["agent-ready", "agent-keep-open"]
610
+ labels: ["agent-keep-open"]
585
611
  },
586
612
  {
587
613
  key: "test-coverage",
@@ -597,7 +623,7 @@ Agent schedule: every 6h
597
623
 
598
624
  - Run the narrowest relevant test command after code changes and record verification.
599
625
  `,
600
- labels: ["agent-ready", "agent-keep-open"]
626
+ labels: ["agent-keep-open"]
601
627
  },
602
628
  {
603
629
  key: "documentation",
@@ -613,7 +639,7 @@ Agent schedule: every 8h
613
639
 
614
640
  - Run any doc-build or link-check command after changes and record verification.
615
641
  `,
616
- labels: ["agent-ready", "agent-keep-open"]
642
+ labels: ["agent-keep-open"]
617
643
  },
618
644
  {
619
645
  key: "dependency-audit",
@@ -629,7 +655,7 @@ Agent schedule: every 12h
629
655
 
630
656
  - Run \`pnpm audit\` and \`pnpm install --frozen-lockfile\` after changes and record verification.
631
657
  `,
632
- labels: ["agent-ready", "agent-keep-open"]
658
+ labels: ["agent-keep-open"]
633
659
  },
634
660
  {
635
661
  key: "refactor",
@@ -645,7 +671,7 @@ Agent schedule: every 8h
645
671
 
646
672
  - Run the narrowest relevant test command after refactoring and record verification.
647
673
  `,
648
- labels: ["agent-ready", "agent-keep-open"]
674
+ labels: ["agent-keep-open"]
649
675
  }
650
676
  ];
651
677
 
@@ -656,8 +682,8 @@ async function maybeCreateStarterIssues(options, config, prereq) {
656
682
  return result;
657
683
  }
658
684
 
659
- if (!prereq.ghAuthOk) {
660
- result.reason = "gh-auth-not-ready";
685
+ if (!prereq.forgeAuthOk) {
686
+ result.reason = `${config.forge.provider}-auth-not-ready`;
661
687
  return result;
662
688
  }
663
689
 
@@ -701,24 +727,28 @@ async function maybeCreateStarterIssues(options, config, prereq) {
701
727
  // Ensure labels exist
702
728
  console.log("\nCreating labels and issues...");
703
729
  const requiredLabels = [
704
- { name: "agent-ready", color: "0E8A16", description: "Ready for agent automation" },
705
730
  { name: "agent-keep-open", color: "D4C5F9", description: "Recurring issue — agent works on this continuously" }
706
731
  ];
707
- for (const label of requiredLabels) {
708
- spawnSync("gh", ["label", "create", label.name, "--repo", config.repoSlug, "--description", label.description, "--color", label.color, "--force"], { stdio: "pipe", timeout: 15000 });
732
+ if (config.forge.provider === "gitea") {
733
+ for (const label of requiredLabels) {
734
+ runForgeFlowConfigCommand(config, [
735
+ "flow_github_label_create",
736
+ shellQuote(config.repoSlug),
737
+ shellQuote(label.name),
738
+ shellQuote(label.description),
739
+ shellQuote(label.color)
740
+ ]);
741
+ }
742
+ } else {
743
+ for (const label of requiredLabels) {
744
+ spawnSync("gh", ["label", "create", label.name, "--repo", config.repoSlug, "--description", label.description, "--color", label.color, "--force"], { stdio: "pipe", timeout: 15000 });
745
+ }
709
746
  }
710
747
 
711
748
  for (const { issue } of toCreate) {
712
- const ghArgs = [
713
- "issue", "create",
714
- "--repo", config.repoSlug,
715
- "--title", issue.title,
716
- "--body", issue.body,
717
- "--label", issue.labels.join(",")
718
- ];
719
- const ghResult = spawnSync("gh", ghArgs, { encoding: "utf8", stdio: "pipe", timeout: 30000 });
720
- if (ghResult.status === 0) {
721
- const url = (ghResult.stdout || "").trim();
749
+ const createResult = createStarterIssueOnForge(config, issue);
750
+ if (createResult.status === 0) {
751
+ const url = (createResult.stdout || "").trim();
722
752
  result.created.push({ key: issue.key, title: issue.title, url });
723
753
  console.log(` Created: ${issue.title}`);
724
754
  if (url) {
@@ -726,7 +756,7 @@ async function maybeCreateStarterIssues(options, config, prereq) {
726
756
  }
727
757
  } else {
728
758
  console.log(` Failed to create: ${issue.title}`);
729
- const stderr = (ghResult.stderr || "").trim();
759
+ const stderr = (createResult.stderr || "").trim();
730
760
  if (stderr) {
731
761
  console.log(` ${stderr.split("\n")[0]}`);
732
762
  }
@@ -739,6 +769,87 @@ async function maybeCreateStarterIssues(options, config, prereq) {
739
769
  return result;
740
770
  }
741
771
 
772
+ function buildForgeCommandEnv(config) {
773
+ const env = { ...process.env };
774
+ env.ACP_FORGE_PROVIDER = config.forge.provider;
775
+ env.F_LOSNING_FORGE_PROVIDER = config.forge.provider;
776
+ if (config.forge.provider === "gitea") {
777
+ if (config.forge.giteaBaseUrl) {
778
+ env.ACP_GITEA_BASE_URL = config.forge.giteaBaseUrl;
779
+ env.GITEA_BASE_URL = config.forge.giteaBaseUrl;
780
+ }
781
+ if (config.forge.giteaToken) {
782
+ env.ACP_GITEA_TOKEN = config.forge.giteaToken;
783
+ env.GITEA_TOKEN = config.forge.giteaToken;
784
+ }
785
+ if (config.forge.giteaUsername) {
786
+ env.ACP_GITEA_USERNAME = config.forge.giteaUsername;
787
+ env.GITEA_USERNAME = config.forge.giteaUsername;
788
+ }
789
+ if (config.forge.giteaPassword) {
790
+ env.ACP_GITEA_PASSWORD = config.forge.giteaPassword;
791
+ env.GITEA_PASSWORD = config.forge.giteaPassword;
792
+ }
793
+ }
794
+ return env;
795
+ }
796
+
797
+ function runForgeFlowConfigCommand(config, commandParts, extraEnv = {}) {
798
+ const flowConfigLib = path.join(packageRoot, "tools", "bin", "flow-config-lib.sh");
799
+ const script = `set -euo pipefail\nsource ${shellQuote(flowConfigLib)}\n${commandParts.join(" ")}\n`;
800
+ return spawnSync("bash", ["-lc", script], {
801
+ encoding: "utf8",
802
+ stdio: "pipe",
803
+ timeout: 30000,
804
+ env: {
805
+ ...buildForgeCommandEnv(config),
806
+ ...extraEnv
807
+ }
808
+ });
809
+ }
810
+
811
+ function createStarterIssueOnForge(config, issue) {
812
+ if (config.forge.provider !== "gitea") {
813
+ const ghArgs = [
814
+ "issue", "create",
815
+ "--repo", config.repoSlug,
816
+ "--title", issue.title,
817
+ "--body", issue.body,
818
+ "--label", issue.labels.join(",")
819
+ ];
820
+ return spawnSync("gh", ghArgs, { encoding: "utf8", stdio: "pipe", timeout: 30000 });
821
+ }
822
+
823
+ const bodyFile = fs.mkdtempSync(path.join(os.tmpdir(), "acp-starter-issue-"));
824
+ const bodyPath = path.join(bodyFile, "body.md");
825
+ fs.writeFileSync(bodyPath, issue.body);
826
+ try {
827
+ const createResult = runForgeFlowConfigCommand(
828
+ config,
829
+ ["flow_github_issue_create", shellQuote(config.repoSlug), "$ACP_ISSUE_TITLE", "$ACP_ISSUE_BODY_FILE"],
830
+ {
831
+ ACP_ISSUE_TITLE: issue.title,
832
+ ACP_ISSUE_BODY_FILE: bodyPath
833
+ }
834
+ );
835
+ const issueUrl = (createResult.stdout || "").trim();
836
+ if (createResult.status === 0 && issueUrl && issue.labels.length > 0) {
837
+ const issueNumber = issueUrl.split("/").pop();
838
+ for (const labelName of issue.labels) {
839
+ spawnSync("bash", [path.join(packageRoot, "tools", "bin", "agent-github-update-labels"), "--repo-slug", config.repoSlug, "--number", issueNumber, "--add", labelName], {
840
+ encoding: "utf8",
841
+ stdio: "pipe",
842
+ timeout: 30000,
843
+ env: buildForgeCommandEnv(config)
844
+ });
845
+ }
846
+ }
847
+ return createResult;
848
+ } finally {
849
+ fs.rmSync(bodyFile, { recursive: true, force: true });
850
+ }
851
+ }
852
+
742
853
  function buildSetupPaths(platformHome, repoRoot, profileId, overrides) {
743
854
  const agentRoot = path.resolve(overrides.agentRoot || path.join(platformHome, "projects", profileId));
744
855
  const repoRootResolved = path.resolve(repoRoot);
@@ -753,12 +864,27 @@ function buildSetupPaths(platformHome, repoRoot, profileId, overrides) {
753
864
  };
754
865
  }
755
866
 
756
- function collectPrereqStatus(codingWorker) {
757
- const requiredTools = ["bash", "git", "gh", "jq", "python3", "tmux"];
867
+ function collectPrereqStatus(codingWorker, forge) {
868
+ const forgeProvider = (forge && forge.provider) || "github";
869
+ const requiredTools = ["bash", "git", "jq", "python3", "tmux"];
870
+ if (forgeProvider !== "gitea") {
871
+ requiredTools.push("gh");
872
+ }
758
873
  const missingRequired = requiredTools.filter((tool) => !commandExists(tool));
759
874
  const workerCommand = codingWorker;
760
875
  const workerAvailable = commandExists(workerCommand);
761
876
  const ghAuthResult = commandExists("gh") ? runCapture("gh", ["auth", "status"]) : { status: 1, stdout: "", stderr: "" };
877
+ const giteaAuthOk = forgeProvider !== "gitea"
878
+ ? false
879
+ : Boolean(
880
+ forge &&
881
+ forge.giteaBaseUrl &&
882
+ (
883
+ forge.giteaToken ||
884
+ (forge.giteaUsername && forge.giteaPassword)
885
+ )
886
+ );
887
+ const forgeAuthOk = forgeProvider === "gitea" ? giteaAuthOk : ghAuthResult.status === 0;
762
888
 
763
889
  return {
764
890
  missingRequired,
@@ -766,6 +892,8 @@ function collectPrereqStatus(codingWorker) {
766
892
  workerCommand,
767
893
  workerAvailable,
768
894
  ghAuthOk: ghAuthResult.status === 0,
895
+ forgeProvider,
896
+ forgeAuthOk,
769
897
  ghAuthOutput: `${ghAuthResult.stdout}${ghAuthResult.stderr}`.trim()
770
898
  };
771
899
  }
@@ -774,6 +902,9 @@ function detectPackageManager() {
774
902
  if (commandExists("brew")) {
775
903
  return { name: "brew" };
776
904
  }
905
+ if (commandExists("apt")) {
906
+ return { name: "apt" };
907
+ }
777
908
  if (commandExists("apt-get")) {
778
909
  return { name: "apt-get" };
779
910
  }
@@ -789,6 +920,9 @@ function detectPackageManager() {
789
920
  if (commandExists("zypper")) {
790
921
  return { name: "zypper" };
791
922
  }
923
+ if (commandExists("apk")) {
924
+ return { name: "apk" };
925
+ }
792
926
  return null;
793
927
  }
794
928
 
@@ -812,6 +946,24 @@ function dependencyPackageMap(managerName) {
812
946
  python3: "python3",
813
947
  tmux: "tmux"
814
948
  };
949
+ case "apt":
950
+ return {
951
+ bash: "bash",
952
+ git: "git",
953
+ gh: "gh",
954
+ jq: "jq",
955
+ python3: "python3",
956
+ tmux: "tmux"
957
+ };
958
+ case "apk":
959
+ return {
960
+ bash: "bash",
961
+ git: "git",
962
+ gh: "gh",
963
+ jq: "jq",
964
+ python3: "python3",
965
+ tmux: "tmux"
966
+ };
815
967
  case "dnf":
816
968
  case "yum":
817
969
  return {
@@ -870,6 +1022,10 @@ function buildDependencyInstallPlan(missingTools) {
870
1022
  commands.push([...prefix, "apt-get", "update"]);
871
1023
  commands.push([...prefix, "apt-get", "install", "-y", ...packages]);
872
1024
  break;
1025
+ case "apt":
1026
+ commands.push([...prefix, "apt", "update"]);
1027
+ commands.push([...prefix, "apt", "install", "-y", ...packages]);
1028
+ break;
873
1029
  case "dnf":
874
1030
  commands.push([...prefix, "dnf", "install", "-y", ...packages]);
875
1031
  break;
@@ -882,6 +1038,9 @@ function buildDependencyInstallPlan(missingTools) {
882
1038
  case "zypper":
883
1039
  commands.push([...prefix, "zypper", "install", "-y", ...packages]);
884
1040
  break;
1041
+ case "apk":
1042
+ commands.push([...prefix, "apk", "add", "--no-cache", ...packages]);
1043
+ break;
885
1044
  default:
886
1045
  return null;
887
1046
  }
@@ -923,7 +1082,7 @@ async function maybeInstallMissingDependencies(options, prereq) {
923
1082
  if (!plan) {
924
1083
  console.log("\nACP found missing core dependencies but cannot install them automatically on this machine.");
925
1084
  console.log(`- missing tools: ${prereq.missingRequired.join(", ")}`);
926
- console.log("- supported auto-install package managers today: brew, apt-get, dnf, yum, pacman, zypper");
1085
+ console.log("- supported auto-install package managers today: brew, apt, apt-get, dnf, yum, pacman, zypper, apk");
927
1086
  return {
928
1087
  status: "unavailable",
929
1088
  reason: "no-supported-package-manager",
@@ -990,6 +1149,9 @@ async function maybeInstallMissingDependencies(options, prereq) {
990
1149
  }
991
1150
 
992
1151
  async function maybeRunGithubAuthLogin(options, prereq) {
1152
+ if (prereq.forgeProvider === "gitea") {
1153
+ return { status: prereq.forgeAuthOk ? "not-needed" : "skipped", reason: prereq.forgeAuthOk ? "" : "gitea-auth-not-ready" };
1154
+ }
993
1155
  if (prereq.ghAuthOk) {
994
1156
  return { status: "not-needed", reason: "" };
995
1157
  }
@@ -1326,7 +1488,7 @@ function printPrereqSummary(prereq) {
1326
1488
  console.log("\nPrerequisite check");
1327
1489
  console.log(`- core tools: ${prereq.coreToolsOk ? "ok" : `missing ${prereq.missingRequired.join(", ")}`}`);
1328
1490
  console.log(`- worker backend (${prereq.workerCommand}): ${prereq.workerAvailable ? "found" : "missing on PATH"}`);
1329
- console.log(`- GitHub auth: ${prereq.ghAuthOk ? "ok" : "not ready"}`);
1491
+ console.log(`- ${prereq.forgeProvider === "gitea" ? "Gitea auth" : "GitHub auth"}: ${prereq.forgeAuthOk ? "ok" : "not ready"}`);
1330
1492
  console.log(`- backend note: ${backendReadinessHint(prereq.workerCommand)}`);
1331
1493
  }
1332
1494
 
@@ -1434,7 +1596,11 @@ function buildScopedContext(context, profileId) {
1434
1596
  function renderSetupSummary(config) {
1435
1597
  console.log("\nSetup plan");
1436
1598
  console.log(`- profile id: ${config.profileId}`);
1599
+ console.log(`- forge provider: ${config.forge.provider}`);
1437
1600
  console.log(`- repo slug: ${config.repoSlug}`);
1601
+ if (config.forge.provider === "gitea") {
1602
+ console.log(`- gitea base url: ${config.forge.giteaBaseUrl || "(not set)"}`);
1603
+ }
1438
1604
  console.log(`- repo root: ${config.paths.repoRoot}`);
1439
1605
  console.log(`- agent root: ${config.paths.agentRoot}`);
1440
1606
  console.log(`- agent repo root: ${config.paths.agentRepoRoot}`);
@@ -1442,6 +1608,10 @@ function renderSetupSummary(config) {
1442
1608
  console.log(`- coding worker: ${config.codingWorker}`);
1443
1609
  }
1444
1610
 
1611
+ function forgeAuthLabel(config) {
1612
+ return config.forge.provider === "gitea" ? "Gitea auth" : "GitHub auth";
1613
+ }
1614
+
1445
1615
  function planStatusWithReason(status, reason = "") {
1446
1616
  return {
1447
1617
  status,
@@ -1483,8 +1653,10 @@ function buildSetupDryRunPlan(options, context, config) {
1483
1653
  }
1484
1654
 
1485
1655
  let githubAuthAction = planStatusWithReason("not-needed");
1486
- if (!prereq.ghAuthOk) {
1487
- if (!commandExists("gh")) {
1656
+ if (!prereq.forgeAuthOk) {
1657
+ if (config.forge.provider === "gitea") {
1658
+ githubAuthAction = planStatusWithReason("blocked", "gitea-auth-not-ready");
1659
+ } else if (!commandExists("gh")) {
1488
1660
  githubAuthAction = planStatusWithReason("blocked", "gh-missing");
1489
1661
  } else if (options.ghAuthLogin === false) {
1490
1662
  githubAuthAction = planStatusWithReason("skipped", "disabled");
@@ -1503,8 +1675,8 @@ function buildSetupDryRunPlan(options, context, config) {
1503
1675
  runtimeStartAction = planStatusWithReason("blocked", `missing-worker:${prereq.workerCommand}`);
1504
1676
  } else if (anchorSync.status !== "ok") {
1505
1677
  runtimeStartAction = planStatusWithReason("blocked", `anchor-sync-${anchorSync.reason}`);
1506
- } else if (!prereq.ghAuthOk) {
1507
- runtimeStartAction = planStatusWithReason("blocked", "gh-auth-not-ready");
1678
+ } else if (!prereq.forgeAuthOk) {
1679
+ runtimeStartAction = planStatusWithReason("blocked", `${config.forge.provider}-auth-not-ready`);
1508
1680
  } else {
1509
1681
  runtimeStartAction = planStatusWithReason("would-run");
1510
1682
  }
@@ -1556,9 +1728,13 @@ function printSetupDryRunPlan(context, config, plan) {
1556
1728
  if (plan.workerAction.status !== "not-needed" && plan.workerInstallPlan && plan.workerInstallPlan.commands.length > 0) {
1557
1729
  console.log(` command preview: ${plan.workerInstallPlan.commands.map(formatCommand).join(" && ")}`);
1558
1730
  }
1559
- console.log(`- GitHub auth step: ${plan.githubAuthAction.status}${plan.githubAuthAction.reason ? ` (${plan.githubAuthAction.reason})` : ""}`);
1731
+ console.log(`- ${forgeAuthLabel(config)} step: ${plan.githubAuthAction.status}${plan.githubAuthAction.reason ? ` (${plan.githubAuthAction.reason})` : ""}`);
1560
1732
  if (plan.githubAuthAction.status !== "not-needed") {
1561
- console.log(` command preview: gh auth login`);
1733
+ if (config.forge.provider === "github") {
1734
+ console.log(` command preview: gh auth login`);
1735
+ } else {
1736
+ console.log(` next step: pass --gitea-base-url plus --gitea-token or --gitea-username/--gitea-password`);
1737
+ }
1562
1738
  }
1563
1739
  console.log(`- runtime start: ${plan.runtimeStartAction.status}${plan.runtimeStartAction.reason ? ` (${plan.runtimeStartAction.reason})` : ""}`);
1564
1740
  if (process.platform === "darwin") {
@@ -1572,6 +1748,7 @@ function buildSetupResultPayload(params) {
1572
1748
  setupMode: params.setupMode,
1573
1749
  profileId: params.profileId,
1574
1750
  repoSlug: params.repoSlug,
1751
+ forge: params.forge,
1575
1752
  codingWorker: params.codingWorker,
1576
1753
  profileExists: params.profileExists,
1577
1754
  paths: {
@@ -1648,8 +1825,8 @@ function collectFinalSetupIssues(config, prereq, doctorKv, runtimeStartStatus) {
1648
1825
  if (!prereq.workerAvailable) {
1649
1826
  issues.push(`missing worker backend on PATH: ${prereq.workerCommand}`);
1650
1827
  }
1651
- if (!prereq.ghAuthOk) {
1652
- issues.push("GitHub CLI is not authenticated");
1828
+ if (!prereq.forgeAuthOk) {
1829
+ issues.push(config.forge.provider === "gitea" ? "Gitea auth is not configured" : "GitHub CLI is not authenticated");
1653
1830
  }
1654
1831
  if ((doctorKv.DOCTOR_STATUS || "") !== "ok") {
1655
1832
  issues.push(`doctor status is ${doctorKv.DOCTOR_STATUS || "unknown"}`);
@@ -1692,8 +1869,12 @@ async function maybeRunFinalSetupFixups(options, scopedContext, config, currentS
1692
1869
  else if (worker === "claude") console.log(" Fix: npm install -g @anthropic-ai/claude-code && claude auth login");
1693
1870
  else console.log(` Fix: install ${worker} and add it to PATH`);
1694
1871
  }
1695
- if (!currentState.prereq.ghAuthOk) {
1696
- console.log(" Fix: run gh auth login");
1872
+ if (!currentState.prereq.forgeAuthOk) {
1873
+ if (config.forge.provider === "gitea") {
1874
+ console.log(" Fix: pass --gitea-base-url plus --gitea-token or --gitea-username/--gitea-password");
1875
+ } else {
1876
+ console.log(" Fix: run gh auth login");
1877
+ }
1697
1878
  }
1698
1879
 
1699
1880
  if (!options.interactive) {
@@ -1736,19 +1917,19 @@ async function maybeRunFinalSetupFixups(options, scopedContext, config, currentS
1736
1917
  if (!prereq.coreToolsOk) {
1737
1918
  actions.push("install-core-tools");
1738
1919
  dependencyInstall = await maybeInstallMissingDependencies({ ...options, installMissingDeps: true, interactive: false }, prereq);
1739
- prereq = collectPrereqStatus(config.codingWorker);
1920
+ prereq = collectPrereqStatus(config.codingWorker, config.forge);
1740
1921
  }
1741
1922
 
1742
1923
  if (!prereq.workerAvailable) {
1743
1924
  actions.push("install-worker-backend");
1744
1925
  workerSetupStep = await maybeShowWorkerSetupGuide({ ...options, installMissingBackend: true, interactive: options.interactive }, prereq);
1745
- prereq = collectPrereqStatus(config.codingWorker);
1926
+ prereq = collectPrereqStatus(config.codingWorker, config.forge);
1746
1927
  }
1747
1928
 
1748
- if (!prereq.ghAuthOk) {
1929
+ if (!prereq.forgeAuthOk) {
1749
1930
  actions.push("github-auth-login");
1750
1931
  githubAuthStep = await maybeRunGithubAuthLogin({ ...options, ghAuthLogin: true, interactive: false }, prereq);
1751
- prereq = collectPrereqStatus(config.codingWorker);
1932
+ prereq = collectPrereqStatus(config.codingWorker, config.forge);
1752
1933
  }
1753
1934
 
1754
1935
  if ((doctorKv.DOCTOR_STATUS || "") !== "ok") {
@@ -1768,9 +1949,9 @@ async function maybeRunFinalSetupFixups(options, scopedContext, config, currentS
1768
1949
  } else if (anchorSync && anchorSync.status !== "ok") {
1769
1950
  runtimeStartStatus = "skipped";
1770
1951
  runtimeStartReason = `anchor-sync-${anchorSync.reason}`;
1771
- } else if (!prereq.ghAuthOk) {
1952
+ } else if (!prereq.forgeAuthOk) {
1772
1953
  runtimeStartStatus = "skipped";
1773
- runtimeStartReason = "gh-auth-not-ready";
1954
+ runtimeStartReason = `${config.forge.provider}-auth-not-ready`;
1774
1955
  } else if ((doctorKv.DOCTOR_STATUS || "") !== "ok") {
1775
1956
  runtimeStartStatus = "skipped";
1776
1957
  runtimeStartReason = `doctor-${doctorKv.DOCTOR_STATUS || "not-ok"}`;
@@ -1821,16 +2002,29 @@ async function collectSetupConfig(options, context) {
1821
2002
  const detectedRepoSlug = options.repoSlug || detectRepoSlug(detectedRepoRoot);
1822
2003
  const suggestedProfileId = options.profileId || sanitizeProfileId((detectedRepoSlug.split("/").pop() || path.basename(detectedRepoRoot)));
1823
2004
  const suggestedWorker = options.codingWorker || detectPreferredWorker();
2005
+ const detectedForgeProvider = options.forgeProvider || process.env.ACP_FORGE_PROVIDER || "github";
2006
+ const defaultGiteaBaseUrl = options.giteaBaseUrl || process.env.ACP_GITEA_BASE_URL || process.env.GITEA_BASE_URL || "http://127.0.0.1:3000";
1824
2007
 
1825
2008
  let repoRoot = detectedRepoRoot;
1826
2009
  let repoSlug = detectedRepoSlug;
1827
2010
  let profileId = suggestedProfileId;
1828
2011
  let codingWorker = suggestedWorker;
1829
-
1830
- if (!fs.existsSync(detectedRepoRoot)) {
2012
+ let forgeProvider = detectedForgeProvider;
2013
+ let giteaBaseUrl = defaultGiteaBaseUrl;
2014
+ let giteaToken = options.giteaToken || process.env.ACP_GITEA_TOKEN || process.env.GITEA_TOKEN || "";
2015
+ let giteaUsername = options.giteaUsername || process.env.ACP_GITEA_USERNAME || process.env.GITEA_USERNAME || "";
2016
+ let giteaPassword = options.giteaPassword || process.env.ACP_GITEA_PASSWORD || process.env.GITEA_PASSWORD || "";
2017
+
2018
+ const detectedRepoRootExists = fs.existsSync(detectedRepoRoot);
2019
+ if (!detectedRepoRootExists && !options.allowMissingRepo) {
1831
2020
  throw new Error(`setup repo root does not exist: ${detectedRepoRoot}`);
1832
2021
  }
1833
2022
 
2023
+ if (options.allowMissingRepo && !detectedRepoRootExists) {
2024
+ console.log(`\nSource repo root not found yet: ${detectedRepoRoot}`);
2025
+ console.log("- continuing because --allow-missing-repo was set; profile adopt will run in missing-repo mode.");
2026
+ }
2027
+
1834
2028
  if (!options.interactive) {
1835
2029
  if (!repoSlug) {
1836
2030
  throw new Error("setup could not detect --repo-slug automatically; pass --repo-slug <owner/repo> or run interactively inside a git checkout with origin set");
@@ -1842,7 +2036,22 @@ async function collectSetupConfig(options, context) {
1842
2036
  printWizardStep(1, 4, "Project details");
1843
2037
 
1844
2038
  repoRoot = path.resolve(await promptText(rl, "Local repo root", detectedRepoRoot));
1845
- repoSlug = await promptText(rl, "GitHub repo slug", repoSlug || "");
2039
+ let forgeInput = forgeProvider;
2040
+ while (!["github", "gitea"].includes(forgeInput)) {
2041
+ forgeInput = await promptText(rl, "Forge provider (github / gitea)", forgeProvider || "github");
2042
+ }
2043
+ forgeProvider = forgeInput;
2044
+ repoSlug = await promptText(rl, "Forge repo slug", repoSlug || "");
2045
+ if (forgeProvider === "gitea") {
2046
+ giteaBaseUrl = await promptText(rl, "Gitea base URL", giteaBaseUrl);
2047
+ giteaToken = await promptText(rl, "Gitea token (Enter to skip)", giteaToken);
2048
+ if (!giteaToken) {
2049
+ giteaUsername = await promptText(rl, "Gitea username (Enter to skip)", giteaUsername);
2050
+ if (giteaUsername) {
2051
+ giteaPassword = await promptText(rl, "Gitea password (Enter to skip)", giteaPassword);
2052
+ }
2053
+ }
2054
+ }
1846
2055
  profileId = sanitizeProfileId(await promptText(rl, "Profile id", profileId));
1847
2056
 
1848
2057
  let workerInput = codingWorker;
@@ -1860,13 +2069,21 @@ async function collectSetupConfig(options, context) {
1860
2069
  }
1861
2070
 
1862
2071
  const paths = buildSetupPaths(context.platformHome, repoRoot, profileId, options);
1863
- const prereq = collectPrereqStatus(codingWorker);
2072
+ const forge = {
2073
+ provider: forgeProvider,
2074
+ giteaBaseUrl,
2075
+ giteaToken,
2076
+ giteaUsername,
2077
+ giteaPassword
2078
+ };
2079
+ const prereq = collectPrereqStatus(codingWorker, forge);
1864
2080
  const config = {
1865
2081
  profileId,
1866
2082
  repoSlug,
1867
2083
  repoRoot,
1868
2084
  codingWorker,
1869
2085
  paths,
2086
+ forge,
1870
2087
  prereq
1871
2088
  };
1872
2089
 
@@ -1879,7 +2096,7 @@ async function collectSetupConfig(options, context) {
1879
2096
  printPrereqSummary(prereq);
1880
2097
  const rl = createPromptInterface();
1881
2098
  try {
1882
- if (!prereq.coreToolsOk || !prereq.workerAvailable || !prereq.ghAuthOk) {
2099
+ if (!prereq.coreToolsOk || !prereq.workerAvailable || !prereq.forgeAuthOk) {
1883
2100
  console.log("\nACP can still scaffold the profile now, but runtime start may be skipped until these checks are green.");
1884
2101
  }
1885
2102
  const shouldContinue = await promptYesNo(rl, "Continue with these values", true);
@@ -2026,7 +2243,7 @@ async function runSetupFlow(forwardedArgs) {
2026
2243
  workerBackendInstallExample: plan.workerGuide.installExamples[0] || "",
2027
2244
  workerBackendAuthExample: plan.workerGuide.authExamples[0] || "",
2028
2245
  workerBackendVerifyExample: plan.workerGuide.verifyExamples[0] || "",
2029
- githubAuthStatus: plan.prereq.ghAuthOk ? "ok" : "not-ready",
2246
+ githubAuthStatus: plan.prereq.forgeAuthOk ? "ok" : "not-ready",
2030
2247
  githubAuthStepStatus: plan.githubAuthAction.status,
2031
2248
  githubAuthStepReason: plan.githubAuthAction.reason || "",
2032
2249
  dependencyInstallStatus: plan.dependencyAction.status,
@@ -2050,6 +2267,7 @@ async function runSetupFlow(forwardedArgs) {
2050
2267
  console.log(`SETUP_STATUS=dry-run`);
2051
2268
  console.log(`SETUP_MODE=dry-run`);
2052
2269
  console.log(`PROFILE_ID=${config.profileId}`);
2270
+ console.log(`FORGE_PROVIDER=${config.forge.provider}`);
2053
2271
  console.log(`REPO_SLUG=${config.repoSlug}`);
2054
2272
  console.log(`REPO_ROOT=${config.paths.repoRoot}`);
2055
2273
  console.log(`AGENT_ROOT=${config.paths.agentRoot}`);
@@ -2068,7 +2286,7 @@ async function runSetupFlow(forwardedArgs) {
2068
2286
  }
2069
2287
  console.log(`WORKER_BACKEND_COMMAND=${plan.prereq.workerCommand}`);
2070
2288
  console.log(`WORKER_BACKEND_STATUS=${plan.prereq.workerAvailable ? "ok" : "missing"}`);
2071
- console.log(`GITHUB_AUTH_STATUS=${plan.prereq.ghAuthOk ? "ok" : "not-ready"}`);
2289
+ console.log(`GITHUB_AUTH_STATUS=${plan.prereq.forgeAuthOk ? "ok" : "not-ready"}`);
2072
2290
  console.log(`DEPENDENCY_INSTALL_STATUS=${plan.dependencyAction.status}`);
2073
2291
  if (plan.dependencyAction.reason) {
2074
2292
  console.log(`DEPENDENCY_INSTALL_REASON=${plan.dependencyAction.reason}`);
@@ -2134,16 +2352,16 @@ async function runSetupFlow(forwardedArgs) {
2134
2352
  console.error("dependency installation failed");
2135
2353
  return 1;
2136
2354
  }
2137
- prereq = collectPrereqStatus(config.codingWorker);
2355
+ prereq = collectPrereqStatus(config.codingWorker, config.forge);
2138
2356
 
2139
2357
  let githubAuthStep = await maybeRunGithubAuthLogin(options, prereq);
2140
2358
  if (githubAuthStep.status === "failed") {
2141
- console.error("GitHub authentication failed");
2359
+ console.error(config.forge.provider === "gitea" ? "Gitea authentication failed" : "GitHub authentication failed");
2142
2360
  return 1;
2143
2361
  }
2144
- prereq = collectPrereqStatus(config.codingWorker);
2362
+ prereq = collectPrereqStatus(config.codingWorker, config.forge);
2145
2363
  let workerSetupStep = await maybeShowWorkerSetupGuide(options, prereq);
2146
- prereq = collectPrereqStatus(config.codingWorker);
2364
+ prereq = collectPrereqStatus(config.codingWorker, config.forge);
2147
2365
 
2148
2366
  // Check OpenRouter API key when openclaw or pi is selected
2149
2367
  if ((config.codingWorker === "openclaw" || config.codingWorker === "pi") && !process.env.OPENROUTER_API_KEY) {
@@ -2215,6 +2433,7 @@ async function runSetupFlow(forwardedArgs) {
2215
2433
  const initArgs = [
2216
2434
  "--profile-id", config.profileId,
2217
2435
  "--repo-slug", config.repoSlug,
2436
+ "--forge-provider", config.forge.provider,
2218
2437
  "--repo-root", config.paths.repoRoot,
2219
2438
  "--agent-root", config.paths.agentRoot,
2220
2439
  "--agent-repo-root", config.paths.agentRepoRoot,
@@ -2224,6 +2443,12 @@ async function runSetupFlow(forwardedArgs) {
2224
2443
  "--source-repo-root", config.paths.sourceRepoRoot,
2225
2444
  "--coding-worker", config.codingWorker
2226
2445
  ];
2446
+ if (config.forge.provider === "gitea") {
2447
+ if (config.forge.giteaBaseUrl) initArgs.push("--gitea-base-url", config.forge.giteaBaseUrl);
2448
+ if (config.forge.giteaToken) initArgs.push("--gitea-token", config.forge.giteaToken);
2449
+ if (config.forge.giteaUsername) initArgs.push("--gitea-username", config.forge.giteaUsername);
2450
+ if (config.forge.giteaPassword) initArgs.push("--gitea-password", config.forge.giteaPassword);
2451
+ }
2227
2452
  if (options.force) {
2228
2453
  initArgs.push("--force");
2229
2454
  }
@@ -2257,9 +2482,13 @@ async function runSetupFlow(forwardedArgs) {
2257
2482
  } else if (anchorSync.status !== "ok") {
2258
2483
  runtimeStartReason = `anchor-sync-${anchorSync.reason}`;
2259
2484
  console.log("runtime start skipped: ACP deferred anchor repo sync for this setup run.");
2260
- } else if (!prereq.ghAuthOk) {
2261
- runtimeStartReason = "gh-auth-not-ready";
2262
- console.log("runtime start skipped: GitHub CLI is not authenticated yet. Run `gh auth login` and start the runtime afterwards.");
2485
+ } else if (!prereq.forgeAuthOk) {
2486
+ runtimeStartReason = `${config.forge.provider}-auth-not-ready`;
2487
+ if (config.forge.provider === "gitea") {
2488
+ console.log("runtime start skipped: Gitea auth is not configured yet. Pass --gitea-base-url and --gitea-token (or username/password), then re-run setup or start the runtime afterwards.");
2489
+ } else {
2490
+ console.log("runtime start skipped: GitHub CLI is not authenticated yet. Run `gh auth login` and start the runtime afterwards.");
2491
+ }
2263
2492
  } else {
2264
2493
  runSetupStep(scopedContext, "Start the runtime", "tools/bin/project-runtimectl.sh", ["start", "--profile-id", config.profileId], { useRuntimeCopy: true });
2265
2494
  const runtimeStatusOutput = runSetupStep(scopedContext, "Read back runtime status", "tools/bin/project-runtimectl.sh", ["status", "--profile-id", config.profileId], { useRuntimeCopy: true });
@@ -2365,6 +2594,7 @@ async function runSetupFlow(forwardedArgs) {
2365
2594
  setupMode: "run",
2366
2595
  profileId: config.profileId,
2367
2596
  repoSlug: config.repoSlug,
2597
+ forge: config.forge,
2368
2598
  codingWorker: config.codingWorker,
2369
2599
  profileExists: true,
2370
2600
  repoRoot: config.paths.repoRoot,
@@ -2389,7 +2619,7 @@ async function runSetupFlow(forwardedArgs) {
2389
2619
  workerBackendInstallExample: workerSetupStep.guide.installExamples[0] || "",
2390
2620
  workerBackendAuthExample: workerSetupStep.guide.authExamples[0] || "",
2391
2621
  workerBackendVerifyExample: workerSetupStep.guide.verifyExamples[0] || "",
2392
- githubAuthStatus: prereq.ghAuthOk ? "ok" : "not-ready",
2622
+ githubAuthStatus: prereq.forgeAuthOk ? "ok" : "not-ready",
2393
2623
  githubAuthStepStatus: githubAuthStep.status,
2394
2624
  githubAuthStepReason: githubAuthStep.reason || "",
2395
2625
  dependencyInstallStatus: dependencyInstall.status,
@@ -2420,7 +2650,10 @@ async function runSetupFlow(forwardedArgs) {
2420
2650
  console.log(` Runtime : ${context.runtimeHome}`);
2421
2651
 
2422
2652
  const pendingItems = [];
2423
- if (!prereq.ghAuthOk) pendingItems.push("GitHub CLI not authenticated — run: gh auth login");
2653
+ if (!prereq.forgeAuthOk) {
2654
+ if (config.forge.provider === "gitea") pendingItems.push("Gitea auth not configured — rerun setup with --gitea-base-url and --gitea-token");
2655
+ else pendingItems.push("GitHub CLI not authenticated — run: gh auth login");
2656
+ }
2424
2657
  if (!prereq.workerAvailable) pendingItems.push(`${config.codingWorker} not found on PATH — install it before starting`);
2425
2658
  if (config.codingWorker === "openclaw" && !process.env.OPENROUTER_API_KEY) {
2426
2659
  pendingItems.push("OPENROUTER_API_KEY not set — required for openclaw workers");
@@ -2459,7 +2692,7 @@ async function runSetupFlow(forwardedArgs) {
2459
2692
  }
2460
2693
  } else {
2461
2694
  console.log("\n Getting started:");
2462
- console.log(` 1. Add the label 'agent-ready' to a GitHub issue in ${config.repoSlug}`);
2695
+ console.log(` 1. Leave a normal issue open in ${config.repoSlug}, or add 'agent-keep-open' for a recurring issue`);
2463
2696
  console.log(" 2. ACP picks it up automatically, assigns a worker, and opens a PR");
2464
2697
  console.log(" 3. Watch progress in the dashboard or with 'runtime status'");
2465
2698
  }
@@ -2472,11 +2705,13 @@ async function runSetupFlow(forwardedArgs) {
2472
2705
  // Machine-readable KV output for non-interactive / scripted runs
2473
2706
  console.log("\nSetup complete.");
2474
2707
  console.log(`- profile: ${config.profileId}`);
2708
+ console.log(`- forge provider: ${config.forge.provider}`);
2475
2709
  console.log(`- repo: ${config.repoSlug}`);
2476
2710
  console.log(`- runtime home: ${context.runtimeHome}`);
2477
2711
 
2478
2712
  console.log(`SETUP_STATUS=ok`);
2479
2713
  console.log(`PROFILE_ID=${config.profileId}`);
2714
+ console.log(`FORGE_PROVIDER=${config.forge.provider}`);
2480
2715
  console.log(`REPO_SLUG=${config.repoSlug}`);
2481
2716
  console.log(`REPO_ROOT=${config.paths.repoRoot}`);
2482
2717
  console.log(`AGENT_ROOT=${config.paths.agentRoot}`);
@@ -2521,7 +2756,7 @@ async function runSetupFlow(forwardedArgs) {
2521
2756
  if (workerSetupStep.guide.verifyExamples[0]) {
2522
2757
  console.log(`WORKER_BACKEND_VERIFY_EXAMPLE=${workerSetupStep.guide.verifyExamples[0]}`);
2523
2758
  }
2524
- console.log(`GITHUB_AUTH_STATUS=${prereq.ghAuthOk ? "ok" : "not-ready"}`);
2759
+ console.log(`GITHUB_AUTH_STATUS=${prereq.forgeAuthOk ? "ok" : "not-ready"}`);
2525
2760
  console.log(`FINAL_FIXUP_STATUS=${finalFixup.status}`);
2526
2761
  console.log(`FINAL_FIXUP_ACTIONS=${finalFixup.actions.join(",")}`);
2527
2762
  console.log(`DEPENDENCY_INSTALL_STATUS=${dependencyInstall.status}`);