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.
- 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 +322 -9
- 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 +24 -12
- package/tools/bin/agent-project-run-claude-session +2 -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 +27 -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 +1 -1
- package/tools/bin/flow-config-lib.sh +116 -3
- package/tools/bin/flow-resident-worker-lib.sh +1 -1
- package/tools/bin/flow-shell-lib.sh +5 -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 +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 +4 -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/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
|
@@ -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
|
-
|
|
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.
|
|
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`,
|
|
@@ -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="$(
|
|
@@ -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
|