feat: add AssignAgent, ManagePullRequest, ReportToIssue actions

AssignAgent activates a plan and starts an agent session.
ManagePullRequest evaluates PR state/CI checks and merges when ready.
ReportToIssue posts progress comments on Forgejo issues.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-04 14:40:17 +00:00
parent 08d397fbf6
commit 6ac515d80e
5 changed files with 252 additions and 0 deletions

View file

@ -0,0 +1,40 @@
<?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\Actions\Forge;
use Core\Actions\Action;
use Core\Mod\Agentic\Actions\Session\StartSession;
use Core\Mod\Agentic\Models\AgentPlan;
use Core\Mod\Agentic\Models\AgentSession;
/**
* Assign an agent to a plan and start a session.
*
* Activates the plan if it is still in draft status, then
* delegates to StartSession to create the working session.
*
* Usage:
* $session = AssignAgent::run($plan, 'opus', $workspaceId);
*/
class AssignAgent
{
use Action;
public function handle(AgentPlan $plan, string $agentType, int $workspaceId): AgentSession
{
if ($plan->status !== AgentPlan::STATUS_ACTIVE) {
$plan->activate();
}
return StartSession::run($agentType, $plan->slug, $workspaceId);
}
}

View file

@ -0,0 +1,59 @@
<?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\Actions\Forge;
use Core\Actions\Action;
use Core\Mod\Agentic\Services\ForgejoService;
/**
* Evaluate and merge a Forgejo pull request when ready.
*
* Checks the PR state, mergeability, and CI status before
* attempting the merge. Returns a result array describing
* the outcome.
*
* Usage:
* $result = ManagePullRequest::run('core', 'app', 10);
*/
class ManagePullRequest
{
use Action;
/**
* @return array{merged: bool, pr_number?: int, reason?: string}
*/
public function handle(string $owner, string $repo, int $prNumber): array
{
$forge = app(ForgejoService::class);
$pr = $forge->getPullRequest($owner, $repo, $prNumber);
if (($pr['state'] ?? '') !== 'open') {
return ['merged' => false, 'reason' => 'not_open'];
}
if (empty($pr['mergeable'])) {
return ['merged' => false, 'reason' => 'conflicts'];
}
$headSha = $pr['head']['sha'] ?? '';
$status = $forge->getCombinedStatus($owner, $repo, $headSha);
if (($status['state'] ?? '') !== 'success') {
return ['merged' => false, 'reason' => 'checks_pending'];
}
$forge->mergePullRequest($owner, $repo, $prNumber);
return ['merged' => true, 'pr_number' => $prNumber];
}
}

View file

@ -0,0 +1,34 @@
<?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\Actions\Forge;
use Core\Actions\Action;
use Core\Mod\Agentic\Services\ForgejoService;
/**
* Post a progress comment on a Forgejo issue.
*
* Wraps ForgejoService::createComment() for use as a
* standalone action within the orchestration pipeline.
*
* Usage:
* ReportToIssue::run('core', 'app', 5, 'Phase 1 complete.');
*/
class ReportToIssue
{
use Action;
public function handle(string $owner, string $repo, int $issueNumber, string $message): void
{
app(ForgejoService::class)->createComment($owner, $repo, $issueNumber, $message);
}
}

View file

@ -1,5 +1,12 @@
<?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\Services;

View file

@ -0,0 +1,112 @@
<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
use Core\Mod\Agentic\Actions\Forge\AssignAgent;
use Core\Mod\Agentic\Actions\Forge\ManagePullRequest;
use Core\Mod\Agentic\Actions\Forge\ReportToIssue;
use Core\Mod\Agentic\Models\AgentPlan;
use Core\Mod\Agentic\Models\AgentSession;
use Core\Mod\Agentic\Services\ForgejoService;
use Core\Tenant\Models\Workspace;
use Illuminate\Support\Facades\Http;
beforeEach(function () {
$this->workspace = Workspace::factory()->create();
$this->service = new ForgejoService(
baseUrl: 'https://forge.example.com',
token: 'test-token-abc',
);
$this->app->instance(ForgejoService::class, $this->service);
});
it('assigns an agent to a plan and starts a session', function () {
$plan = AgentPlan::factory()->create([
'workspace_id' => $this->workspace->id,
'status' => AgentPlan::STATUS_DRAFT,
]);
$session = AssignAgent::run($plan, 'opus', $this->workspace->id);
expect($session)->toBeInstanceOf(AgentSession::class);
expect($session->agent_type)->toBe('opus');
expect($session->agent_plan_id)->toBe($plan->id);
// Plan should be activated
$plan->refresh();
expect($plan->status)->toBe(AgentPlan::STATUS_ACTIVE);
});
it('reports progress to a Forgejo issue', function () {
Http::fake([
'forge.example.com/api/v1/repos/core/app/issues/5/comments' => Http::response([
'id' => 1,
'body' => 'Progress update: phase 1 complete.',
]),
]);
ReportToIssue::run('core', 'app', 5, 'Progress update: phase 1 complete.');
Http::assertSent(function ($request) {
return str_contains($request->url(), '/repos/core/app/issues/5/comments')
&& $request['body'] === 'Progress update: phase 1 complete.';
});
});
it('merges a PR when checks pass', function () {
Http::fake([
// Get PR — open and mergeable
'forge.example.com/api/v1/repos/core/app/pulls/10' => Http::response([
'number' => 10,
'state' => 'open',
'mergeable' => true,
'head' => ['sha' => 'abc123'],
]),
// Combined status — success
'forge.example.com/api/v1/repos/core/app/commits/abc123/status' => Http::response([
'state' => 'success',
]),
// Merge
'forge.example.com/api/v1/repos/core/app/pulls/10/merge' => Http::response([], 200),
]);
$result = ManagePullRequest::run('core', 'app', 10);
expect($result)->toMatchArray([
'merged' => true,
'pr_number' => 10,
]);
});
it('does not merge PR when checks are pending', function () {
Http::fake([
'forge.example.com/api/v1/repos/core/app/pulls/10' => Http::response([
'number' => 10,
'state' => 'open',
'mergeable' => true,
'head' => ['sha' => 'abc123'],
]),
'forge.example.com/api/v1/repos/core/app/commits/abc123/status' => Http::response([
'state' => 'pending',
]),
]);
$result = ManagePullRequest::run('core', 'app', 10);
expect($result)->toMatchArray([
'merged' => false,
'reason' => 'checks_pending',
]);
});