whereJsonContains('metadata->issue_number', $issueNumber) ->whereJsonContains('metadata->repo_owner', $owner) ->whereJsonContains('metadata->repo_name', $repo) ->first(); if ($existing !== null) { return $existing->load('agentPhases'); } $tasks = $this->extractTasks((string) $workItem['issue_body']); $plan = CreatePlan::run([ 'title' => (string) $workItem['issue_title'], 'slug' => "forge-{$owner}-{$repo}-{$issueNumber}", 'description' => (string) $workItem['issue_body'], 'phases' => [ [ 'name' => "Resolve issue #{$issueNumber}", 'description' => "Complete all tasks for issue #{$issueNumber}", 'tasks' => $tasks, ], ], ], $workspaceId); $plan->update([ 'metadata' => [ 'source' => 'forgejo', 'epic_number' => (int) $workItem['epic_number'], 'issue_number' => $issueNumber, 'repo_owner' => $owner, 'repo_name' => $repo, 'assignee' => $workItem['assignee'] ?? null, ], ]); return $plan->load('agentPhases'); } /** * Extract task names from markdown checklist items. * * Matches lines like `- [ ] Create picker UI` and returns * just the task name portion. * * @return array */ private function extractTasks(string $body): array { $tasks = []; if (preg_match_all('/- \[[ xX]\] (.+)/', $body, $matches)) { foreach ($matches[1] as $taskName) { $tasks[] = trim($taskName); } } return $tasks; } }