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