agent/php/Services/MantisClient.php
Snider 48b2c2fe58 feat(agent/php): artisan agentic:sync-profiles + agentic:dispatch-queue (#829)
agentic:sync-profiles iterates AgentProfile rows, calls GET {gateway}/v1/models
via Http::withToken, infers capability_tags from exposed model ids
(claude-opus → handoff/analysis/core; gpt-5.4-mini → dispatch/cheap;
embedding-* → embedding), leaves last_dispatched_at untouched.

agentic:dispatch-queue uses extended MantisClient->listOpen() (new
small wrapper), skips assigned tickets + 5min in-flight markers, runs
ProfileSelector::pickFor, adds suppression note via MantisClient->note,
queues DispatchMantisTicketJob up to --limit (default 3). Both
commands emit progress via Log.

Pest Feature tests use Http::fake + Queue::fake. Tests register the
new commands directly (Boot.php registration is a deferred follow-up
per #837 lane note).

Codex note: php -l clean; pest blocked by unrelated repo migration
infra (dedicated brain connection + SQLite-incompatible agent_sessions
rename).

Closes tasks.lthn.sh/view.php?id=829

Co-authored-by: Codex <noreply@openai.com>
2026-04-26 01:08:42 +01:00

87 lines
2.2 KiB
PHP

<?php
// SPDX-License-Identifier: EUPL-1.2
declare(strict_types=1);
namespace Core\Mod\Agentic\Services;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class MantisClient
{
public function __construct(
private ?string $baseUrl = null,
private ?string $token = null,
) {}
/**
* @return list<array<string, mixed>>
*/
public function listOpen(): array
{
$response = $this->request()->get('/api/rest/issues', [
'status' => 'new',
]);
if (! $response->successful()) {
throw new RuntimeException("Mantis listOpen failed: {$response->status()}");
}
$issues = $response->json('issues');
return is_array($issues) ? array_values(array_filter($issues, 'is_array')) : [];
}
public function note(int $ticketId, string $text): void
{
$response = $this->request()->post("/api/rest/issues/{$ticketId}/notes", [
'text' => $text,
]);
if (! $response->successful()) {
throw new RuntimeException("Mantis note failed: {$response->status()}");
}
}
public function close(int $ticketId, string $resolution = 'fixed'): void
{
$response = $this->request()->patch("/api/rest/issues/{$ticketId}", [
'status' => [
'name' => 'closed',
],
'resolution' => [
'name' => $resolution,
],
]);
if (! $response->successful()) {
throw new RuntimeException("Mantis close failed: {$response->status()}");
}
}
private function request(): PendingRequest
{
return Http::acceptJson()
->baseUrl($this->resolveBaseUrl())
->withHeaders([
'Authorization' => $this->resolveToken(),
])
->timeout(15);
}
private function resolveBaseUrl(): string
{
return rtrim(
$this->baseUrl ?? (string) config('agentic.mantis.base_url', 'https://tasks.lthn.sh'),
'/',
);
}
private function resolveToken(): string
{
return $this->token ?? (string) config('agentic.mantis.token');
}
}