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.
- package/README.md +323 -349
- package/bin/pr-risk.sh +28 -6
- package/hooks/heartbeat-hooks.sh +62 -22
- package/npm/bin/agent-control-plane.js +434 -12
- package/package.json +1 -1
- package/references/architecture.md +8 -0
- package/references/control-plane-map.md +6 -2
- package/references/release-checklist.md +0 -2
- package/tools/bin/agent-github-update-labels +6 -1
- package/tools/bin/agent-project-catch-up-issue-pr-links +118 -0
- package/tools/bin/agent-project-catch-up-merged-prs +77 -21
- package/tools/bin/agent-project-catch-up-scheduled-issue-retries +123 -0
- package/tools/bin/agent-project-cleanup-session +84 -0
- package/tools/bin/agent-project-heartbeat-loop +10 -3
- package/tools/bin/agent-project-reconcile-issue-session +45 -12
- package/tools/bin/agent-project-reconcile-pr-session +25 -0
- package/tools/bin/agent-project-run-claude-session +2 -2
- package/tools/bin/agent-project-run-codex-resilient +57 -2
- package/tools/bin/agent-project-run-kilo-session +346 -14
- package/tools/bin/agent-project-run-ollama-session +658 -0
- package/tools/bin/agent-project-run-openclaw-session +73 -25
- package/tools/bin/agent-project-run-opencode-session +354 -14
- package/tools/bin/agent-project-run-pi-session +479 -0
- package/tools/bin/agent-project-worker-status +38 -1
- package/tools/bin/flow-config-lib.sh +123 -3
- package/tools/bin/flow-resident-worker-lib.sh +1 -1
- package/tools/bin/flow-shell-lib.sh +7 -2
- package/tools/bin/heartbeat-recovery-preflight.sh +1 -0
- package/tools/bin/heartbeat-safe-auto.sh +105 -17
- package/tools/bin/install-project-launchd.sh +19 -2
- package/tools/bin/prepare-worktree.sh +4 -4
- package/tools/bin/profile-activate.sh +2 -2
- package/tools/bin/profile-adopt.sh +2 -2
- package/tools/bin/project-init.sh +1 -1
- package/tools/bin/project-runtimectl.sh +90 -7
- package/tools/bin/provider-cooldown-state.sh +14 -14
- package/tools/bin/render-flow-config.sh +30 -33
- package/tools/bin/run-codex-task.sh +53 -4
- package/tools/bin/scaffold-profile.sh +18 -3
- package/tools/bin/start-issue-worker.sh +4 -1
- package/tools/bin/start-pr-fix-worker.sh +33 -0
- package/tools/bin/start-pr-review-worker.sh +34 -0
- package/tools/bin/start-resident-issue-loop.sh +5 -4
- package/tools/bin/sync-agent-repo.sh +2 -2
- package/tools/bin/sync-dependency-baseline.sh +3 -3
- package/tools/bin/sync-shared-agent-home.sh +4 -1
- package/tools/dashboard/app.js +62 -0
- package/tools/dashboard/dashboard_snapshot.py +53 -4
- package/tools/dashboard/index.html +5 -1
- package/tools/dashboard/styles.css +97 -20
- package/tools/templates/pr-fix-template.md +4 -8
- package/tools/templates/pr-merge-repair-template.md +4 -8
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1611
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
44
|
+
Launches Crush (formerly OpenCode) worker sessions via `crush run`.
|
|
41
45
|
- `tools/bin/agent-project-run-kilo-session`
|
|
42
|
-
|
|
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`,
|