agent-control-plane 0.1.16 → 0.3.0

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 (63) hide show
  1. package/README.md +93 -14
  2. package/bin/pr-risk.sh +28 -6
  3. package/hooks/heartbeat-hooks.sh +62 -22
  4. package/npm/bin/agent-control-plane.js +360 -10
  5. package/package.json +6 -3
  6. package/references/architecture.md +8 -0
  7. package/references/control-plane-map.md +6 -2
  8. package/references/release-checklist.md +0 -2
  9. package/tools/bin/agent-github-update-labels +6 -1
  10. package/tools/bin/agent-project-catch-up-issue-pr-links +118 -0
  11. package/tools/bin/agent-project-catch-up-merged-prs +78 -21
  12. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +123 -0
  13. package/tools/bin/agent-project-cleanup-session +132 -4
  14. package/tools/bin/agent-project-heartbeat-loop +116 -1461
  15. package/tools/bin/agent-project-reconcile-issue-session +90 -117
  16. package/tools/bin/agent-project-reconcile-pr-session +76 -111
  17. package/tools/bin/agent-project-run-claude-session +12 -2
  18. package/tools/bin/agent-project-run-codex-resilient +86 -9
  19. package/tools/bin/agent-project-run-codex-session +16 -5
  20. package/tools/bin/agent-project-run-kilo-session +356 -14
  21. package/tools/bin/agent-project-run-ollama-session +658 -0
  22. package/tools/bin/agent-project-run-openclaw-session +37 -25
  23. package/tools/bin/agent-project-run-opencode-session +364 -14
  24. package/tools/bin/agent-project-run-pi-session +479 -0
  25. package/tools/bin/agent-project-worker-status +11 -8
  26. package/tools/bin/cleanup-worktree.sh +6 -1
  27. package/tools/bin/flow-config-lib.sh +196 -3
  28. package/tools/bin/flow-resident-worker-lib.sh +120 -2
  29. package/tools/bin/flow-shell-lib.sh +29 -2
  30. package/tools/bin/heartbeat-loop-cache-lib.sh +164 -0
  31. package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
  32. package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
  33. package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
  34. package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
  35. package/tools/bin/heartbeat-recovery-preflight.sh +13 -1
  36. package/tools/bin/heartbeat-safe-auto.sh +119 -20
  37. package/tools/bin/install-project-launchd.sh +19 -2
  38. package/tools/bin/prepare-worktree.sh +4 -4
  39. package/tools/bin/profile-activate.sh +2 -2
  40. package/tools/bin/profile-adopt.sh +2 -2
  41. package/tools/bin/project-init.sh +1 -1
  42. package/tools/bin/project-launchd-bootstrap.sh +11 -8
  43. package/tools/bin/project-runtimectl.sh +90 -7
  44. package/tools/bin/provider-cooldown-state.sh +14 -14
  45. package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
  46. package/tools/bin/render-flow-config.sh +30 -33
  47. package/tools/bin/resident-issue-controller-lib.sh +448 -0
  48. package/tools/bin/resident-issue-queue-status.py +35 -0
  49. package/tools/bin/run-codex-task.sh +53 -4
  50. package/tools/bin/scaffold-profile.sh +18 -3
  51. package/tools/bin/start-issue-worker.sh +1 -1
  52. package/tools/bin/start-pr-fix-worker.sh +30 -0
  53. package/tools/bin/start-pr-review-worker.sh +31 -0
  54. package/tools/bin/start-resident-issue-loop.sh +27 -438
  55. package/tools/bin/sync-agent-repo.sh +2 -2
  56. package/tools/bin/sync-dependency-baseline.sh +3 -3
  57. package/tools/bin/sync-shared-agent-home.sh +4 -1
  58. package/tools/dashboard/app.js +7 -0
  59. package/tools/dashboard/dashboard_snapshot.py +13 -29
  60. package/tools/templates/pr-fix-template.md +3 -7
  61. package/tools/templates/pr-merge-repair-template.md +3 -7
  62. package/tools/templates/pr-review-template.md +2 -1
  63. package/SKILL.md +0 -149
@@ -57,7 +57,7 @@ Options:
57
57
  --retained-repo-root <path> Manual checkout root to keep linked in the profile
58
58
  --vscode-workspace-file <path>
59
59
  Workspace file path ACP should generate/use
60
- --coding-worker <backend> One of: codex, claude, openclaw
60
+ --coding-worker <backend> One of: codex, claude, openclaw, ollama, pi, opencode, kilo
61
61
  --force Overwrite an existing profile
62
62
  --skip-anchor-sync Skip profile-adopt anchor repo sync
63
63
  --skip-workspace-sync Skip profile-adopt workspace sync
@@ -75,6 +75,11 @@ Options:
75
75
  --no-start-runtime Do not start the runtime after setup
76
76
  --install-launchd Install macOS autostart after a successful runtime start
77
77
  --no-install-launchd Do not install macOS autostart
78
+ --start-dashboard Start the dashboard in background after setup
79
+ --no-start-dashboard Do not start the dashboard
80
+ --dashboard-port <port> Dashboard port (default: 8765)
81
+ --create-starter-issues Create recurring issues so ACP starts working immediately
82
+ --no-create-starter-issues Skip starter issue creation
78
83
  --yes Accept detected defaults without prompting
79
84
  --non-interactive Same as --yes
80
85
  --help Show this help
@@ -394,6 +399,9 @@ function parseSetupArgs(args) {
394
399
  json: false,
395
400
  startRuntime: null,
396
401
  installLaunchd: null,
402
+ startDashboard: null,
403
+ dashboardPort: 8765,
404
+ createStarterIssues: null,
397
405
  interactive: process.stdin.isTTY && process.stdout.isTTY,
398
406
  help: false
399
407
  };
@@ -481,6 +489,21 @@ function parseSetupArgs(args) {
481
489
  case "--no-install-launchd":
482
490
  options.installLaunchd = false;
483
491
  break;
492
+ case "--start-dashboard":
493
+ options.startDashboard = true;
494
+ break;
495
+ case "--no-start-dashboard":
496
+ options.startDashboard = false;
497
+ break;
498
+ case "--dashboard-port":
499
+ options.dashboardPort = parseInt(args[++index] || "8765", 10);
500
+ break;
501
+ case "--create-starter-issues":
502
+ options.createStarterIssues = true;
503
+ break;
504
+ case "--no-create-starter-issues":
505
+ options.createStarterIssues = false;
506
+ break;
484
507
  case "--yes":
485
508
  case "--non-interactive":
486
509
  options.interactive = false;
@@ -543,6 +566,179 @@ async function promptYesNo(rl, label, defaultValue) {
543
566
  return defaultValue;
544
567
  }
545
568
 
569
+ const STARTER_ISSUE_CATALOG = [
570
+ {
571
+ key: "code-quality",
572
+ title: "Keep code quality high: fix lint warnings, type errors, and dead code",
573
+ body: `## Recurring: Code Quality Sweep
574
+
575
+ Checklist:
576
+ - [ ] Fix any TypeScript / ESLint / type-check errors in the codebase
577
+ - [ ] Remove dead exports, unused imports, and unreachable code paths
578
+ - [ ] Resolve any TODO/FIXME comments that are straightforward to address
579
+
580
+ Agent schedule: every 4h
581
+
582
+ - Run \`pnpm typecheck\` (or the repo-equivalent lint/typecheck) after code changes and record verification.
583
+ `,
584
+ labels: ["agent-ready", "agent-keep-open"]
585
+ },
586
+ {
587
+ key: "test-coverage",
588
+ title: "Improve test coverage for critical and untested modules",
589
+ body: `## Recurring: Test Coverage Improvement
590
+
591
+ Checklist:
592
+ - [ ] Identify source files with zero or minimal test coverage
593
+ - [ ] Add focused unit or integration tests for the most critical untested paths
594
+ - [ ] Ensure new tests actually run and pass in the local test runner
595
+
596
+ Agent schedule: every 6h
597
+
598
+ - Run the narrowest relevant test command after code changes and record verification.
599
+ `,
600
+ labels: ["agent-ready", "agent-keep-open"]
601
+ },
602
+ {
603
+ key: "documentation",
604
+ title: "Keep documentation accurate and up to date",
605
+ body: `## Recurring: Documentation Refresh
606
+
607
+ Checklist:
608
+ - [ ] Update README sections that are outdated or reference removed features
609
+ - [ ] Add or fix JSDoc / inline comments for public APIs that lack them
610
+ - [ ] Ensure setup instructions and examples still work on a clean checkout
611
+
612
+ Agent schedule: every 8h
613
+
614
+ - Run any doc-build or link-check command after changes and record verification.
615
+ `,
616
+ labels: ["agent-ready", "agent-keep-open"]
617
+ },
618
+ {
619
+ key: "dependency-audit",
620
+ title: "Audit and update dependencies for security and freshness",
621
+ body: `## Recurring: Dependency Audit
622
+
623
+ Checklist:
624
+ - [ ] Run the package manager audit and fix any critical/high vulnerabilities
625
+ - [ ] Update patch-level dependencies that have safe upgrades available
626
+ - [ ] Verify the lockfile is consistent after changes
627
+
628
+ Agent schedule: every 12h
629
+
630
+ - Run \`pnpm audit\` and \`pnpm install --frozen-lockfile\` after changes and record verification.
631
+ `,
632
+ labels: ["agent-ready", "agent-keep-open"]
633
+ },
634
+ {
635
+ key: "refactor",
636
+ title: "Refactor complex or duplicated code for maintainability",
637
+ body: `## Recurring: Refactoring Sweep
638
+
639
+ Checklist:
640
+ - [ ] Identify functions or modules with high complexity or duplication
641
+ - [ ] Extract shared logic into well-named helpers or utilities
642
+ - [ ] Ensure refactored code passes existing tests without behavior changes
643
+
644
+ Agent schedule: every 8h
645
+
646
+ - Run the narrowest relevant test command after refactoring and record verification.
647
+ `,
648
+ labels: ["agent-ready", "agent-keep-open"]
649
+ }
650
+ ];
651
+
652
+ async function maybeCreateStarterIssues(options, config, prereq) {
653
+ const result = { status: "skipped", created: [], reason: "not-requested" };
654
+
655
+ if (options.createStarterIssues === false) {
656
+ return result;
657
+ }
658
+
659
+ if (!prereq.ghAuthOk) {
660
+ result.reason = "gh-auth-not-ready";
661
+ return result;
662
+ }
663
+
664
+ let shouldCreate = options.createStarterIssues === true;
665
+ if (!shouldCreate && options.interactive) {
666
+ const rl0 = createPromptInterface();
667
+ try {
668
+ shouldCreate = await promptYesNo(rl0, "\nCreate starter issues so ACP starts working immediately", true);
669
+ } finally {
670
+ rl0.close();
671
+ }
672
+ }
673
+ if (!shouldCreate) {
674
+ return result;
675
+ }
676
+
677
+ let toCreate;
678
+ if (options.interactive) {
679
+ const rlSel = createPromptInterface();
680
+ try {
681
+ console.log("\nSelect which recurring issues to create (ACP will work on these continuously):\n");
682
+ const selections = [];
683
+ for (const issue of STARTER_ISSUE_CATALOG) {
684
+ const selected = await promptYesNo(rlSel, ` ${issue.title}`, true);
685
+ selections.push({ issue, selected });
686
+ }
687
+ toCreate = selections.filter((s) => s.selected);
688
+ } finally {
689
+ rlSel.close();
690
+ }
691
+ } else {
692
+ // Non-interactive: create all starter issues
693
+ toCreate = STARTER_ISSUE_CATALOG.map((issue) => ({ issue, selected: true }));
694
+ }
695
+
696
+ if (toCreate.length === 0) {
697
+ console.log("No issues selected.");
698
+ return result;
699
+ }
700
+
701
+ // Ensure labels exist
702
+ console.log("\nCreating labels and issues...");
703
+ const requiredLabels = [
704
+ { name: "agent-ready", color: "0E8A16", description: "Ready for agent automation" },
705
+ { name: "agent-keep-open", color: "D4C5F9", description: "Recurring issue — agent works on this continuously" }
706
+ ];
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 });
709
+ }
710
+
711
+ 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();
722
+ result.created.push({ key: issue.key, title: issue.title, url });
723
+ console.log(` Created: ${issue.title}`);
724
+ if (url) {
725
+ console.log(` ${url}`);
726
+ }
727
+ } else {
728
+ console.log(` Failed to create: ${issue.title}`);
729
+ const stderr = (ghResult.stderr || "").trim();
730
+ if (stderr) {
731
+ console.log(` ${stderr.split("\n")[0]}`);
732
+ }
733
+ }
734
+ }
735
+
736
+ result.status = result.created.length > 0 ? "ok" : "failed";
737
+ result.reason = "";
738
+
739
+ return result;
740
+ }
741
+
546
742
  function buildSetupPaths(platformHome, repoRoot, profileId, overrides) {
547
743
  const agentRoot = path.resolve(overrides.agentRoot || path.join(platformHome, "projects", profileId));
548
744
  const repoRootResolved = path.resolve(repoRoot);
@@ -578,6 +774,9 @@ function detectPackageManager() {
578
774
  if (commandExists("brew")) {
579
775
  return { name: "brew" };
580
776
  }
777
+ if (commandExists("apt")) {
778
+ return { name: "apt" };
779
+ }
581
780
  if (commandExists("apt-get")) {
582
781
  return { name: "apt-get" };
583
782
  }
@@ -590,6 +789,12 @@ function detectPackageManager() {
590
789
  if (commandExists("pacman")) {
591
790
  return { name: "pacman" };
592
791
  }
792
+ if (commandExists("zypper")) {
793
+ return { name: "zypper" };
794
+ }
795
+ if (commandExists("apk")) {
796
+ return { name: "apk" };
797
+ }
593
798
  return null;
594
799
  }
595
800
 
@@ -613,6 +818,24 @@ function dependencyPackageMap(managerName) {
613
818
  python3: "python3",
614
819
  tmux: "tmux"
615
820
  };
821
+ case "apt":
822
+ return {
823
+ bash: "bash",
824
+ git: "git",
825
+ gh: "gh",
826
+ jq: "jq",
827
+ python3: "python3",
828
+ tmux: "tmux"
829
+ };
830
+ case "apk":
831
+ return {
832
+ bash: "bash",
833
+ git: "git",
834
+ gh: "gh",
835
+ jq: "jq",
836
+ python3: "python3",
837
+ tmux: "tmux"
838
+ };
616
839
  case "dnf":
617
840
  case "yum":
618
841
  return {
@@ -671,6 +894,10 @@ function buildDependencyInstallPlan(missingTools) {
671
894
  commands.push([...prefix, "apt-get", "update"]);
672
895
  commands.push([...prefix, "apt-get", "install", "-y", ...packages]);
673
896
  break;
897
+ case "apt":
898
+ commands.push([...prefix, "apt", "update"]);
899
+ commands.push([...prefix, "apt", "install", "-y", ...packages]);
900
+ break;
674
901
  case "dnf":
675
902
  commands.push([...prefix, "dnf", "install", "-y", ...packages]);
676
903
  break;
@@ -680,6 +907,12 @@ function buildDependencyInstallPlan(missingTools) {
680
907
  case "pacman":
681
908
  commands.push([...prefix, "pacman", "-Sy", "--noconfirm", ...packages]);
682
909
  break;
910
+ case "zypper":
911
+ commands.push([...prefix, "zypper", "install", "-y", ...packages]);
912
+ break;
913
+ case "apk":
914
+ commands.push([...prefix, "apk", "add", "--no-cache", ...packages]);
915
+ break;
683
916
  default:
684
917
  return null;
685
918
  }
@@ -721,7 +954,7 @@ async function maybeInstallMissingDependencies(options, prereq) {
721
954
  if (!plan) {
722
955
  console.log("\nACP found missing core dependencies but cannot install them automatically on this machine.");
723
956
  console.log(`- missing tools: ${prereq.missingRequired.join(", ")}`);
724
- console.log("- supported auto-install package managers today: brew, apt-get, dnf, yum, pacman");
957
+ console.log("- supported auto-install package managers today: brew, apt, apt-get, dnf, yum, pacman, zypper, apk");
725
958
  return {
726
959
  status: "unavailable",
727
960
  reason: "no-supported-package-manager",
@@ -1355,6 +1588,9 @@ function printSetupDryRunPlan(context, config, plan) {
1355
1588
  console.log(` command preview: ${plan.workerInstallPlan.commands.map(formatCommand).join(" && ")}`);
1356
1589
  }
1357
1590
  console.log(`- GitHub auth step: ${plan.githubAuthAction.status}${plan.githubAuthAction.reason ? ` (${plan.githubAuthAction.reason})` : ""}`);
1591
+ if (plan.githubAuthAction.status !== "not-needed") {
1592
+ console.log(` command preview: gh auth login`);
1593
+ }
1358
1594
  console.log(`- runtime start: ${plan.runtimeStartAction.status}${plan.runtimeStartAction.reason ? ` (${plan.runtimeStartAction.reason})` : ""}`);
1359
1595
  if (process.platform === "darwin") {
1360
1596
  console.log(`- launchd install: ${plan.launchdAction.status}${plan.launchdAction.reason ? ` (${plan.launchdAction.reason})` : ""}`);
@@ -1622,10 +1858,16 @@ async function collectSetupConfig(options, context) {
1622
1858
  let profileId = suggestedProfileId;
1623
1859
  let codingWorker = suggestedWorker;
1624
1860
 
1625
- if (!fs.existsSync(detectedRepoRoot)) {
1861
+ const detectedRepoRootExists = fs.existsSync(detectedRepoRoot);
1862
+ if (!detectedRepoRootExists && !options.allowMissingRepo) {
1626
1863
  throw new Error(`setup repo root does not exist: ${detectedRepoRoot}`);
1627
1864
  }
1628
1865
 
1866
+ if (options.allowMissingRepo && !detectedRepoRootExists) {
1867
+ console.log(`\nSource repo root not found yet: ${detectedRepoRoot}`);
1868
+ console.log("- continuing because --allow-missing-repo was set; profile adopt will run in missing-repo mode.");
1869
+ }
1870
+
1629
1871
  if (!options.interactive) {
1630
1872
  if (!repoSlug) {
1631
1873
  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");
@@ -1641,8 +1883,8 @@ async function collectSetupConfig(options, context) {
1641
1883
  profileId = sanitizeProfileId(await promptText(rl, "Profile id", profileId));
1642
1884
 
1643
1885
  let workerInput = codingWorker;
1644
- while (!["codex", "claude", "openclaw"].includes(workerInput)) {
1645
- workerInput = await promptText(rl, "Coding worker (codex / claude / openclaw)", codingWorker || "openclaw");
1886
+ while (!["codex", "claude", "openclaw", "ollama", "pi", "opencode", "kilo"].includes(workerInput)) {
1887
+ workerInput = await promptText(rl, "Coding worker (codex / claude / openclaw / ollama / pi / opencode / kilo)", codingWorker || "openclaw");
1646
1888
  }
1647
1889
  codingWorker = workerInput;
1648
1890
  } finally {
@@ -1650,7 +1892,7 @@ async function collectSetupConfig(options, context) {
1650
1892
  }
1651
1893
  }
1652
1894
 
1653
- if (!["codex", "claude", "openclaw"].includes(codingWorker)) {
1895
+ if (!["codex", "claude", "openclaw", "ollama", "pi", "opencode", "kilo"].includes(codingWorker)) {
1654
1896
  throw new Error(`unsupported coding worker: ${codingWorker}`);
1655
1897
  }
1656
1898
 
@@ -1687,6 +1929,9 @@ async function collectSetupConfig(options, context) {
1687
1929
  if (process.platform === "darwin" && options.installLaunchd === null && options.startRuntime) {
1688
1930
  options.installLaunchd = await promptYesNo(rl, "Install macOS autostart for this profile", false);
1689
1931
  }
1932
+ if (options.startDashboard === null) {
1933
+ options.startDashboard = await promptYesNo(rl, "Start the monitoring dashboard in background", true);
1934
+ }
1690
1935
  } finally {
1691
1936
  rl.close();
1692
1937
  }
@@ -1698,11 +1943,16 @@ async function collectSetupConfig(options, context) {
1698
1943
  if (options.installLaunchd === null) {
1699
1944
  options.installLaunchd = false;
1700
1945
  }
1946
+ if (options.startDashboard === null) {
1947
+ options.startDashboard = false;
1948
+ }
1701
1949
 
1702
1950
  return {
1703
1951
  ...config,
1704
1952
  startRuntime: Boolean(options.startRuntime),
1705
- installLaunchd: Boolean(options.installLaunchd)
1953
+ installLaunchd: Boolean(options.installLaunchd),
1954
+ startDashboard: Boolean(options.startDashboard),
1955
+ dashboardPort: options.dashboardPort
1706
1956
  };
1707
1957
  }
1708
1958
 
@@ -1932,9 +2182,10 @@ async function runSetupFlow(forwardedArgs) {
1932
2182
  let workerSetupStep = await maybeShowWorkerSetupGuide(options, prereq);
1933
2183
  prereq = collectPrereqStatus(config.codingWorker);
1934
2184
 
1935
- // Check OpenRouter API key when openclaw is selected
1936
- if (config.codingWorker === "openclaw" && !process.env.OPENROUTER_API_KEY) {
1937
- console.log("\nOpenClaw requires an OpenRouter API key (OPENROUTER_API_KEY).");
2185
+ // Check OpenRouter API key when openclaw or pi is selected
2186
+ if ((config.codingWorker === "openclaw" || config.codingWorker === "pi") && !process.env.OPENROUTER_API_KEY) {
2187
+ const workerLabel = config.codingWorker === "openclaw" ? "OpenClaw" : "Pi";
2188
+ console.log(`\n${workerLabel} requires an OpenRouter API key (OPENROUTER_API_KEY).`);
1938
2189
  console.log("- Get a free key at: https://openrouter.ai/keys");
1939
2190
  if (options.interactive) {
1940
2191
  const rl = createPromptInterface();
@@ -1957,6 +2208,29 @@ async function runSetupFlow(forwardedArgs) {
1957
2208
  }
1958
2209
  }
1959
2210
 
2211
+ // Check Ollama readiness when ollama is selected
2212
+ if (config.codingWorker === "ollama") {
2213
+ const ollamaRunning = spawnSync("curl", ["-sf", "http://localhost:11434/api/tags"], { timeout: 5000 });
2214
+ if (ollamaRunning.status !== 0) {
2215
+ console.log("\nOllama does not appear to be running at http://localhost:11434.");
2216
+ console.log("- Install from: https://ollama.com");
2217
+ console.log("- Start it with: ollama serve");
2218
+ } else {
2219
+ console.log("\nOllama is running. Checking available models...");
2220
+ try {
2221
+ const tagsJson = JSON.parse(ollamaRunning.stdout.toString());
2222
+ const modelNames = (tagsJson.models || []).map((m) => m.name || m.model || "").filter(Boolean);
2223
+ if (modelNames.length === 0) {
2224
+ console.log("No models pulled yet. Pull one with: ollama pull qwen2.5-coder:7b");
2225
+ } else {
2226
+ console.log(`Available models: ${modelNames.slice(0, 5).join(", ")}${modelNames.length > 5 ? ` (+${modelNames.length - 5} more)` : ""}`);
2227
+ }
2228
+ } catch (_) {
2229
+ console.log("Could not parse model list. Ensure a model is pulled: ollama pull qwen2.5-coder:7b");
2230
+ }
2231
+ }
2232
+ }
2233
+
1960
2234
  if (options.interactive) {
1961
2235
  printWizardStep(4, 4, "Install");
1962
2236
  }
@@ -2048,6 +2322,56 @@ async function runSetupFlow(forwardedArgs) {
2048
2322
  }
2049
2323
  }
2050
2324
 
2325
+ let dashboardStatus = "skipped";
2326
+ let dashboardReason = "not-requested";
2327
+ let dashboardUrl = "";
2328
+ if (config.startDashboard) {
2329
+ const dashboardScript = path.join(scopedContext.stableSkillRoot || scopedContext.packageRoot, "tools", "bin", "serve-dashboard.sh");
2330
+ const dashboardLogDir = path.join(config.paths.agentRoot || context.platformHome, "dashboard-logs");
2331
+ fs.mkdirSync(dashboardLogDir, { recursive: true });
2332
+ const dashboardLogFile = path.join(dashboardLogDir, "dashboard.log");
2333
+ const dashboardPidFile = path.join(dashboardLogDir, "dashboard.pid");
2334
+
2335
+ // Kill any existing dashboard on the same port
2336
+ try {
2337
+ if (fs.existsSync(dashboardPidFile)) {
2338
+ const oldPid = fs.readFileSync(dashboardPidFile, "utf8").trim();
2339
+ if (oldPid && /^\d+$/.test(oldPid)) {
2340
+ try { process.kill(Number(oldPid), "SIGTERM"); } catch (_) { /* already dead */ }
2341
+ }
2342
+ }
2343
+ } catch (_) { /* ignore */ }
2344
+
2345
+ console.log(`\n== Start dashboard (background, port ${config.dashboardPort}) ==`);
2346
+ syncRuntimeHome(context, { stdio: "pipe" });
2347
+ const rtCtx = createRuntimeExecutionContext(context);
2348
+ const rtDashboardScript = path.join(rtCtx.stableSkillRoot, "tools", "bin", "serve-dashboard.sh");
2349
+ const { spawn } = require("child_process");
2350
+ const logHandle = fs.openSync(dashboardLogFile, "a");
2351
+ const dashboardProc = spawn("bash", [rtDashboardScript, "--host", "127.0.0.1", "--port", String(config.dashboardPort)], {
2352
+ detached: true,
2353
+ stdio: ["ignore", logHandle, logHandle],
2354
+ env: { ...process.env, ACP_PROFILE_REGISTRY_ROOT: context.profileRegistryRoot }
2355
+ });
2356
+ dashboardProc.unref();
2357
+ fs.closeSync(logHandle);
2358
+
2359
+ if (dashboardProc.pid) {
2360
+ fs.writeFileSync(dashboardPidFile, `${dashboardProc.pid}\n`);
2361
+ dashboardUrl = `http://127.0.0.1:${config.dashboardPort}`;
2362
+ dashboardStatus = "ok";
2363
+ dashboardReason = "";
2364
+ console.log(`Dashboard running at ${dashboardUrl} (PID ${dashboardProc.pid})`);
2365
+ console.log(`Log: ${dashboardLogFile}`);
2366
+ } else {
2367
+ dashboardStatus = "failed";
2368
+ dashboardReason = "spawn-failed";
2369
+ console.log("Dashboard failed to start.");
2370
+ }
2371
+ }
2372
+
2373
+ const starterIssues = await maybeCreateStarterIssues(options, config, prereq);
2374
+
2051
2375
  const finalFixup = await maybeRunFinalSetupFixups(options, scopedContext, config, {
2052
2376
  anchorSync,
2053
2377
  prereq,
@@ -2148,12 +2472,38 @@ async function runSetupFlow(forwardedArgs) {
2148
2472
  }
2149
2473
  }
2150
2474
 
2475
+ if (dashboardStatus === "ok" && dashboardUrl) {
2476
+ console.log(`\n Dashboard: ${dashboardUrl}`);
2477
+ }
2478
+
2151
2479
  console.log("\n Next commands:");
2152
2480
  if (runtimeStartStatus !== "ok") {
2153
2481
  console.log(` npx agent-control-plane@latest runtime start --profile-id ${config.profileId}`);
2154
2482
  }
2155
2483
  console.log(` npx agent-control-plane@latest runtime status --profile-id ${config.profileId}`);
2484
+ if (dashboardStatus !== "ok") {
2485
+ console.log(` npx agent-control-plane@latest dashboard`);
2486
+ }
2156
2487
  console.log(` npx agent-control-plane@latest doctor`);
2488
+
2489
+ if (starterIssues.created.length > 0) {
2490
+ console.log("\n Starter issues created (ACP will start working on these):");
2491
+ for (const issue of starterIssues.created) {
2492
+ console.log(` - ${issue.title}`);
2493
+ if (issue.url) {
2494
+ console.log(` ${issue.url}`);
2495
+ }
2496
+ }
2497
+ } else {
2498
+ console.log("\n Getting started:");
2499
+ console.log(` 1. Add the label 'agent-ready' to a GitHub issue in ${config.repoSlug}`);
2500
+ console.log(" 2. ACP picks it up automatically, assigns a worker, and opens a PR");
2501
+ console.log(" 3. Watch progress in the dashboard or with 'runtime status'");
2502
+ }
2503
+ if (config.codingWorker === "openclaw" || config.codingWorker === "pi") {
2504
+ console.log(`\n Tip: ${config.codingWorker} uses free-tier models by default.`);
2505
+ console.log(" No API costs until you switch to a paid model in the profile YAML.");
2506
+ }
2157
2507
  console.log("");
2158
2508
  } else {
2159
2509
  // Machine-readable KV output for non-interactive / scripted runs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-control-plane",
3
- "version": "0.1.16",
3
+ "version": "0.3.0",
4
4
  "description": "Help a repo keep GitHub-driven coding agents running reliably without constant human babysitting",
5
5
  "homepage": "https://github.com/ducminhnguyen0319/agent-control-plane",
6
6
  "bugs": {
@@ -19,7 +19,6 @@
19
19
  },
20
20
  "files": [
21
21
  "README.md",
22
- "SKILL.md",
23
22
  "assets/workflow-catalog.json",
24
23
  "bin/agent-control-plane",
25
24
  "bin/issue-resource-class.sh",
@@ -48,7 +47,11 @@
48
47
  "scripts": {
49
48
  "doctor": "node ./npm/bin/agent-control-plane.js doctor",
50
49
  "smoke": "node ./npm/bin/agent-control-plane.js smoke",
51
- "test": "bash tools/tests/test-agent-control-plane-npm-cli.sh && bash tools/tests/test-agent-project-detached-launch-stable-cwd.sh && bash tools/tests/test-agent-project-claude-session-wrapper-reaps-child-on-term.sh && bash tools/tests/test-agent-project-claude-session-wrapper-does-not-retry-provider-quota.sh && bash tools/tests/test-agent-project-reconcile-issue-provider-quota-schedules-provider-cooldown.sh && bash tools/tests/test-pr-reconcile-hooks-refreshes-recurring-issue-checklist.sh && bash tools/tests/test-issue-reconcile-hooks-kick-scheduler-uses-profile.sh && bash tools/tests/test-profile-adopt-skip-anchor-sync-creates-agent-repo-root.sh && bash tools/tests/test-vendored-codex-quota-claude-oauth-only.sh && bash tools/tests/test-package-smoke-command.sh"
50
+ "test": "bash tools/tests/test-agent-control-plane-npm-cli.sh && bash tools/tests/test-agent-project-detached-launch-stable-cwd.sh && bash tools/tests/test-agent-project-claude-session-wrapper-reaps-child-on-term.sh && bash tools/tests/test-agent-project-claude-session-wrapper-does-not-retry-provider-quota.sh && bash tools/tests/test-agent-project-run-codex-resilient-uses-path-python-and-gnu-stat.sh && bash tools/tests/test-heartbeat-safe-auto-uses-path-python.sh && bash tools/tests/test-heartbeat-safe-auto-skips-self-sync.sh && bash tools/tests/test-agent-project-catch-up-terminal-prs-defaults-closed-hook.sh && bash tools/tests/test-agent-project-codex-session-wrapper-prefers-path-codex.sh && bash tools/tests/test-agent-project-codex-session-wrapper-recovers-var-tmp-logged-artifacts.sh && bash tools/tests/test-agent-project-cleanup-session-removes-registered-worktree-without-rg.sh && bash tools/tests/test-agent-project-cleanup-session-propagates-failure-with-session.sh && bash tools/tests/test-cleanup-worktree-syncs-workspace-after-cleanup-failure.sh && bash tools/tests/test-resident-issue-queue-status-contract.sh && bash tools/tests/test-agent-project-reconcile-issue-provider-quota-schedules-provider-cooldown.sh && bash tools/tests/test-agent-project-reconcile-issue-session-warns-on-cleanup-failure.sh && bash tools/tests/test-agent-project-reconcile-pr-session-warns-on-cleanup-failure.sh && bash tools/tests/test-pr-reconcile-hooks-refreshes-recurring-issue-checklist.sh && bash tools/tests/test-issue-reconcile-hooks-kick-scheduler-uses-profile.sh && bash tools/tests/test-profile-adopt-skip-anchor-sync-creates-agent-repo-root.sh && bash tools/tests/test-vendored-codex-quota-claude-oauth-only.sh && bash tools/tests/test-package-smoke-command.sh"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "provenance": true
52
55
  },
53
56
  "keywords": [
54
57
  "agents",
@@ -41,10 +41,18 @@ flowchart LR
41
41
  Router --> Codex["agent-project-run-codex-session"]
42
42
  Router --> Claude["agent-project-run-claude-session"]
43
43
  Router --> OpenClaw["agent-project-run-openclaw-session"]
44
+ Router --> Ollama["agent-project-run-ollama-session"]
45
+ Router --> Pi["agent-project-run-pi-session"]
46
+ Router --> OpenCode["agent-project-run-opencode-session"]
47
+ Router --> Kilo["agent-project-run-kilo-session"]
44
48
 
45
49
  Codex --> Artifacts["run.env / runner.env /\nresult.env / verification.jsonl"]
46
50
  Claude --> Artifacts
47
51
  OpenClaw --> Artifacts
52
+ Ollama --> Artifacts
53
+ Pi --> Artifacts
54
+ OpenCode --> Artifacts
55
+ Kilo --> Artifacts
48
56
 
49
57
  Artifacts --> Reconcile["reconcile-issue-worker.sh\nreconcile-pr-worker.sh"]
50
58
  Reconcile --> GitHub["GitHub issues / PRs / labels / comments"]
@@ -36,10 +36,14 @@ roots, labels, worker preferences, prompts, and project-specific guardrails.
36
36
  Launches Claude-backed worker sessions.
37
37
  - `tools/bin/agent-project-run-openclaw-session`
38
38
  Launches OpenClaw-backed worker sessions.
39
+ - `tools/bin/agent-project-run-ollama-session`
40
+ Launches Ollama-backed worker sessions with a Node.js agentic loop.
41
+ - `tools/bin/agent-project-run-pi-session`
42
+ Launches Pi-backed worker sessions in `--print --no-session` mode.
39
43
  - `tools/bin/agent-project-run-opencode-session`
40
- Placeholder adapter stub for the planned `opencode` backend.
44
+ Launches Crush (formerly OpenCode) worker sessions via `crush run`.
41
45
  - `tools/bin/agent-project-run-kilo-session`
42
- Placeholder adapter stub for the planned `kilo` backend.
46
+ Launches Kilo Code worker sessions via `kilo run --auto --format json`.
43
47
  - `tools/bin/project-init.sh`
44
48
  Runs scaffold + smoke + adopt + runtime sync for one installed profile.
45
49
  - `tools/bin/scaffold-profile.sh`
@@ -9,8 +9,6 @@ Maintainer checklist for shipping a new public package release of
9
9
  - confirm `package.json` version is intentional
10
10
  - review `CHANGELOG.md` and prepare release notes from
11
11
  `.github/release-template.md`
12
- - refresh README demo media if the dashboard UI changed:
13
- `bash tools/bin/render-dashboard-demo-media.sh`
14
12
  - review `README.md`, `SECURITY.md`, `CONTRIBUTING.md`, and `CLA.md` for stale
15
13
  links or policy text
16
14
  - if a public GitHub repo now exists, set or verify the `homepage`,
@@ -48,7 +48,12 @@ if [[ -z "$repo_slug" || -z "$number" ]]; then
48
48
  fi
49
49
 
50
50
  resource="issues/${number}"
51
- current_json="$(flow_github_api_repo "${repo_slug}" "${resource}")"
51
+ # Use caller-provided cached JSON if available to skip the GET call
52
+ if [[ -n "${ACP_CACHED_ISSUE_JSON:-}" ]]; then
53
+ current_json="${ACP_CACHED_ISSUE_JSON}"
54
+ else
55
+ current_json="$(flow_github_api_repo "${repo_slug}" "${resource}")"
56
+ fi
52
57
  add_json="$(jq -R . <"$add_file" | jq -s .)"
53
58
  remove_json="$(jq -R . <"$remove_file" | jq -s .)"
54
59
  payload="$(