agent-control-plane 0.1.16 → 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 (47) 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 +322 -9
  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 +24 -12
  16. package/tools/bin/agent-project-run-claude-session +2 -2
  17. package/tools/bin/agent-project-run-kilo-session +346 -14
  18. package/tools/bin/agent-project-run-ollama-session +658 -0
  19. package/tools/bin/agent-project-run-openclaw-session +27 -25
  20. package/tools/bin/agent-project-run-opencode-session +354 -14
  21. package/tools/bin/agent-project-run-pi-session +479 -0
  22. package/tools/bin/agent-project-worker-status +1 -1
  23. package/tools/bin/flow-config-lib.sh +116 -3
  24. package/tools/bin/flow-resident-worker-lib.sh +1 -1
  25. package/tools/bin/flow-shell-lib.sh +5 -2
  26. package/tools/bin/heartbeat-recovery-preflight.sh +1 -0
  27. package/tools/bin/heartbeat-safe-auto.sh +105 -17
  28. package/tools/bin/install-project-launchd.sh +19 -2
  29. package/tools/bin/prepare-worktree.sh +4 -4
  30. package/tools/bin/profile-activate.sh +2 -2
  31. package/tools/bin/profile-adopt.sh +2 -2
  32. package/tools/bin/project-init.sh +1 -1
  33. package/tools/bin/project-runtimectl.sh +90 -7
  34. package/tools/bin/provider-cooldown-state.sh +14 -14
  35. package/tools/bin/render-flow-config.sh +30 -33
  36. package/tools/bin/run-codex-task.sh +53 -4
  37. package/tools/bin/scaffold-profile.sh +18 -3
  38. package/tools/bin/start-issue-worker.sh +1 -1
  39. package/tools/bin/start-pr-fix-worker.sh +30 -0
  40. package/tools/bin/start-pr-review-worker.sh +31 -0
  41. package/tools/bin/start-resident-issue-loop.sh +4 -4
  42. package/tools/bin/sync-agent-repo.sh +2 -2
  43. package/tools/bin/sync-dependency-baseline.sh +3 -3
  44. package/tools/bin/sync-shared-agent-home.sh +4 -1
  45. package/tools/templates/pr-fix-template.md +3 -7
  46. package/tools/templates/pr-merge-repair-template.md +3 -7
  47. package/tools/templates/pr-review-template.md +2 -1
@@ -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);
@@ -590,6 +786,9 @@ function detectPackageManager() {
590
786
  if (commandExists("pacman")) {
591
787
  return { name: "pacman" };
592
788
  }
789
+ if (commandExists("zypper")) {
790
+ return { name: "zypper" };
791
+ }
593
792
  return null;
594
793
  }
595
794
 
@@ -680,6 +879,9 @@ function buildDependencyInstallPlan(missingTools) {
680
879
  case "pacman":
681
880
  commands.push([...prefix, "pacman", "-Sy", "--noconfirm", ...packages]);
682
881
  break;
882
+ case "zypper":
883
+ commands.push([...prefix, "zypper", "install", "-y", ...packages]);
884
+ break;
683
885
  default:
684
886
  return null;
685
887
  }
@@ -721,7 +923,7 @@ async function maybeInstallMissingDependencies(options, prereq) {
721
923
  if (!plan) {
722
924
  console.log("\nACP found missing core dependencies but cannot install them automatically on this machine.");
723
925
  console.log(`- missing tools: ${prereq.missingRequired.join(", ")}`);
724
- 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");
725
927
  return {
726
928
  status: "unavailable",
727
929
  reason: "no-supported-package-manager",
@@ -1355,6 +1557,9 @@ function printSetupDryRunPlan(context, config, plan) {
1355
1557
  console.log(` command preview: ${plan.workerInstallPlan.commands.map(formatCommand).join(" && ")}`);
1356
1558
  }
1357
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
+ }
1358
1563
  console.log(`- runtime start: ${plan.runtimeStartAction.status}${plan.runtimeStartAction.reason ? ` (${plan.runtimeStartAction.reason})` : ""}`);
1359
1564
  if (process.platform === "darwin") {
1360
1565
  console.log(`- launchd install: ${plan.launchdAction.status}${plan.launchdAction.reason ? ` (${plan.launchdAction.reason})` : ""}`);
@@ -1641,8 +1846,8 @@ async function collectSetupConfig(options, context) {
1641
1846
  profileId = sanitizeProfileId(await promptText(rl, "Profile id", profileId));
1642
1847
 
1643
1848
  let workerInput = codingWorker;
1644
- while (!["codex", "claude", "openclaw"].includes(workerInput)) {
1645
- workerInput = await promptText(rl, "Coding worker (codex / claude / openclaw)", codingWorker || "openclaw");
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");
1646
1851
  }
1647
1852
  codingWorker = workerInput;
1648
1853
  } finally {
@@ -1650,7 +1855,7 @@ async function collectSetupConfig(options, context) {
1650
1855
  }
1651
1856
  }
1652
1857
 
1653
- if (!["codex", "claude", "openclaw"].includes(codingWorker)) {
1858
+ if (!["codex", "claude", "openclaw", "ollama", "pi", "opencode", "kilo"].includes(codingWorker)) {
1654
1859
  throw new Error(`unsupported coding worker: ${codingWorker}`);
1655
1860
  }
1656
1861
 
@@ -1687,6 +1892,9 @@ async function collectSetupConfig(options, context) {
1687
1892
  if (process.platform === "darwin" && options.installLaunchd === null && options.startRuntime) {
1688
1893
  options.installLaunchd = await promptYesNo(rl, "Install macOS autostart for this profile", false);
1689
1894
  }
1895
+ if (options.startDashboard === null) {
1896
+ options.startDashboard = await promptYesNo(rl, "Start the monitoring dashboard in background", true);
1897
+ }
1690
1898
  } finally {
1691
1899
  rl.close();
1692
1900
  }
@@ -1698,11 +1906,16 @@ async function collectSetupConfig(options, context) {
1698
1906
  if (options.installLaunchd === null) {
1699
1907
  options.installLaunchd = false;
1700
1908
  }
1909
+ if (options.startDashboard === null) {
1910
+ options.startDashboard = false;
1911
+ }
1701
1912
 
1702
1913
  return {
1703
1914
  ...config,
1704
1915
  startRuntime: Boolean(options.startRuntime),
1705
- installLaunchd: Boolean(options.installLaunchd)
1916
+ installLaunchd: Boolean(options.installLaunchd),
1917
+ startDashboard: Boolean(options.startDashboard),
1918
+ dashboardPort: options.dashboardPort
1706
1919
  };
1707
1920
  }
1708
1921
 
@@ -1932,9 +2145,10 @@ async function runSetupFlow(forwardedArgs) {
1932
2145
  let workerSetupStep = await maybeShowWorkerSetupGuide(options, prereq);
1933
2146
  prereq = collectPrereqStatus(config.codingWorker);
1934
2147
 
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).");
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).`);
1938
2152
  console.log("- Get a free key at: https://openrouter.ai/keys");
1939
2153
  if (options.interactive) {
1940
2154
  const rl = createPromptInterface();
@@ -1957,6 +2171,29 @@ async function runSetupFlow(forwardedArgs) {
1957
2171
  }
1958
2172
  }
1959
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
+
1960
2197
  if (options.interactive) {
1961
2198
  printWizardStep(4, 4, "Install");
1962
2199
  }
@@ -2048,6 +2285,56 @@ async function runSetupFlow(forwardedArgs) {
2048
2285
  }
2049
2286
  }
2050
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
+
2051
2338
  const finalFixup = await maybeRunFinalSetupFixups(options, scopedContext, config, {
2052
2339
  anchorSync,
2053
2340
  prereq,
@@ -2148,12 +2435,38 @@ async function runSetupFlow(forwardedArgs) {
2148
2435
  }
2149
2436
  }
2150
2437
 
2438
+ if (dashboardStatus === "ok" && dashboardUrl) {
2439
+ console.log(`\n Dashboard: ${dashboardUrl}`);
2440
+ }
2441
+
2151
2442
  console.log("\n Next commands:");
2152
2443
  if (runtimeStartStatus !== "ok") {
2153
2444
  console.log(` npx agent-control-plane@latest runtime start --profile-id ${config.profileId}`);
2154
2445
  }
2155
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
+ }
2156
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
+ }
2157
2470
  console.log("");
2158
2471
  } else {
2159
2472
  // 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.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`,
@@ -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="$(
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ Usage:
7
+ agent-project-catch-up-issue-pr-links --repo-slug <owner/repo> --state-root <path> --hook-file <path> [--limit <n>]
8
+
9
+ Clear stale issue retry state when an issue already has a linked PR comment and
10
+ that PR still exists (open, closed, or merged).
11
+ EOF
12
+ }
13
+
14
+ repo_slug=""
15
+ state_root=""
16
+ hook_file=""
17
+ limit="100"
18
+
19
+ while [[ $# -gt 0 ]]; do
20
+ case "$1" in
21
+ --repo-slug) repo_slug="${2:-}"; shift 2 ;;
22
+ --state-root) state_root="${2:-}"; shift 2 ;;
23
+ --hook-file) hook_file="${2:-}"; shift 2 ;;
24
+ --limit) limit="${2:-}"; shift 2 ;;
25
+ --help|-h) usage; exit 0 ;;
26
+ *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
27
+ esac
28
+ done
29
+
30
+ if [[ -z "$repo_slug" || -z "$state_root" || -z "$hook_file" ]]; then
31
+ usage >&2
32
+ exit 1
33
+ fi
34
+
35
+ if [[ ! -f "$hook_file" ]]; then
36
+ echo "missing hook file: $hook_file" >&2
37
+ exit 1
38
+ fi
39
+
40
+ # shellcheck source=/dev/null
41
+ source "$hook_file"
42
+
43
+ if ! declare -F issue_clear_retry >/dev/null 2>&1; then
44
+ issue_clear_retry() { :; }
45
+ fi
46
+
47
+ ledger_dir="${state_root}/linked-pr-issue-catchup"
48
+ retry_dir="${state_root}/retries/issues"
49
+ mkdir -p "$ledger_dir" "$retry_dir"
50
+
51
+ extract_latest_linked_pr() {
52
+ local issue_json="${1:-}"
53
+ ISSUE_JSON="$issue_json" python3 - <<'PY'
54
+ import json
55
+ import os
56
+ import re
57
+
58
+ issue = json.loads(os.environ.get("ISSUE_JSON", "{}") or "{}")
59
+ latest = ""
60
+ latest_at = ""
61
+ for comment in issue.get("comments", []) or []:
62
+ body = comment.get("body") or ""
63
+ match = None
64
+ for candidate in re.finditer(r"Opened PR #(\d+)", body):
65
+ match = candidate
66
+ if not match:
67
+ continue
68
+ created = comment.get("createdAt") or ""
69
+ pr_number = match.group(1)
70
+ if created >= latest_at:
71
+ latest_at = created
72
+ latest = pr_number
73
+
74
+ print(latest)
75
+ PY
76
+ }
77
+
78
+ pr_exists() {
79
+ local pr_number="${1:?pr number required}"
80
+ local pr_json=""
81
+ pr_json="$(flow_github_pr_view_json "$repo_slug" "$pr_number" 2>/dev/null || true)"
82
+ [[ -n "$pr_json" && "$pr_json" != "{}" ]]
83
+ }
84
+
85
+ for retry_file in "$retry_dir"/*.env; do
86
+ [[ -f "$retry_file" ]] || continue
87
+ issue_id="$(basename "${retry_file%.env}")"
88
+ [[ -n "$issue_id" ]] || continue
89
+
90
+ retry_reason="$(awk -F= '/^LAST_REASON=/{print $2; exit}' "$retry_file" 2>/dev/null | tr -d '\r' || true)"
91
+ if [[ "$retry_reason" != "host-publish-failed" ]]; then
92
+ continue
93
+ fi
94
+
95
+ ledger_file="${ledger_dir}/${issue_id}.env"
96
+ if [[ -f "$ledger_file" ]]; then
97
+ continue
98
+ fi
99
+
100
+ issue_json="$(flow_github_issue_view_json "$repo_slug" "$issue_id" 2>/dev/null || true)"
101
+ [[ -n "$issue_json" && "$issue_json" != "{}" ]] || continue
102
+
103
+ linked_pr="$(extract_latest_linked_pr "$issue_json")"
104
+ [[ -n "$linked_pr" ]] || continue
105
+ if ! pr_exists "$linked_pr"; then
106
+ continue
107
+ fi
108
+
109
+ ISSUE_ID="$issue_id" issue_clear_retry || true
110
+
111
+ {
112
+ printf 'ISSUE_ID=%s\n' "$issue_id"
113
+ printf 'LINKED_PR=%s\n' "$linked_pr"
114
+ printf 'PROCESSED_AT=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
115
+ } >"$ledger_file"
116
+
117
+ printf 'CATCHUP_LINKED_PR_ISSUE=%s\n' "$issue_id"
118
+ done