agent-control-plane 0.3.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 (43) 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 +256 -58
  9. package/package.json +7 -6
  10. package/tools/bin/agent-github-update-labels +36 -2
  11. package/tools/bin/agent-project-catch-up-merged-prs +3 -2
  12. package/tools/bin/agent-project-publish-issue-pr +6 -3
  13. package/tools/bin/agent-project-reconcile-issue-session +12 -1
  14. package/tools/bin/agent-project-reconcile-pr-session +90 -32
  15. package/tools/bin/agent-project-retry-state +18 -7
  16. package/tools/bin/agent-project-run-codex-resilient +13 -5
  17. package/tools/bin/agent-project-sync-source-repo-main +163 -0
  18. package/tools/bin/flow-config-lib.sh +1203 -60
  19. package/tools/bin/flow-shell-lib.sh +32 -0
  20. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  21. package/tools/bin/github-write-outbox.sh +470 -0
  22. package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
  23. package/tools/bin/heartbeat-safe-auto.sh +42 -0
  24. package/tools/bin/install-project-launchd.sh +17 -2
  25. package/tools/bin/project-init.sh +21 -1
  26. package/tools/bin/project-launchd-bootstrap.sh +5 -1
  27. package/tools/bin/project-runtimectl.sh +46 -2
  28. package/tools/bin/resident-issue-controller-lib.sh +2 -2
  29. package/tools/bin/scaffold-profile.sh +61 -3
  30. package/tools/bin/start-pr-fix-worker.sh +47 -10
  31. package/tools/bin/start-resident-issue-loop.sh +2 -2
  32. package/tools/dashboard/app.js +30 -1
  33. package/tools/dashboard/dashboard_snapshot.py +55 -0
  34. package/tools/templates/pr-fix-template.md +3 -1
  35. package/tools/templates/pr-merge-repair-template.md +2 -1
  36. package/references/architecture.md +0 -217
  37. package/references/commands.md +0 -128
  38. package/references/control-plane-map.md +0 -124
  39. package/references/docs-map.md +0 -73
  40. package/references/release-checklist.md +0 -65
  41. package/references/repo-map.md +0 -36
  42. package/tools/bin/resident-issue-queue-status.py +0 -35
  43. 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
  }
@@ -1021,6 +1149,9 @@ async function maybeInstallMissingDependencies(options, prereq) {
1021
1149
  }
1022
1150
 
1023
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
+ }
1024
1155
  if (prereq.ghAuthOk) {
1025
1156
  return { status: "not-needed", reason: "" };
1026
1157
  }
@@ -1357,7 +1488,7 @@ function printPrereqSummary(prereq) {
1357
1488
  console.log("\nPrerequisite check");
1358
1489
  console.log(`- core tools: ${prereq.coreToolsOk ? "ok" : `missing ${prereq.missingRequired.join(", ")}`}`);
1359
1490
  console.log(`- worker backend (${prereq.workerCommand}): ${prereq.workerAvailable ? "found" : "missing on PATH"}`);
1360
- console.log(`- GitHub auth: ${prereq.ghAuthOk ? "ok" : "not ready"}`);
1491
+ console.log(`- ${prereq.forgeProvider === "gitea" ? "Gitea auth" : "GitHub auth"}: ${prereq.forgeAuthOk ? "ok" : "not ready"}`);
1361
1492
  console.log(`- backend note: ${backendReadinessHint(prereq.workerCommand)}`);
1362
1493
  }
1363
1494
 
@@ -1465,7 +1596,11 @@ function buildScopedContext(context, profileId) {
1465
1596
  function renderSetupSummary(config) {
1466
1597
  console.log("\nSetup plan");
1467
1598
  console.log(`- profile id: ${config.profileId}`);
1599
+ console.log(`- forge provider: ${config.forge.provider}`);
1468
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
+ }
1469
1604
  console.log(`- repo root: ${config.paths.repoRoot}`);
1470
1605
  console.log(`- agent root: ${config.paths.agentRoot}`);
1471
1606
  console.log(`- agent repo root: ${config.paths.agentRepoRoot}`);
@@ -1473,6 +1608,10 @@ function renderSetupSummary(config) {
1473
1608
  console.log(`- coding worker: ${config.codingWorker}`);
1474
1609
  }
1475
1610
 
1611
+ function forgeAuthLabel(config) {
1612
+ return config.forge.provider === "gitea" ? "Gitea auth" : "GitHub auth";
1613
+ }
1614
+
1476
1615
  function planStatusWithReason(status, reason = "") {
1477
1616
  return {
1478
1617
  status,
@@ -1514,8 +1653,10 @@ function buildSetupDryRunPlan(options, context, config) {
1514
1653
  }
1515
1654
 
1516
1655
  let githubAuthAction = planStatusWithReason("not-needed");
1517
- if (!prereq.ghAuthOk) {
1518
- 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")) {
1519
1660
  githubAuthAction = planStatusWithReason("blocked", "gh-missing");
1520
1661
  } else if (options.ghAuthLogin === false) {
1521
1662
  githubAuthAction = planStatusWithReason("skipped", "disabled");
@@ -1534,8 +1675,8 @@ function buildSetupDryRunPlan(options, context, config) {
1534
1675
  runtimeStartAction = planStatusWithReason("blocked", `missing-worker:${prereq.workerCommand}`);
1535
1676
  } else if (anchorSync.status !== "ok") {
1536
1677
  runtimeStartAction = planStatusWithReason("blocked", `anchor-sync-${anchorSync.reason}`);
1537
- } else if (!prereq.ghAuthOk) {
1538
- runtimeStartAction = planStatusWithReason("blocked", "gh-auth-not-ready");
1678
+ } else if (!prereq.forgeAuthOk) {
1679
+ runtimeStartAction = planStatusWithReason("blocked", `${config.forge.provider}-auth-not-ready`);
1539
1680
  } else {
1540
1681
  runtimeStartAction = planStatusWithReason("would-run");
1541
1682
  }
@@ -1587,9 +1728,13 @@ function printSetupDryRunPlan(context, config, plan) {
1587
1728
  if (plan.workerAction.status !== "not-needed" && plan.workerInstallPlan && plan.workerInstallPlan.commands.length > 0) {
1588
1729
  console.log(` command preview: ${plan.workerInstallPlan.commands.map(formatCommand).join(" && ")}`);
1589
1730
  }
1590
- 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})` : ""}`);
1591
1732
  if (plan.githubAuthAction.status !== "not-needed") {
1592
- 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
+ }
1593
1738
  }
1594
1739
  console.log(`- runtime start: ${plan.runtimeStartAction.status}${plan.runtimeStartAction.reason ? ` (${plan.runtimeStartAction.reason})` : ""}`);
1595
1740
  if (process.platform === "darwin") {
@@ -1603,6 +1748,7 @@ function buildSetupResultPayload(params) {
1603
1748
  setupMode: params.setupMode,
1604
1749
  profileId: params.profileId,
1605
1750
  repoSlug: params.repoSlug,
1751
+ forge: params.forge,
1606
1752
  codingWorker: params.codingWorker,
1607
1753
  profileExists: params.profileExists,
1608
1754
  paths: {
@@ -1679,8 +1825,8 @@ function collectFinalSetupIssues(config, prereq, doctorKv, runtimeStartStatus) {
1679
1825
  if (!prereq.workerAvailable) {
1680
1826
  issues.push(`missing worker backend on PATH: ${prereq.workerCommand}`);
1681
1827
  }
1682
- if (!prereq.ghAuthOk) {
1683
- 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");
1684
1830
  }
1685
1831
  if ((doctorKv.DOCTOR_STATUS || "") !== "ok") {
1686
1832
  issues.push(`doctor status is ${doctorKv.DOCTOR_STATUS || "unknown"}`);
@@ -1723,8 +1869,12 @@ async function maybeRunFinalSetupFixups(options, scopedContext, config, currentS
1723
1869
  else if (worker === "claude") console.log(" Fix: npm install -g @anthropic-ai/claude-code && claude auth login");
1724
1870
  else console.log(` Fix: install ${worker} and add it to PATH`);
1725
1871
  }
1726
- if (!currentState.prereq.ghAuthOk) {
1727
- 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
+ }
1728
1878
  }
1729
1879
 
1730
1880
  if (!options.interactive) {
@@ -1767,19 +1917,19 @@ async function maybeRunFinalSetupFixups(options, scopedContext, config, currentS
1767
1917
  if (!prereq.coreToolsOk) {
1768
1918
  actions.push("install-core-tools");
1769
1919
  dependencyInstall = await maybeInstallMissingDependencies({ ...options, installMissingDeps: true, interactive: false }, prereq);
1770
- prereq = collectPrereqStatus(config.codingWorker);
1920
+ prereq = collectPrereqStatus(config.codingWorker, config.forge);
1771
1921
  }
1772
1922
 
1773
1923
  if (!prereq.workerAvailable) {
1774
1924
  actions.push("install-worker-backend");
1775
1925
  workerSetupStep = await maybeShowWorkerSetupGuide({ ...options, installMissingBackend: true, interactive: options.interactive }, prereq);
1776
- prereq = collectPrereqStatus(config.codingWorker);
1926
+ prereq = collectPrereqStatus(config.codingWorker, config.forge);
1777
1927
  }
1778
1928
 
1779
- if (!prereq.ghAuthOk) {
1929
+ if (!prereq.forgeAuthOk) {
1780
1930
  actions.push("github-auth-login");
1781
1931
  githubAuthStep = await maybeRunGithubAuthLogin({ ...options, ghAuthLogin: true, interactive: false }, prereq);
1782
- prereq = collectPrereqStatus(config.codingWorker);
1932
+ prereq = collectPrereqStatus(config.codingWorker, config.forge);
1783
1933
  }
1784
1934
 
1785
1935
  if ((doctorKv.DOCTOR_STATUS || "") !== "ok") {
@@ -1799,9 +1949,9 @@ async function maybeRunFinalSetupFixups(options, scopedContext, config, currentS
1799
1949
  } else if (anchorSync && anchorSync.status !== "ok") {
1800
1950
  runtimeStartStatus = "skipped";
1801
1951
  runtimeStartReason = `anchor-sync-${anchorSync.reason}`;
1802
- } else if (!prereq.ghAuthOk) {
1952
+ } else if (!prereq.forgeAuthOk) {
1803
1953
  runtimeStartStatus = "skipped";
1804
- runtimeStartReason = "gh-auth-not-ready";
1954
+ runtimeStartReason = `${config.forge.provider}-auth-not-ready`;
1805
1955
  } else if ((doctorKv.DOCTOR_STATUS || "") !== "ok") {
1806
1956
  runtimeStartStatus = "skipped";
1807
1957
  runtimeStartReason = `doctor-${doctorKv.DOCTOR_STATUS || "not-ok"}`;
@@ -1852,11 +2002,18 @@ async function collectSetupConfig(options, context) {
1852
2002
  const detectedRepoSlug = options.repoSlug || detectRepoSlug(detectedRepoRoot);
1853
2003
  const suggestedProfileId = options.profileId || sanitizeProfileId((detectedRepoSlug.split("/").pop() || path.basename(detectedRepoRoot)));
1854
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";
1855
2007
 
1856
2008
  let repoRoot = detectedRepoRoot;
1857
2009
  let repoSlug = detectedRepoSlug;
1858
2010
  let profileId = suggestedProfileId;
1859
2011
  let codingWorker = suggestedWorker;
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 || "";
1860
2017
 
1861
2018
  const detectedRepoRootExists = fs.existsSync(detectedRepoRoot);
1862
2019
  if (!detectedRepoRootExists && !options.allowMissingRepo) {
@@ -1879,7 +2036,22 @@ async function collectSetupConfig(options, context) {
1879
2036
  printWizardStep(1, 4, "Project details");
1880
2037
 
1881
2038
  repoRoot = path.resolve(await promptText(rl, "Local repo root", detectedRepoRoot));
1882
- 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
+ }
1883
2055
  profileId = sanitizeProfileId(await promptText(rl, "Profile id", profileId));
1884
2056
 
1885
2057
  let workerInput = codingWorker;
@@ -1897,13 +2069,21 @@ async function collectSetupConfig(options, context) {
1897
2069
  }
1898
2070
 
1899
2071
  const paths = buildSetupPaths(context.platformHome, repoRoot, profileId, options);
1900
- 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);
1901
2080
  const config = {
1902
2081
  profileId,
1903
2082
  repoSlug,
1904
2083
  repoRoot,
1905
2084
  codingWorker,
1906
2085
  paths,
2086
+ forge,
1907
2087
  prereq
1908
2088
  };
1909
2089
 
@@ -1916,7 +2096,7 @@ async function collectSetupConfig(options, context) {
1916
2096
  printPrereqSummary(prereq);
1917
2097
  const rl = createPromptInterface();
1918
2098
  try {
1919
- if (!prereq.coreToolsOk || !prereq.workerAvailable || !prereq.ghAuthOk) {
2099
+ if (!prereq.coreToolsOk || !prereq.workerAvailable || !prereq.forgeAuthOk) {
1920
2100
  console.log("\nACP can still scaffold the profile now, but runtime start may be skipped until these checks are green.");
1921
2101
  }
1922
2102
  const shouldContinue = await promptYesNo(rl, "Continue with these values", true);
@@ -2063,7 +2243,7 @@ async function runSetupFlow(forwardedArgs) {
2063
2243
  workerBackendInstallExample: plan.workerGuide.installExamples[0] || "",
2064
2244
  workerBackendAuthExample: plan.workerGuide.authExamples[0] || "",
2065
2245
  workerBackendVerifyExample: plan.workerGuide.verifyExamples[0] || "",
2066
- githubAuthStatus: plan.prereq.ghAuthOk ? "ok" : "not-ready",
2246
+ githubAuthStatus: plan.prereq.forgeAuthOk ? "ok" : "not-ready",
2067
2247
  githubAuthStepStatus: plan.githubAuthAction.status,
2068
2248
  githubAuthStepReason: plan.githubAuthAction.reason || "",
2069
2249
  dependencyInstallStatus: plan.dependencyAction.status,
@@ -2087,6 +2267,7 @@ async function runSetupFlow(forwardedArgs) {
2087
2267
  console.log(`SETUP_STATUS=dry-run`);
2088
2268
  console.log(`SETUP_MODE=dry-run`);
2089
2269
  console.log(`PROFILE_ID=${config.profileId}`);
2270
+ console.log(`FORGE_PROVIDER=${config.forge.provider}`);
2090
2271
  console.log(`REPO_SLUG=${config.repoSlug}`);
2091
2272
  console.log(`REPO_ROOT=${config.paths.repoRoot}`);
2092
2273
  console.log(`AGENT_ROOT=${config.paths.agentRoot}`);
@@ -2105,7 +2286,7 @@ async function runSetupFlow(forwardedArgs) {
2105
2286
  }
2106
2287
  console.log(`WORKER_BACKEND_COMMAND=${plan.prereq.workerCommand}`);
2107
2288
  console.log(`WORKER_BACKEND_STATUS=${plan.prereq.workerAvailable ? "ok" : "missing"}`);
2108
- console.log(`GITHUB_AUTH_STATUS=${plan.prereq.ghAuthOk ? "ok" : "not-ready"}`);
2289
+ console.log(`GITHUB_AUTH_STATUS=${plan.prereq.forgeAuthOk ? "ok" : "not-ready"}`);
2109
2290
  console.log(`DEPENDENCY_INSTALL_STATUS=${plan.dependencyAction.status}`);
2110
2291
  if (plan.dependencyAction.reason) {
2111
2292
  console.log(`DEPENDENCY_INSTALL_REASON=${plan.dependencyAction.reason}`);
@@ -2171,16 +2352,16 @@ async function runSetupFlow(forwardedArgs) {
2171
2352
  console.error("dependency installation failed");
2172
2353
  return 1;
2173
2354
  }
2174
- prereq = collectPrereqStatus(config.codingWorker);
2355
+ prereq = collectPrereqStatus(config.codingWorker, config.forge);
2175
2356
 
2176
2357
  let githubAuthStep = await maybeRunGithubAuthLogin(options, prereq);
2177
2358
  if (githubAuthStep.status === "failed") {
2178
- console.error("GitHub authentication failed");
2359
+ console.error(config.forge.provider === "gitea" ? "Gitea authentication failed" : "GitHub authentication failed");
2179
2360
  return 1;
2180
2361
  }
2181
- prereq = collectPrereqStatus(config.codingWorker);
2362
+ prereq = collectPrereqStatus(config.codingWorker, config.forge);
2182
2363
  let workerSetupStep = await maybeShowWorkerSetupGuide(options, prereq);
2183
- prereq = collectPrereqStatus(config.codingWorker);
2364
+ prereq = collectPrereqStatus(config.codingWorker, config.forge);
2184
2365
 
2185
2366
  // Check OpenRouter API key when openclaw or pi is selected
2186
2367
  if ((config.codingWorker === "openclaw" || config.codingWorker === "pi") && !process.env.OPENROUTER_API_KEY) {
@@ -2252,6 +2433,7 @@ async function runSetupFlow(forwardedArgs) {
2252
2433
  const initArgs = [
2253
2434
  "--profile-id", config.profileId,
2254
2435
  "--repo-slug", config.repoSlug,
2436
+ "--forge-provider", config.forge.provider,
2255
2437
  "--repo-root", config.paths.repoRoot,
2256
2438
  "--agent-root", config.paths.agentRoot,
2257
2439
  "--agent-repo-root", config.paths.agentRepoRoot,
@@ -2261,6 +2443,12 @@ async function runSetupFlow(forwardedArgs) {
2261
2443
  "--source-repo-root", config.paths.sourceRepoRoot,
2262
2444
  "--coding-worker", config.codingWorker
2263
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
+ }
2264
2452
  if (options.force) {
2265
2453
  initArgs.push("--force");
2266
2454
  }
@@ -2294,9 +2482,13 @@ async function runSetupFlow(forwardedArgs) {
2294
2482
  } else if (anchorSync.status !== "ok") {
2295
2483
  runtimeStartReason = `anchor-sync-${anchorSync.reason}`;
2296
2484
  console.log("runtime start skipped: ACP deferred anchor repo sync for this setup run.");
2297
- } else if (!prereq.ghAuthOk) {
2298
- runtimeStartReason = "gh-auth-not-ready";
2299
- 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
+ }
2300
2492
  } else {
2301
2493
  runSetupStep(scopedContext, "Start the runtime", "tools/bin/project-runtimectl.sh", ["start", "--profile-id", config.profileId], { useRuntimeCopy: true });
2302
2494
  const runtimeStatusOutput = runSetupStep(scopedContext, "Read back runtime status", "tools/bin/project-runtimectl.sh", ["status", "--profile-id", config.profileId], { useRuntimeCopy: true });
@@ -2402,6 +2594,7 @@ async function runSetupFlow(forwardedArgs) {
2402
2594
  setupMode: "run",
2403
2595
  profileId: config.profileId,
2404
2596
  repoSlug: config.repoSlug,
2597
+ forge: config.forge,
2405
2598
  codingWorker: config.codingWorker,
2406
2599
  profileExists: true,
2407
2600
  repoRoot: config.paths.repoRoot,
@@ -2426,7 +2619,7 @@ async function runSetupFlow(forwardedArgs) {
2426
2619
  workerBackendInstallExample: workerSetupStep.guide.installExamples[0] || "",
2427
2620
  workerBackendAuthExample: workerSetupStep.guide.authExamples[0] || "",
2428
2621
  workerBackendVerifyExample: workerSetupStep.guide.verifyExamples[0] || "",
2429
- githubAuthStatus: prereq.ghAuthOk ? "ok" : "not-ready",
2622
+ githubAuthStatus: prereq.forgeAuthOk ? "ok" : "not-ready",
2430
2623
  githubAuthStepStatus: githubAuthStep.status,
2431
2624
  githubAuthStepReason: githubAuthStep.reason || "",
2432
2625
  dependencyInstallStatus: dependencyInstall.status,
@@ -2457,7 +2650,10 @@ async function runSetupFlow(forwardedArgs) {
2457
2650
  console.log(` Runtime : ${context.runtimeHome}`);
2458
2651
 
2459
2652
  const pendingItems = [];
2460
- 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
+ }
2461
2657
  if (!prereq.workerAvailable) pendingItems.push(`${config.codingWorker} not found on PATH — install it before starting`);
2462
2658
  if (config.codingWorker === "openclaw" && !process.env.OPENROUTER_API_KEY) {
2463
2659
  pendingItems.push("OPENROUTER_API_KEY not set — required for openclaw workers");
@@ -2496,7 +2692,7 @@ async function runSetupFlow(forwardedArgs) {
2496
2692
  }
2497
2693
  } else {
2498
2694
  console.log("\n Getting started:");
2499
- 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`);
2500
2696
  console.log(" 2. ACP picks it up automatically, assigns a worker, and opens a PR");
2501
2697
  console.log(" 3. Watch progress in the dashboard or with 'runtime status'");
2502
2698
  }
@@ -2509,11 +2705,13 @@ async function runSetupFlow(forwardedArgs) {
2509
2705
  // Machine-readable KV output for non-interactive / scripted runs
2510
2706
  console.log("\nSetup complete.");
2511
2707
  console.log(`- profile: ${config.profileId}`);
2708
+ console.log(`- forge provider: ${config.forge.provider}`);
2512
2709
  console.log(`- repo: ${config.repoSlug}`);
2513
2710
  console.log(`- runtime home: ${context.runtimeHome}`);
2514
2711
 
2515
2712
  console.log(`SETUP_STATUS=ok`);
2516
2713
  console.log(`PROFILE_ID=${config.profileId}`);
2714
+ console.log(`FORGE_PROVIDER=${config.forge.provider}`);
2517
2715
  console.log(`REPO_SLUG=${config.repoSlug}`);
2518
2716
  console.log(`REPO_ROOT=${config.paths.repoRoot}`);
2519
2717
  console.log(`AGENT_ROOT=${config.paths.agentRoot}`);
@@ -2558,7 +2756,7 @@ async function runSetupFlow(forwardedArgs) {
2558
2756
  if (workerSetupStep.guide.verifyExamples[0]) {
2559
2757
  console.log(`WORKER_BACKEND_VERIFY_EXAMPLE=${workerSetupStep.guide.verifyExamples[0]}`);
2560
2758
  }
2561
- console.log(`GITHUB_AUTH_STATUS=${prereq.ghAuthOk ? "ok" : "not-ready"}`);
2759
+ console.log(`GITHUB_AUTH_STATUS=${prereq.forgeAuthOk ? "ok" : "not-ready"}`);
2562
2760
  console.log(`FINAL_FIXUP_STATUS=${finalFixup.status}`);
2563
2761
  console.log(`FINAL_FIXUP_ACTIONS=${finalFixup.actions.join(",")}`);
2564
2762
  console.log(`DEPENDENCY_INSTALL_STATUS=${dependencyInstall.status}`);