feat(automation): issue-to-deploy pipeline — Report Issue button, Claude watcher, tag-triggered deploy

- Report Issue button in portal header files a Forgejo issue (portal + claude-queue labels)
- Windows scheduled watcher runs headless Claude Code on queued issues and opens a PR
- .forgejo/workflows/deploy.yml deploys v* release tags via the pms1 host runner (pm2 restart ppms)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hardik 2026-06-11 16:39:43 +05:30
parent 4c6b9c670f
commit 8b6d4e8ea6
11 changed files with 594 additions and 0 deletions

View file

@ -0,0 +1,42 @@
name: Deploy release to production
# Pushing a release tag (vX.Y.Z) deploys that tag to the portal at
# pms.pelagiamarine.com. Runs on the pms1 host runner (label: host),
# which executes as shad0w with direct access to the pm2-managed app.
on:
push:
tags:
- "v*"
jobs:
deploy:
runs-on: host
steps:
- name: Deploy tag to ~/pms and restart ppms
run: |
set -euo pipefail
export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
TAG="${GITHUB_REF_NAME}"
echo "=== Deploying $TAG ==="
cd "$HOME/pms"
git fetch origin --tags --force
git checkout -f "refs/tags/$TAG"
cd App
pnpm install --frozen-lockfile
pnpm build # includes prisma generate
pnpm db:migrate:deploy
pm2 restart ppms --update-env
echo "=== Deployed $TAG ==="
- name: Verify portal responds
run: |
sleep 5
code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/login)
echo "Portal /login returned HTTP $code"
test "$code" = "200"

5
.gitignore vendored
View file

@ -27,6 +27,11 @@ App/coverage/
.codex/
.antigravity/
# Claude issue watcher (real token + logs stay local)
automation/watcher.config.json
automation/logs/
automation/.watcher.lock
# OS
.DS_Store
Thumbs.db

View file

@ -48,3 +48,9 @@ EMAIL_FROM_NAME="Pelagia Portal"
# Development default (localhost:3002) is used if this is unset.
# Start the service with: cd GstService && npm run dev
GST_SERVICE_URL=http://localhost:3003
# ── Forgejo issue reporting (Report Issue button) ─────────────
# Token needs write:issue scope on the repo below.
FORGEJO_URL=https://git.pelagiamarine.com
FORGEJO_REPO=shad0w/pelagia-portal
FORGEJO_TOKEN=

View file

@ -5,6 +5,7 @@ import { LogOut } from "lucide-react";
import type { Role } from "@prisma/client";
import { CartIcon } from "./cart-icon";
import { NotificationBell } from "./notification-bell";
import { ReportIssueButton } from "./report-issue-button";
const ROLE_LABELS: Record<Role, string> = {
TECHNICAL: "Technical",
@ -39,6 +40,7 @@ export function Header({ user, initialUnreadCount, initialNotifications }: Heade
<div />
<div className="flex items-center gap-2">
{CART_ROLES.includes(user.role) && <CartIcon />}
<ReportIssueButton />
<NotificationBell
initialUnreadCount={initialUnreadCount}
initialNotifications={initialNotifications}

View file

@ -0,0 +1,52 @@
"use server";
import { auth } from "@/auth";
import { createForgejoIssue } from "@/lib/forgejo";
import { z } from "zod";
const PRIORITIES = ["P0 — Critical (broken / blocking)", "P1 — High", "P2 — Medium", "P3 — Low"] as const;
const reportSchema = z.object({
title: z.string().min(5, "Please give the issue a short title (min 5 characters)").max(150),
description: z.string().min(10, "Please describe the issue (min 10 characters)").max(5000),
priority: z.enum(PRIORITIES),
page: z.string().max(500).optional(),
});
type Result = { ok: true; issueNumber: number; issueUrl: string } | { error: string };
export async function reportIssue(formData: FormData): Promise<Result> {
const session = await auth();
if (!session?.user) return { error: "Unauthorized" };
const parsed = reportSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { error: parsed.error.errors[0].message };
const { title, description, priority, page } = parsed.data;
const body = [
"### Raised by",
`${session.user.name} (${session.user.email}, ${session.user.role}) — via portal Report Issue button`,
"",
"### Description",
description,
"",
"### Priority",
priority,
"",
"### Context",
`- Page: \`${page || "unknown"}\``,
`- Reported at: ${new Date().toISOString()}`,
].join("\n");
try {
const issue = await createForgejoIssue({
title: `[Issue]: ${title}`,
body,
labels: ["portal", "claude-queue"],
});
return { ok: true, issueNumber: issue.number, issueUrl: issue.html_url };
} catch (err) {
console.error("reportIssue failed:", err);
return { error: "Could not file the issue. Please try again or contact the administrator." };
}
}

View file

@ -0,0 +1,130 @@
"use client";
import { useState, useTransition } from "react";
import { usePathname } from "next/navigation";
import { Bug } from "lucide-react";
import { AdminDialog } from "@/components/ui/admin-dialog";
import { reportIssue } from "./report-issue-actions";
const PRIORITIES = [
"P0 — Critical (broken / blocking)",
"P1 — High",
"P2 — Medium",
"P3 — Low",
] as const;
export function ReportIssueButton() {
const pathname = usePathname();
const [open, setOpen] = useState(false);
const [pending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const [filedIssue, setFiledIssue] = useState<{ number: number; url: string } | null>(null);
function close() {
setOpen(false);
setError(null);
setFiledIssue(null);
}
function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const formData = new FormData(e.currentTarget);
formData.set("page", pathname);
startTransition(async () => {
const result = await reportIssue(formData);
if ("error" in result) {
setError(result.error);
} else {
setFiledIssue({ number: result.issueNumber, url: result.issueUrl });
}
});
}
return (
<>
<button
onClick={() => setOpen(true)}
className="flex items-center gap-1.5 rounded-lg p-2 text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700 transition-colors"
title="Report an issue"
>
<Bug className="h-4 w-4" />
</button>
<AdminDialog title="Report an issue" open={open} onClose={close}>
{filedIssue ? (
<div className="space-y-4">
<p className="text-sm text-neutral-700">
Thanks issue <span className="font-semibold">#{filedIssue.number}</span> has been
filed and queued for an automated fix. You&apos;ll see the change in an upcoming
release.
</p>
<button
onClick={close}
className="w-full rounded-lg bg-neutral-900 px-4 py-2 text-sm font-medium text-white hover:bg-neutral-700 transition-colors"
>
Done
</button>
</div>
) : (
<form onSubmit={onSubmit} className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-neutral-700" htmlFor="ri-title">
Title
</label>
<input
id="ri-title"
name="title"
required
minLength={5}
maxLength={150}
placeholder="Short summary of the problem"
className="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-neutral-500 focus:outline-none"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-neutral-700" htmlFor="ri-description">
Description
</label>
<textarea
id="ri-description"
name="description"
required
minLength={10}
maxLength={5000}
rows={5}
placeholder="What happened? What did you expect? Steps to reproduce if it's a bug."
className="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-neutral-500 focus:outline-none"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-neutral-700" htmlFor="ri-priority">
Priority
</label>
<select
id="ri-priority"
name="priority"
defaultValue={PRIORITIES[2]}
className="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-neutral-500 focus:outline-none"
>
{PRIORITIES.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
<button
type="submit"
disabled={pending}
className="w-full rounded-lg bg-neutral-900 px-4 py-2 text-sm font-medium text-white hover:bg-neutral-700 transition-colors disabled:opacity-50"
>
{pending ? "Filing issue…" : "Report issue"}
</button>
</form>
)}
</AdminDialog>
</>
);
}

56
App/lib/forgejo.ts Normal file
View file

@ -0,0 +1,56 @@
// Forgejo issue integration for the in-app "Report Issue" button.
// Issues are created on the configured repo and labelled so the
// automated Claude watcher can pick them up.
const FORGEJO_URL = process.env.FORGEJO_URL ?? "https://git.pelagiamarine.com";
const FORGEJO_REPO = process.env.FORGEJO_REPO ?? "shad0w/pelagia-portal";
interface CreateIssueInput {
title: string;
body: string;
labels: string[];
}
interface ForgejoIssue {
number: number;
html_url: string;
}
async function forgejoFetch(path: string, init?: RequestInit): Promise<Response> {
const token = process.env.FORGEJO_TOKEN;
if (!token) throw new Error("FORGEJO_TOKEN is not configured");
return fetch(`${FORGEJO_URL}/api/v1${path}`, {
...init,
headers: {
Authorization: `token ${token}`,
"Content-Type": "application/json",
...init?.headers,
},
});
}
/** Resolve label names to IDs (the create-issue API only accepts IDs). */
async function resolveLabelIds(names: string[]): Promise<number[]> {
const res = await forgejoFetch(`/repos/${FORGEJO_REPO}/labels?limit=50`);
if (!res.ok) return [];
const labels: { id: number; name: string }[] = await res.json();
return labels.filter((l) => names.includes(l.name)).map((l) => l.id);
}
export async function createForgejoIssue(input: CreateIssueInput): Promise<ForgejoIssue> {
const labelIds = await resolveLabelIds(input.labels);
const res = await forgejoFetch(`/repos/${FORGEJO_REPO}/issues`, {
method: "POST",
body: JSON.stringify({ title: input.title, body: input.body, labels: labelIds }),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Forgejo issue creation failed (${res.status}): ${text.slice(0, 200)}`);
}
const issue = await res.json();
return { number: issue.number, html_url: issue.html_url };
}

69
automation/README.md Normal file
View file

@ -0,0 +1,69 @@
# Automated issue-to-deploy pipeline
End-to-end flow from a user clicking **Report Issue** in the portal to a fix
running in production:
```
Portal header (bug icon) [App/components/layout/report-issue-button.tsx]
│ server action → Forgejo API
Forgejo issue (labels: portal, claude-queue) [git.pelagiamarine.com/shad0w/pelagia-portal]
│ polled every 10 min by Windows Scheduled Task "PelagiaClaudeIssueWatcher"
claude-issue-watcher.ps1 (this folder) [dev PC, runs headless Claude Code]
│ Claude implements + verifies fix in C:\...\src\pelagia-autofix
│ watcher pushes branch claude/issue-N and opens a PR (label: claude-pr)
Human review: merge the PR, then create a release tag vX.Y.Z
│ tag push triggers .forgejo/workflows/deploy.yml
forgejo-runner on pms1 (pm2: forgejo-runner, label "host")
│ checks out the tag in ~/pms, pnpm install + build + prisma migrate deploy
pm2 restart ppms → live at pms.pelagiamarine.com
```
## Components
| Piece | Where | Notes |
|---|---|---|
| Report Issue button | `App/components/layout/report-issue-button.tsx` + `report-issue-actions.ts` | Any signed-in user; files issue with `portal` + `claude-queue` labels |
| Forgejo helper | `App/lib/forgejo.ts` | Needs `FORGEJO_URL`, `FORGEJO_REPO`, `FORGEJO_TOKEN` env (token scope: `write:issue`) |
| Issue watcher | `automation/claude-issue-watcher.ps1` | Config in `watcher.config.json` (gitignored — copy from the example). Logs in `automation/logs/` |
| Scheduled task | `automation/register-watcher-task.ps1` | Registers `PelagiaClaudeIssueWatcher`, every 10 min, single-instance |
| Deploy workflow | `.forgejo/workflows/deploy.yml` | Triggers on `v*` tags; runs on the `host` runner |
| Runner | pms1 `~/forgejo-runner`, pm2 process `forgejo-runner` | Registered as `pms1-host` with labels `host`, `docker` |
## Issue label lifecycle
`claude-queue``claude-working``claude-pr` (PR opened, awaiting review)
or `claude-failed` (no verified fix; reason posted as an issue comment).
To retry a failed issue, re-add the `claude-queue` label.
To queue any manually-created issue for Claude, just add the `claude-queue` label.
## Releasing
After merging a Claude PR (or any change) on `master`:
```powershell
git pull
git tag v0.2.0 # semver: bump patch for fixes, minor for features
git push pms1 master --tags
```
The runner deploys the tag and restarts the app. Watch progress under
**Actions** on the Forgejo repo, or `pm2 logs forgejo-runner` on pms1.
## Operational notes
- The watcher runs Claude Code with `--dangerously-skip-permissions` inside the
dedicated `pelagia-autofix` clone — never point `workDir` at your main checkout.
- Watcher only works issues while this PC is on; queued issues are picked up on
the next run after boot (`-StartWhenAvailable`).
- Tokens: `portal-report-issue` (write:issue, used by the app) and
`claude-watcher` (write:issue + write:repository, used by the watcher).
Both belong to the `shad0w` Forgejo account. Rotate via
`docker exec -u 1000 forgejo forgejo admin user generate-access-token ...`.
- Server-side env for the button lives in `~/pms/App/.env` on pms1
(`FORGEJO_URL=http://127.0.0.1:3001` so it does not depend on the tunnel).

View file

@ -0,0 +1,197 @@
# Claude issue watcher for the Pelagia portal.
#
# Polls Forgejo for open issues labelled `claude-queue`, runs headless
# Claude Code on a dedicated clone to implement a fix, pushes a
# `claude/issue-N` branch, and opens a PR that closes the issue.
# Label lifecycle: claude-queue -> claude-working -> claude-pr | claude-failed
#
# Intended to run unattended via Windows Task Scheduler (see
# register-watcher-task.ps1). Logs to automation/logs/.
[CmdletBinding()]
param(
[string]$ConfigPath
)
$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
if (-not $ConfigPath) { $ConfigPath = Join-Path $scriptDir 'watcher.config.json' }
if (-not (Test-Path $ConfigPath)) {
throw "Config not found: $ConfigPath (copy watcher.config.example.json and fill in the token)"
}
$cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
$logDir = Join-Path $scriptDir 'logs'
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir | Out-Null }
$logFile = Join-Path $logDir ("watcher-{0}.log" -f (Get-Date -Format 'yyyy-MM-dd'))
function Log([string]$msg) {
$line = "{0} {1}" -f (Get-Date -Format 'HH:mm:ss'), $msg
$line | Out-File -FilePath $logFile -Append -Encoding utf8
Write-Host $line
}
# ── Single-instance lock ────────────────────────────────────────────
$lockFile = Join-Path $scriptDir '.watcher.lock'
if (Test-Path $lockFile) {
$lockPid = Get-Content $lockFile -TotalCount 1
if ($lockPid -and (Get-Process -Id $lockPid -ErrorAction SilentlyContinue)) {
Log "Another watcher run (PID $lockPid) is active; exiting."
exit 0
}
}
$PID | Out-File -FilePath $lockFile -Encoding ascii
try {
# ── Forgejo API helpers ─────────────────────────────────────────────
$apiBase = "$($cfg.forgejoUrl)/api/v1"
$headers = @{ Authorization = "token $($cfg.token)" }
function Api([string]$Method, [string]$Path, $Body = $null) {
$params = @{ Method = $Method; Uri = "$apiBase$Path"; Headers = $headers }
if ($null -ne $Body) {
$params.Body = ($Body | ConvertTo-Json -Depth 5)
$params.ContentType = 'application/json'
}
Invoke-RestMethod @params
}
function Set-IssueLabels([int]$IssueNumber, [string[]]$Remove, [string[]]$Add) {
$allLabels = Api GET "/repos/$($cfg.repo)/labels?limit=50"
$issue = Api GET "/repos/$($cfg.repo)/issues/$IssueNumber"
$current = @($issue.labels | ForEach-Object { $_.name })
$wanted = @(($current | Where-Object { $Remove -notcontains $_ }) + $Add | Select-Object -Unique)
$ids = @($allLabels | Where-Object { $wanted -contains $_.name } | ForEach-Object { $_.id })
Api PUT "/repos/$($cfg.repo)/issues/$IssueNumber/labels" @{ labels = $ids } | Out-Null
}
function Add-IssueComment([int]$IssueNumber, [string]$Text) {
Api POST "/repos/$($cfg.repo)/issues/$IssueNumber/comments" @{ body = $Text } | Out-Null
}
# Run git without tripping ErrorActionPreference=Stop on stderr output
# (native stderr lines become ErrorRecords in PS 5.1). Returns exit code.
function Run-Git([string[]]$GitArgs) {
$prev = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
& git @GitArgs 2>&1 | ForEach-Object { $_.ToString() } | Out-File $logFile -Append -Encoding utf8
return $LASTEXITCODE
} finally {
$ErrorActionPreference = $prev
}
}
# ── Find queued issues ──────────────────────────────────────────────
$queued = @(Api GET "/repos/$($cfg.repo)/issues?state=open&labels=claude-queue&type=issues&limit=20" | Where-Object { $_ -and $_.number })
if ($queued.Count -eq 0) {
Log "No queued issues."
exit 0
}
$queued = @($queued | Sort-Object number | Select-Object -First $cfg.maxIssuesPerRun)
Log "Found $($queued.Count) queued issue(s): $(($queued | ForEach-Object { '#' + $_.number }) -join ', ')"
# ── Prepare the dedicated work clone ────────────────────────────────
$repoHost = ([Uri]$cfg.forgejoUrl).Host
$owner = $cfg.repo.Split('/')[0]
$cloneUrl = "https://$($owner):$($cfg.token)@$repoHost/$($cfg.repo).git"
if (-not (Test-Path (Join-Path $cfg.workDir '.git'))) {
Log "Cloning $($cfg.repo) into $($cfg.workDir)"
if ((Run-Git @('clone', $cloneUrl, $cfg.workDir)) -ne 0) { throw "git clone failed" }
Run-Git @('-C', $cfg.workDir, 'config', 'user.name', 'Claude (auto-fix)') | Out-Null
Run-Git @('-C', $cfg.workDir, 'config', 'user.email', 'claude-autofix@pelagiamarine.com') | Out-Null
}
foreach ($issue in $queued) {
$n = $issue.number
$branch = "$($cfg.branchPrefix)$n"
Log "── Working issue #${n}: $($issue.title)"
Set-IssueLabels $n -Remove @('claude-queue', 'claude-failed') -Add @('claude-working')
Add-IssueComment $n "🤖 Claude has started working on this issue on branch ``$branch``."
Run-Git @('-C', $cfg.workDir, 'fetch', 'origin') | Out-Null
if ((Run-Git @('-C', $cfg.workDir, 'checkout', '-B', $branch, "origin/$($cfg.baseBranch)")) -ne 0) {
Log "checkout failed for #$n"; continue
}
$prompt = @"
You are working autonomously on issue #$n of the Pelagia Portal (PPMS), a Next.js 15 purchase-order
management system for a maritime company. The web app lives in the App/ directory read App/CLAUDE.md
first for architecture, conventions, and commands.
## Issue #${n}: $($issue.title)
$($issue.body)
## Your job
1. Investigate the issue and implement a focused, minimal fix in this repository.
2. Verify your change: run ``pnpm type-check`` and ``pnpm lint`` in App/. If you changed behaviour
covered by unit tests, run the relevant tests. Do not start the dev server or any database.
3. Add or adjust tests when it makes sense for the change.
4. Commit ALL your changes to the current branch with a conventional commit message that ends with
the line: Fixes #$n
5. Do NOT push, do NOT create tags, do NOT switch branches. The supervisor script handles push and PR.
If the issue is unclear, too risky to change without human input (e.g. data migrations, payments,
permissions changes), or you cannot verify the fix, make NO commits and instead write a short
explanation to a file named CLAUDE_RESULT.md in the repository root (it will be relayed to the issue).
"@
$promptFile = Join-Path $env:TEMP "claude-issue-$n-prompt.txt"
$prompt | Out-File -FilePath $promptFile -Encoding utf8
$claudeLog = Join-Path $logDir "claude-issue-$n-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
Log "Running Claude Code on #$n (log: $claudeLog)"
# cmd handles the redirects so native stderr never becomes a PS ErrorRecord
Push-Location $cfg.workDir
try {
cmd /c "`"$($cfg.claudeExe)`" -p --dangerously-skip-permissions --max-turns $($cfg.claudeMaxTurns) --output-format text < `"$promptFile`" > `"$claudeLog`" 2>&1"
Log "Claude exited with code $LASTEXITCODE for #$n"
} finally {
Pop-Location
}
# Relay an abort explanation if Claude declined the fix
$resultFile = Join-Path $cfg.workDir 'CLAUDE_RESULT.md'
$abortNote = $null
if (Test-Path $resultFile) {
$abortNote = Get-Content $resultFile -Raw
Remove-Item $resultFile -Force
Run-Git @('-C', $cfg.workDir, 'checkout', '--', '.') | Out-Null
}
$commitCount = [int](& git -C $cfg.workDir rev-list "origin/$($cfg.baseBranch)..HEAD" --count)
if ($commitCount -gt 0) {
Log "Claude made $commitCount commit(s); pushing $branch"
if ((Run-Git @('-C', $cfg.workDir, 'push', '-f', '-u', 'origin', $branch)) -ne 0) {
Log "push failed for #$n"; Set-IssueLabels $n -Remove @('claude-working') -Add @('claude-failed'); continue
}
$prTitle = $issue.title -replace '^\[Issue\]:\s*', ''
$pr = Api POST "/repos/$($cfg.repo)/pulls" @{
base = $cfg.baseBranch
head = $branch
title = "fix: $prTitle"
body = "Automated fix by Claude Code for #$n.`n`nCloses #$n`n`nReview, merge, then create a release tag (vX.Y.Z) to deploy."
}
Set-IssueLabels $n -Remove @('claude-working') -Add @('claude-pr')
Add-IssueComment $n "🤖 Claude opened PR [#$($pr.number)]($($pr.html_url)) with a proposed fix. Review and merge it, then create a release tag to deploy."
Log "PR #$($pr.number) opened for issue #$n"
} else {
Log "No commits produced for #$n; marking claude-failed"
Set-IssueLabels $n -Remove @('claude-working') -Add @('claude-failed')
$reason = if ($abortNote) { $abortNote } else { "Claude did not produce a verified fix. See watcher logs on the dev machine: $claudeLog" }
Add-IssueComment $n "🤖 Automated fix attempt did not produce a change.`n`n$reason"
}
}
} finally {
Remove-Item $lockFile -Force -ErrorAction SilentlyContinue
}

View file

@ -0,0 +1,24 @@
# Registers the Claude issue watcher as a Windows Scheduled Task.
# Runs every 10 minutes while the machine is on; skips if a run is in progress.
# Run once: powershell -ExecutionPolicy Bypass -File register-watcher-task.ps1
$ErrorActionPreference = 'Stop'
$taskName = 'PelagiaClaudeIssueWatcher'
$scriptPath = Join-Path $PSScriptRoot 'claude-issue-watcher.ps1'
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$scriptPath`""
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) `
-RepetitionInterval (New-TimeSpan -Minutes 10)
$settings = New-ScheduledTaskSettingsSet `
-MultipleInstances IgnoreNew `
-ExecutionTimeLimit (New-TimeSpan -Hours 2) `
-StartWhenAvailable
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
-Settings $settings -Description 'Polls Forgejo for claude-queue issues and runs Claude Code on them' -Force
Write-Host "Scheduled task '$taskName' registered (every 10 minutes)."

View file

@ -0,0 +1,11 @@
{
"forgejoUrl": "https://git.pelagiamarine.com",
"repo": "shad0w/pelagia-portal",
"token": "<forgejo token with write:issue,write:repository>",
"workDir": "C:\\Users\\shad0w\\Documents\\src\\pelagia-autofix",
"baseBranch": "master",
"branchPrefix": "claude/issue-",
"maxIssuesPerRun": 1,
"claudeExe": "C:\\Users\\shad0w\\.local\\bin\\claude.exe",
"claudeMaxTurns": 150
}