feat: add agentic:scan, agentic:dispatch, agentic:pr-manage commands
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
6ac515d80e
commit
b32d339a53
5 changed files with 308 additions and 3 deletions
23
Boot.php
23
Boot.php
|
|
@ -35,17 +35,24 @@ class Boot extends ServiceProvider
|
|||
$this->loadMigrationsFrom(__DIR__.'/Migrations');
|
||||
$this->loadTranslationsFrom(__DIR__.'/Lang', 'agentic');
|
||||
$this->configureRateLimiting();
|
||||
$this->scheduleRetentionCleanup();
|
||||
$this->scheduleCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the daily retention cleanup schedule.
|
||||
* Register all scheduled commands.
|
||||
*/
|
||||
protected function scheduleRetentionCleanup(): void
|
||||
protected function scheduleCommands(): void
|
||||
{
|
||||
$this->app->booted(function (): void {
|
||||
$schedule = $this->app->make(Schedule::class);
|
||||
$schedule->command('agentic:plan-cleanup')->daily();
|
||||
|
||||
// Forgejo pipeline — only active when a token is configured
|
||||
if (config('agentic.forge_token') !== '') {
|
||||
$schedule->command('agentic:scan')->everyFiveMinutes();
|
||||
$schedule->command('agentic:dispatch')->everyTwoMinutes();
|
||||
$schedule->command('agentic:pr-manage')->everyFiveMinutes();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -84,6 +91,13 @@ class Boot extends ServiceProvider
|
|||
$this->app->singleton(\Core\Mod\Agentic\Services\AgenticManager::class);
|
||||
$this->app->singleton(\Core\Mod\Agentic\Services\AgentToolRegistry::class);
|
||||
|
||||
$this->app->singleton(\Core\Mod\Agentic\Services\ForgejoService::class, function ($app) {
|
||||
return new \Core\Mod\Agentic\Services\ForgejoService(
|
||||
baseUrl: (string) config('agentic.forge_url', 'https://forge.lthn.ai'),
|
||||
token: (string) config('agentic.forge_token', ''),
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->singleton(\Core\Mod\Agentic\Services\BrainService::class, function ($app) {
|
||||
$ollamaUrl = config('mcp.brain.ollama_url', 'http://localhost:11434');
|
||||
$qdrantUrl = config('mcp.brain.qdrant_url', 'http://localhost:6334');
|
||||
|
|
@ -155,6 +169,9 @@ class Boot extends ServiceProvider
|
|||
$event->command(Console\Commands\PlanRetentionCommand::class);
|
||||
$event->command(Console\Commands\BrainSeedMemoryCommand::class);
|
||||
$event->command(Console\Commands\BrainIngestCommand::class);
|
||||
$event->command(Console\Commands\ScanCommand::class);
|
||||
$event->command(Console\Commands\DispatchCommand::class);
|
||||
$event->command(Console\Commands\PrManageCommand::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
80
Console/Commands/DispatchCommand.php
Normal file
80
Console/Commands/DispatchCommand.php
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Agentic\Console\Commands;
|
||||
|
||||
use Core\Mod\Agentic\Actions\Forge\AssignAgent;
|
||||
use Core\Mod\Agentic\Actions\Forge\ReportToIssue;
|
||||
use Core\Mod\Agentic\Models\AgentPlan;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class DispatchCommand extends Command
|
||||
{
|
||||
protected $signature = 'agentic:dispatch
|
||||
{--workspace=1 : Workspace ID}
|
||||
{--agent-type=opus : Default agent type}
|
||||
{--dry-run : Show what would be dispatched}';
|
||||
|
||||
protected $description = 'Dispatch agents to draft plans sourced from Forgejo';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$workspaceId = (int) $this->option('workspace');
|
||||
$defaultAgentType = (string) $this->option('agent-type');
|
||||
$isDryRun = (bool) $this->option('dry-run');
|
||||
|
||||
$plans = AgentPlan::where('status', AgentPlan::STATUS_DRAFT)
|
||||
->whereJsonContains('metadata->source', 'forgejo')
|
||||
->whereDoesntHave('sessions')
|
||||
->get();
|
||||
|
||||
if ($plans->isEmpty()) {
|
||||
$this->info('No draft Forgejo plans awaiting dispatch.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$dispatched = 0;
|
||||
|
||||
foreach ($plans as $plan) {
|
||||
$assignee = $plan->metadata['assignee'] ?? $defaultAgentType;
|
||||
$issueNumber = $plan->metadata['issue_number'] ?? null;
|
||||
$owner = $plan->metadata['repo_owner'] ?? null;
|
||||
$repo = $plan->metadata['repo_name'] ?? null;
|
||||
|
||||
if ($isDryRun) {
|
||||
$this->line("DRY RUN: Would dispatch '{$assignee}' to plan #{$plan->id} — {$plan->title}");
|
||||
$dispatched++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$session = AssignAgent::run($plan, $assignee, $workspaceId);
|
||||
|
||||
if ($issueNumber !== null && $owner !== null && $repo !== null) {
|
||||
ReportToIssue::run(
|
||||
(string) $owner,
|
||||
(string) $repo,
|
||||
(int) $issueNumber,
|
||||
"Agent **{$assignee}** dispatched. Session: #{$session->id}"
|
||||
);
|
||||
}
|
||||
|
||||
$this->line("Dispatched '{$assignee}' to plan #{$plan->id}: {$plan->title} (session #{$session->id})");
|
||||
$dispatched++;
|
||||
}
|
||||
|
||||
$action = $isDryRun ? 'would be dispatched' : 'dispatched';
|
||||
$this->info("{$dispatched} plan(s) {$action}.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
94
Console/Commands/PrManageCommand.php
Normal file
94
Console/Commands/PrManageCommand.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Agentic\Console\Commands;
|
||||
|
||||
use Core\Mod\Agentic\Actions\Forge\ManagePullRequest;
|
||||
use Core\Mod\Agentic\Services\ForgejoService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class PrManageCommand extends Command
|
||||
{
|
||||
protected $signature = 'agentic:pr-manage
|
||||
{--repos=* : Repos to manage (owner/name format)}
|
||||
{--dry-run : Show what would be merged}';
|
||||
|
||||
protected $description = 'Review and merge ready pull requests on Forgejo repositories';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$repos = $this->option('repos');
|
||||
|
||||
if (empty($repos)) {
|
||||
$repos = config('agentic.scan_repos', []);
|
||||
}
|
||||
|
||||
$repos = array_filter($repos);
|
||||
|
||||
if (empty($repos)) {
|
||||
$this->warn('No repositories configured. Pass --repos or set AGENTIC_SCAN_REPOS.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$isDryRun = (bool) $this->option('dry-run');
|
||||
$forge = app(ForgejoService::class);
|
||||
$totalProcessed = 0;
|
||||
|
||||
foreach ($repos as $repoSpec) {
|
||||
$parts = explode('/', $repoSpec, 2);
|
||||
|
||||
if (count($parts) !== 2) {
|
||||
$this->error("Invalid repo format: {$repoSpec} (expected owner/name)");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
[$owner, $repo] = $parts;
|
||||
|
||||
$this->info("Checking PRs for {$owner}/{$repo}...");
|
||||
|
||||
$pullRequests = $forge->listPullRequests($owner, $repo, 'open');
|
||||
|
||||
if (empty($pullRequests)) {
|
||||
$this->line(" No open PRs.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($pullRequests as $pr) {
|
||||
$prNumber = (int) $pr['number'];
|
||||
$prTitle = (string) ($pr['title'] ?? '');
|
||||
$totalProcessed++;
|
||||
|
||||
if ($isDryRun) {
|
||||
$this->line(" DRY RUN: Would evaluate PR #{$prNumber} — {$prTitle}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = ManagePullRequest::run($owner, $repo, $prNumber);
|
||||
|
||||
if ($result['merged']) {
|
||||
$this->line(" Merged PR #{$prNumber}: {$prTitle}");
|
||||
} else {
|
||||
$reason = $result['reason'] ?? 'unknown';
|
||||
$this->line(" Skipped PR #{$prNumber}: {$prTitle} ({$reason})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$action = $isDryRun ? 'found' : 'processed';
|
||||
$this->info("PR management complete: {$totalProcessed} PR(s) {$action}.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
98
Console/Commands/ScanCommand.php
Normal file
98
Console/Commands/ScanCommand.php
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Agentic\Console\Commands;
|
||||
|
||||
use Core\Mod\Agentic\Actions\Forge\CreatePlanFromIssue;
|
||||
use Core\Mod\Agentic\Actions\Forge\ReportToIssue;
|
||||
use Core\Mod\Agentic\Actions\Forge\ScanForWork;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ScanCommand extends Command
|
||||
{
|
||||
protected $signature = 'agentic:scan
|
||||
{--workspace=1 : Workspace ID}
|
||||
{--repos=* : Repos to scan (owner/name format)}
|
||||
{--dry-run : Show what would be created without acting}';
|
||||
|
||||
protected $description = 'Scan Forgejo repositories for actionable work from epic issues';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$workspaceId = (int) $this->option('workspace');
|
||||
$repos = $this->option('repos');
|
||||
|
||||
if (empty($repos)) {
|
||||
$repos = config('agentic.scan_repos', []);
|
||||
}
|
||||
|
||||
$repos = array_filter($repos);
|
||||
|
||||
if (empty($repos)) {
|
||||
$this->warn('No repositories configured. Pass --repos or set AGENTIC_SCAN_REPOS.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$isDryRun = (bool) $this->option('dry-run');
|
||||
$totalItems = 0;
|
||||
|
||||
foreach ($repos as $repoSpec) {
|
||||
$parts = explode('/', $repoSpec, 2);
|
||||
|
||||
if (count($parts) !== 2) {
|
||||
$this->error("Invalid repo format: {$repoSpec} (expected owner/name)");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
[$owner, $repo] = $parts;
|
||||
|
||||
$this->info("Scanning {$owner}/{$repo}...");
|
||||
|
||||
$workItems = ScanForWork::run($owner, $repo);
|
||||
|
||||
if (empty($workItems)) {
|
||||
$this->line(" No actionable work found.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($workItems as $item) {
|
||||
$totalItems++;
|
||||
$issueNumber = $item['issue_number'];
|
||||
$title = $item['issue_title'];
|
||||
|
||||
if ($isDryRun) {
|
||||
$this->line(" DRY RUN: Would create plan for #{$issueNumber} — {$title}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$plan = CreatePlanFromIssue::run($item, $workspaceId);
|
||||
|
||||
ReportToIssue::run(
|
||||
$owner,
|
||||
$repo,
|
||||
$issueNumber,
|
||||
"Plan created: **{$plan->title}** (#{$plan->id})"
|
||||
);
|
||||
|
||||
$this->line(" Created plan #{$plan->id} for issue #{$issueNumber}: {$title}");
|
||||
}
|
||||
}
|
||||
|
||||
$action = $isDryRun ? 'found' : 'processed';
|
||||
$this->info("Scan complete: {$totalItems} work item(s) {$action}.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
16
agentic.php
16
agentic.php
|
|
@ -18,4 +18,20 @@ return [
|
|||
|
||||
'plan_retention_days' => env('AGENTIC_PLAN_RETENTION_DAYS', 90),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Forgejo Integration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for the Forgejo-based scan/dispatch/PR pipeline.
|
||||
| AGENTIC_SCAN_REPOS is a comma-separated list of owner/name repos.
|
||||
|
|
||||
*/
|
||||
|
||||
'scan_repos' => array_filter(explode(',', env('AGENTIC_SCAN_REPOS', ''))),
|
||||
|
||||
'forge_url' => env('FORGE_URL', 'https://forge.lthn.ai'),
|
||||
|
||||
'forge_token' => env('FORGE_TOKEN', ''),
|
||||
|
||||
];
|
||||
|
|
|
|||
Reference in a new issue