agent-control-plane 0.1.14 → 0.2.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 (53) hide show
  1. package/README.md +323 -349
  2. package/bin/pr-risk.sh +28 -6
  3. package/hooks/heartbeat-hooks.sh +62 -22
  4. package/npm/bin/agent-control-plane.js +434 -12
  5. package/package.json +1 -1
  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 +77 -21
  12. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +123 -0
  13. package/tools/bin/agent-project-cleanup-session +84 -0
  14. package/tools/bin/agent-project-heartbeat-loop +10 -3
  15. package/tools/bin/agent-project-reconcile-issue-session +45 -12
  16. package/tools/bin/agent-project-reconcile-pr-session +25 -0
  17. package/tools/bin/agent-project-run-claude-session +2 -2
  18. package/tools/bin/agent-project-run-codex-resilient +57 -2
  19. package/tools/bin/agent-project-run-kilo-session +346 -14
  20. package/tools/bin/agent-project-run-ollama-session +658 -0
  21. package/tools/bin/agent-project-run-openclaw-session +73 -25
  22. package/tools/bin/agent-project-run-opencode-session +354 -14
  23. package/tools/bin/agent-project-run-pi-session +479 -0
  24. package/tools/bin/agent-project-worker-status +38 -1
  25. package/tools/bin/flow-config-lib.sh +123 -3
  26. package/tools/bin/flow-resident-worker-lib.sh +1 -1
  27. package/tools/bin/flow-shell-lib.sh +7 -2
  28. package/tools/bin/heartbeat-recovery-preflight.sh +1 -0
  29. package/tools/bin/heartbeat-safe-auto.sh +105 -17
  30. package/tools/bin/install-project-launchd.sh +19 -2
  31. package/tools/bin/prepare-worktree.sh +4 -4
  32. package/tools/bin/profile-activate.sh +2 -2
  33. package/tools/bin/profile-adopt.sh +2 -2
  34. package/tools/bin/project-init.sh +1 -1
  35. package/tools/bin/project-runtimectl.sh +90 -7
  36. package/tools/bin/provider-cooldown-state.sh +14 -14
  37. package/tools/bin/render-flow-config.sh +30 -33
  38. package/tools/bin/run-codex-task.sh +53 -4
  39. package/tools/bin/scaffold-profile.sh +18 -3
  40. package/tools/bin/start-issue-worker.sh +4 -1
  41. package/tools/bin/start-pr-fix-worker.sh +33 -0
  42. package/tools/bin/start-pr-review-worker.sh +34 -0
  43. package/tools/bin/start-resident-issue-loop.sh +5 -4
  44. package/tools/bin/sync-agent-repo.sh +2 -2
  45. package/tools/bin/sync-dependency-baseline.sh +3 -3
  46. package/tools/bin/sync-shared-agent-home.sh +4 -1
  47. package/tools/dashboard/app.js +62 -0
  48. package/tools/dashboard/dashboard_snapshot.py +53 -4
  49. package/tools/dashboard/index.html +5 -1
  50. package/tools/dashboard/styles.css +97 -20
  51. package/tools/templates/pr-fix-template.md +4 -8
  52. package/tools/templates/pr-merge-repair-template.md +4 -8
  53. package/tools/templates/pr-review-template.md +2 -1
@@ -22,6 +22,7 @@ Commands:
22
22
  help Show this help
23
23
  version Print package version
24
24
  setup Guided setup flow for one repo profile
25
+ onboard Alias for setup
25
26
  sync Publish the packaged runtime into ~/.agent-runtime
26
27
  install Alias for sync
27
28
  init Scaffold and adopt a project profile
@@ -56,7 +57,7 @@ Options:
56
57
  --retained-repo-root <path> Manual checkout root to keep linked in the profile
57
58
  --vscode-workspace-file <path>
58
59
  Workspace file path ACP should generate/use
59
- --coding-worker <backend> One of: codex, claude, openclaw
60
+ --coding-worker <backend> One of: codex, claude, openclaw, ollama, pi, opencode, kilo
60
61
  --force Overwrite an existing profile
61
62
  --skip-anchor-sync Skip profile-adopt anchor repo sync
62
63
  --skip-workspace-sync Skip profile-adopt workspace sync
@@ -74,6 +75,11 @@ Options:
74
75
  --no-start-runtime Do not start the runtime after setup
75
76
  --install-launchd Install macOS autostart after a successful runtime start
76
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
77
83
  --yes Accept detected defaults without prompting
78
84
  --non-interactive Same as --yes
79
85
  --help Show this help
@@ -393,6 +399,9 @@ function parseSetupArgs(args) {
393
399
  json: false,
394
400
  startRuntime: null,
395
401
  installLaunchd: null,
402
+ startDashboard: null,
403
+ dashboardPort: 8765,
404
+ createStarterIssues: null,
396
405
  interactive: process.stdin.isTTY && process.stdout.isTTY,
397
406
  help: false
398
407
  };
@@ -480,6 +489,21 @@ function parseSetupArgs(args) {
480
489
  case "--no-install-launchd":
481
490
  options.installLaunchd = false;
482
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;
483
507
  case "--yes":
484
508
  case "--non-interactive":
485
509
  options.interactive = false;
@@ -503,6 +527,20 @@ function createPromptInterface() {
503
527
  });
504
528
  }
505
529
 
530
+ function printWizardBanner() {
531
+ console.log("============================================================");
532
+ console.log(" Agent Control Plane — Setup Wizard");
533
+ console.log("============================================================");
534
+ console.log("");
535
+ console.log("This wizard will guide you through setting up one repo profile.");
536
+ console.log("Press Enter at any prompt to accept the value shown in [brackets].");
537
+ console.log("");
538
+ }
539
+
540
+ function printWizardStep(step, total, title) {
541
+ console.log(`\n[${step}/${total}] ${title}`);
542
+ }
543
+
506
544
  function question(rl, prompt) {
507
545
  return new Promise((resolve) => rl.question(prompt, resolve));
508
546
  }
@@ -528,6 +566,179 @@ async function promptYesNo(rl, label, defaultValue) {
528
566
  return defaultValue;
529
567
  }
530
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
+
531
742
  function buildSetupPaths(platformHome, repoRoot, profileId, overrides) {
532
743
  const agentRoot = path.resolve(overrides.agentRoot || path.join(platformHome, "projects", profileId));
533
744
  const repoRootResolved = path.resolve(repoRoot);
@@ -575,6 +786,9 @@ function detectPackageManager() {
575
786
  if (commandExists("pacman")) {
576
787
  return { name: "pacman" };
577
788
  }
789
+ if (commandExists("zypper")) {
790
+ return { name: "zypper" };
791
+ }
578
792
  return null;
579
793
  }
580
794
 
@@ -665,6 +879,9 @@ function buildDependencyInstallPlan(missingTools) {
665
879
  case "pacman":
666
880
  commands.push([...prefix, "pacman", "-Sy", "--noconfirm", ...packages]);
667
881
  break;
882
+ case "zypper":
883
+ commands.push([...prefix, "zypper", "install", "-y", ...packages]);
884
+ break;
668
885
  default:
669
886
  return null;
670
887
  }
@@ -706,7 +923,7 @@ async function maybeInstallMissingDependencies(options, prereq) {
706
923
  if (!plan) {
707
924
  console.log("\nACP found missing core dependencies but cannot install them automatically on this machine.");
708
925
  console.log(`- missing tools: ${prereq.missingRequired.join(", ")}`);
709
- console.log("- supported auto-install package managers today: brew, apt-get, dnf, yum, pacman");
926
+ console.log("- supported auto-install package managers today: brew, apt-get, dnf, yum, pacman, zypper");
710
927
  return {
711
928
  status: "unavailable",
712
929
  reason: "no-supported-package-manager",
@@ -1340,8 +1557,13 @@ function printSetupDryRunPlan(context, config, plan) {
1340
1557
  console.log(` command preview: ${plan.workerInstallPlan.commands.map(formatCommand).join(" && ")}`);
1341
1558
  }
1342
1559
  console.log(`- GitHub auth step: ${plan.githubAuthAction.status}${plan.githubAuthAction.reason ? ` (${plan.githubAuthAction.reason})` : ""}`);
1560
+ if (plan.githubAuthAction.status !== "not-needed") {
1561
+ console.log(` command preview: gh auth login`);
1562
+ }
1343
1563
  console.log(`- runtime start: ${plan.runtimeStartAction.status}${plan.runtimeStartAction.reason ? ` (${plan.runtimeStartAction.reason})` : ""}`);
1344
- console.log(`- launchd install: ${plan.launchdAction.status}${plan.launchdAction.reason ? ` (${plan.launchdAction.reason})` : ""}`);
1564
+ if (process.platform === "darwin") {
1565
+ console.log(`- launchd install: ${plan.launchdAction.status}${plan.launchdAction.reason ? ` (${plan.launchdAction.reason})` : ""}`);
1566
+ }
1345
1567
  }
1346
1568
 
1347
1569
  function buildSetupResultPayload(params) {
@@ -1457,6 +1679,23 @@ async function maybeRunFinalSetupFixups(options, scopedContext, config, currentS
1457
1679
  console.log(`- ${issue}`);
1458
1680
  }
1459
1681
 
1682
+ // Always show actionable hints so operators know what to fix,
1683
+ // even when running non-interactively (--yes / --json / CI).
1684
+ if (!currentState.prereq.coreToolsOk) {
1685
+ const missing = currentState.prereq.missingRequired.join(", ");
1686
+ console.log(` Fix: install missing core tools (${missing})`);
1687
+ }
1688
+ if (!currentState.prereq.workerAvailable) {
1689
+ const worker = currentState.prereq.workerCommand;
1690
+ if (worker === "codex") console.log(" Fix: npm install -g @openai/codex && codex login");
1691
+ else if (worker === "openclaw") console.log(" Fix: npm install -g openclaw && openclaw setup");
1692
+ else if (worker === "claude") console.log(" Fix: npm install -g @anthropic-ai/claude-code && claude auth login");
1693
+ else console.log(` Fix: install ${worker} and add it to PATH`);
1694
+ }
1695
+ if (!currentState.prereq.ghAuthOk) {
1696
+ console.log(" Fix: run gh auth login");
1697
+ }
1698
+
1460
1699
  if (!options.interactive) {
1461
1700
  return {
1462
1701
  status: "skipped",
@@ -1597,25 +1836,26 @@ async function collectSetupConfig(options, context) {
1597
1836
  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");
1598
1837
  }
1599
1838
  } else {
1839
+ printWizardBanner();
1600
1840
  const rl = createPromptInterface();
1601
1841
  try {
1602
- console.log("ACP setup will guide one repo profile from install to operator-ready defaults.");
1603
- console.log("Press Enter to accept the suggested value shown in brackets.\n");
1842
+ printWizardStep(1, 4, "Project details");
1604
1843
 
1605
1844
  repoRoot = path.resolve(await promptText(rl, "Local repo root", detectedRepoRoot));
1606
1845
  repoSlug = await promptText(rl, "GitHub repo slug", repoSlug || "");
1607
1846
  profileId = sanitizeProfileId(await promptText(rl, "Profile id", profileId));
1608
- codingWorker = await promptText(rl, "Coding worker (codex|claude|openclaw)", codingWorker);
1609
1847
 
1610
- if (!["codex", "claude", "openclaw"].includes(codingWorker)) {
1611
- throw new Error(`unsupported coding worker: ${codingWorker}`);
1848
+ let workerInput = codingWorker;
1849
+ while (!["codex", "claude", "openclaw", "ollama", "pi", "opencode", "kilo"].includes(workerInput)) {
1850
+ workerInput = await promptText(rl, "Coding worker (codex / claude / openclaw / ollama / pi / opencode / kilo)", codingWorker || "openclaw");
1612
1851
  }
1852
+ codingWorker = workerInput;
1613
1853
  } finally {
1614
1854
  rl.close();
1615
1855
  }
1616
1856
  }
1617
1857
 
1618
- if (!["codex", "claude", "openclaw"].includes(codingWorker)) {
1858
+ if (!["codex", "claude", "openclaw", "ollama", "pi", "opencode", "kilo"].includes(codingWorker)) {
1619
1859
  throw new Error(`unsupported coding worker: ${codingWorker}`);
1620
1860
  }
1621
1861
 
@@ -1630,6 +1870,9 @@ async function collectSetupConfig(options, context) {
1630
1870
  prereq
1631
1871
  };
1632
1872
 
1873
+ if (options.interactive) {
1874
+ printWizardStep(2, 4, "Review plan");
1875
+ }
1633
1876
  renderSetupSummary(config);
1634
1877
 
1635
1878
  if (options.interactive) {
@@ -1641,7 +1884,7 @@ async function collectSetupConfig(options, context) {
1641
1884
  }
1642
1885
  const shouldContinue = await promptYesNo(rl, "Continue with these values", true);
1643
1886
  if (!shouldContinue) {
1644
- throw new Error("setup cancelled");
1887
+ return null;
1645
1888
  }
1646
1889
  if (options.startRuntime === null) {
1647
1890
  options.startRuntime = await promptYesNo(rl, "Start the runtime after setup", true);
@@ -1649,6 +1892,9 @@ async function collectSetupConfig(options, context) {
1649
1892
  if (process.platform === "darwin" && options.installLaunchd === null && options.startRuntime) {
1650
1893
  options.installLaunchd = await promptYesNo(rl, "Install macOS autostart for this profile", false);
1651
1894
  }
1895
+ if (options.startDashboard === null) {
1896
+ options.startDashboard = await promptYesNo(rl, "Start the monitoring dashboard in background", true);
1897
+ }
1652
1898
  } finally {
1653
1899
  rl.close();
1654
1900
  }
@@ -1660,11 +1906,16 @@ async function collectSetupConfig(options, context) {
1660
1906
  if (options.installLaunchd === null) {
1661
1907
  options.installLaunchd = false;
1662
1908
  }
1909
+ if (options.startDashboard === null) {
1910
+ options.startDashboard = false;
1911
+ }
1663
1912
 
1664
1913
  return {
1665
1914
  ...config,
1666
1915
  startRuntime: Boolean(options.startRuntime),
1667
- installLaunchd: Boolean(options.installLaunchd)
1916
+ installLaunchd: Boolean(options.installLaunchd),
1917
+ startDashboard: Boolean(options.startDashboard),
1918
+ dashboardPort: options.dashboardPort
1668
1919
  };
1669
1920
  }
1670
1921
 
@@ -1733,6 +1984,10 @@ async function runSetupFlow(forwardedArgs) {
1733
1984
 
1734
1985
  try {
1735
1986
  const config = await collectSetupConfig(options, context);
1987
+ if (config === null) {
1988
+ console.log("\nSetup cancelled. Run again when you are ready.");
1989
+ return 0;
1990
+ }
1736
1991
  if (options.dryRun) {
1737
1992
  const plan = buildSetupDryRunPlan(options, context, config);
1738
1993
  printSetupDryRunPlan(context, config, plan);
@@ -1869,6 +2124,10 @@ async function runSetupFlow(forwardedArgs) {
1869
2124
  return 1;
1870
2125
  }
1871
2126
 
2127
+ if (options.interactive) {
2128
+ printWizardStep(3, 4, "Prerequisites");
2129
+ }
2130
+
1872
2131
  let prereq = config.prereq;
1873
2132
  let dependencyInstall = await maybeInstallMissingDependencies(options, prereq);
1874
2133
  if (dependencyInstall.status === "failed") {
@@ -1886,6 +2145,59 @@ async function runSetupFlow(forwardedArgs) {
1886
2145
  let workerSetupStep = await maybeShowWorkerSetupGuide(options, prereq);
1887
2146
  prereq = collectPrereqStatus(config.codingWorker);
1888
2147
 
2148
+ // Check OpenRouter API key when openclaw or pi is selected
2149
+ if ((config.codingWorker === "openclaw" || config.codingWorker === "pi") && !process.env.OPENROUTER_API_KEY) {
2150
+ const workerLabel = config.codingWorker === "openclaw" ? "OpenClaw" : "Pi";
2151
+ console.log(`\n${workerLabel} requires an OpenRouter API key (OPENROUTER_API_KEY).`);
2152
+ console.log("- Get a free key at: https://openrouter.ai/keys");
2153
+ if (options.interactive) {
2154
+ const rl = createPromptInterface();
2155
+ let apiKey = "";
2156
+ try {
2157
+ apiKey = (await promptText(rl, "OpenRouter API key (Enter to skip)", "")).trim();
2158
+ } finally {
2159
+ rl.close();
2160
+ }
2161
+ if (apiKey) {
2162
+ process.env.OPENROUTER_API_KEY = apiKey;
2163
+ console.log("API key set for this session.");
2164
+ console.log("To persist it, add the following to your shell profile (~/.zshrc or ~/.bashrc):");
2165
+ console.log(` export OPENROUTER_API_KEY=${JSON.stringify(apiKey)}`);
2166
+ } else {
2167
+ console.log("Skipped. Set OPENROUTER_API_KEY before starting the runtime.");
2168
+ }
2169
+ } else {
2170
+ console.log("Set OPENROUTER_API_KEY in your environment before starting the runtime.");
2171
+ }
2172
+ }
2173
+
2174
+ // Check Ollama readiness when ollama is selected
2175
+ if (config.codingWorker === "ollama") {
2176
+ const ollamaRunning = spawnSync("curl", ["-sf", "http://localhost:11434/api/tags"], { timeout: 5000 });
2177
+ if (ollamaRunning.status !== 0) {
2178
+ console.log("\nOllama does not appear to be running at http://localhost:11434.");
2179
+ console.log("- Install from: https://ollama.com");
2180
+ console.log("- Start it with: ollama serve");
2181
+ } else {
2182
+ console.log("\nOllama is running. Checking available models...");
2183
+ try {
2184
+ const tagsJson = JSON.parse(ollamaRunning.stdout.toString());
2185
+ const modelNames = (tagsJson.models || []).map((m) => m.name || m.model || "").filter(Boolean);
2186
+ if (modelNames.length === 0) {
2187
+ console.log("No models pulled yet. Pull one with: ollama pull qwen2.5-coder:7b");
2188
+ } else {
2189
+ console.log(`Available models: ${modelNames.slice(0, 5).join(", ")}${modelNames.length > 5 ? ` (+${modelNames.length - 5} more)` : ""}`);
2190
+ }
2191
+ } catch (_) {
2192
+ console.log("Could not parse model list. Ensure a model is pulled: ollama pull qwen2.5-coder:7b");
2193
+ }
2194
+ }
2195
+ }
2196
+
2197
+ if (options.interactive) {
2198
+ printWizardStep(4, 4, "Install");
2199
+ }
2200
+
1889
2201
  const scopedContext = buildScopedContext(context, config.profileId);
1890
2202
  const anchorSync = buildAnchorSyncDecision(options, config.paths.sourceRepoRoot);
1891
2203
 
@@ -1973,6 +2285,56 @@ async function runSetupFlow(forwardedArgs) {
1973
2285
  }
1974
2286
  }
1975
2287
 
2288
+ let dashboardStatus = "skipped";
2289
+ let dashboardReason = "not-requested";
2290
+ let dashboardUrl = "";
2291
+ if (config.startDashboard) {
2292
+ const dashboardScript = path.join(scopedContext.stableSkillRoot || scopedContext.packageRoot, "tools", "bin", "serve-dashboard.sh");
2293
+ const dashboardLogDir = path.join(config.paths.agentRoot || context.platformHome, "dashboard-logs");
2294
+ fs.mkdirSync(dashboardLogDir, { recursive: true });
2295
+ const dashboardLogFile = path.join(dashboardLogDir, "dashboard.log");
2296
+ const dashboardPidFile = path.join(dashboardLogDir, "dashboard.pid");
2297
+
2298
+ // Kill any existing dashboard on the same port
2299
+ try {
2300
+ if (fs.existsSync(dashboardPidFile)) {
2301
+ const oldPid = fs.readFileSync(dashboardPidFile, "utf8").trim();
2302
+ if (oldPid && /^\d+$/.test(oldPid)) {
2303
+ try { process.kill(Number(oldPid), "SIGTERM"); } catch (_) { /* already dead */ }
2304
+ }
2305
+ }
2306
+ } catch (_) { /* ignore */ }
2307
+
2308
+ console.log(`\n== Start dashboard (background, port ${config.dashboardPort}) ==`);
2309
+ syncRuntimeHome(context, { stdio: "pipe" });
2310
+ const rtCtx = createRuntimeExecutionContext(context);
2311
+ const rtDashboardScript = path.join(rtCtx.stableSkillRoot, "tools", "bin", "serve-dashboard.sh");
2312
+ const { spawn } = require("child_process");
2313
+ const logHandle = fs.openSync(dashboardLogFile, "a");
2314
+ const dashboardProc = spawn("bash", [rtDashboardScript, "--host", "127.0.0.1", "--port", String(config.dashboardPort)], {
2315
+ detached: true,
2316
+ stdio: ["ignore", logHandle, logHandle],
2317
+ env: { ...process.env, ACP_PROFILE_REGISTRY_ROOT: context.profileRegistryRoot }
2318
+ });
2319
+ dashboardProc.unref();
2320
+ fs.closeSync(logHandle);
2321
+
2322
+ if (dashboardProc.pid) {
2323
+ fs.writeFileSync(dashboardPidFile, `${dashboardProc.pid}\n`);
2324
+ dashboardUrl = `http://127.0.0.1:${config.dashboardPort}`;
2325
+ dashboardStatus = "ok";
2326
+ dashboardReason = "";
2327
+ console.log(`Dashboard running at ${dashboardUrl} (PID ${dashboardProc.pid})`);
2328
+ console.log(`Log: ${dashboardLogFile}`);
2329
+ } else {
2330
+ dashboardStatus = "failed";
2331
+ dashboardReason = "spawn-failed";
2332
+ console.log("Dashboard failed to start.");
2333
+ }
2334
+ }
2335
+
2336
+ const starterIssues = await maybeCreateStarterIssues(options, config, prereq);
2337
+
1976
2338
  const finalFixup = await maybeRunFinalSetupFixups(options, scopedContext, config, {
1977
2339
  anchorSync,
1978
2340
  prereq,
@@ -2047,12 +2409,71 @@ async function runSetupFlow(forwardedArgs) {
2047
2409
 
2048
2410
  if (options.json) {
2049
2411
  emitSetupJsonPayload(runPayload);
2412
+ } else if (options.interactive) {
2413
+ // Human-friendly summary for interactive terminal runs
2414
+ console.log("\n============================================================");
2415
+ console.log(" Setup complete!");
2416
+ console.log("============================================================");
2417
+ console.log(` Profile : ${config.profileId}`);
2418
+ console.log(` Repo : ${config.repoSlug}`);
2419
+ console.log(` Worker : ${config.codingWorker}`);
2420
+ console.log(` Runtime : ${context.runtimeHome}`);
2421
+
2422
+ const pendingItems = [];
2423
+ if (!prereq.ghAuthOk) pendingItems.push("GitHub CLI not authenticated — run: gh auth login");
2424
+ if (!prereq.workerAvailable) pendingItems.push(`${config.codingWorker} not found on PATH — install it before starting`);
2425
+ if (config.codingWorker === "openclaw" && !process.env.OPENROUTER_API_KEY) {
2426
+ pendingItems.push("OPENROUTER_API_KEY not set — required for openclaw workers");
2427
+ }
2428
+ if (anchorSync.status !== "ok") pendingItems.push(`Anchor repo sync deferred (${anchorSync.reason}) — fix git access and re-run setup`);
2429
+ if ((doctorKv.DOCTOR_STATUS || "") !== "ok") pendingItems.push(`Doctor check flagged issues — run: npx agent-control-plane@latest doctor`);
2430
+
2431
+ if (pendingItems.length > 0) {
2432
+ console.log("\n Pending items before starting:");
2433
+ for (const item of pendingItems) {
2434
+ console.log(` - ${item}`);
2435
+ }
2436
+ }
2437
+
2438
+ if (dashboardStatus === "ok" && dashboardUrl) {
2439
+ console.log(`\n Dashboard: ${dashboardUrl}`);
2440
+ }
2441
+
2442
+ console.log("\n Next commands:");
2443
+ if (runtimeStartStatus !== "ok") {
2444
+ console.log(` npx agent-control-plane@latest runtime start --profile-id ${config.profileId}`);
2445
+ }
2446
+ console.log(` npx agent-control-plane@latest runtime status --profile-id ${config.profileId}`);
2447
+ if (dashboardStatus !== "ok") {
2448
+ console.log(` npx agent-control-plane@latest dashboard`);
2449
+ }
2450
+ console.log(` npx agent-control-plane@latest doctor`);
2451
+
2452
+ if (starterIssues.created.length > 0) {
2453
+ console.log("\n Starter issues created (ACP will start working on these):");
2454
+ for (const issue of starterIssues.created) {
2455
+ console.log(` - ${issue.title}`);
2456
+ if (issue.url) {
2457
+ console.log(` ${issue.url}`);
2458
+ }
2459
+ }
2460
+ } else {
2461
+ console.log("\n Getting started:");
2462
+ console.log(` 1. Add the label 'agent-ready' to a GitHub issue in ${config.repoSlug}`);
2463
+ console.log(" 2. ACP picks it up automatically, assigns a worker, and opens a PR");
2464
+ console.log(" 3. Watch progress in the dashboard or with 'runtime status'");
2465
+ }
2466
+ if (config.codingWorker === "openclaw" || config.codingWorker === "pi") {
2467
+ console.log(`\n Tip: ${config.codingWorker} uses free-tier models by default.`);
2468
+ console.log(" No API costs until you switch to a paid model in the profile YAML.");
2469
+ }
2470
+ console.log("");
2050
2471
  } else {
2472
+ // Machine-readable KV output for non-interactive / scripted runs
2051
2473
  console.log("\nSetup complete.");
2052
2474
  console.log(`- profile: ${config.profileId}`);
2053
2475
  console.log(`- repo: ${config.repoSlug}`);
2054
2476
  console.log(`- runtime home: ${context.runtimeHome}`);
2055
- console.log(`- next status command: npx agent-control-plane@latest runtime status --profile-id ${config.profileId}`);
2056
2477
 
2057
2478
  console.log(`SETUP_STATUS=ok`);
2058
2479
  console.log(`PROFILE_ID=${config.profileId}`);
@@ -2170,6 +2591,7 @@ async function main() {
2170
2591
  console.log(packageJson.version);
2171
2592
  return 0;
2172
2593
  case "setup":
2594
+ case "onboard":
2173
2595
  return runSetupFlow(forwardedArgs);
2174
2596
  case "sync":
2175
2597
  case "install":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-control-plane",
3
- "version": "0.1.14",
3
+ "version": "0.2.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": {
@@ -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`,