Portal issues now file with only the 'portal' label. The watcher runs two phases:
1. Triage — Claude reads each untriaged 'portal' issue (analysis only), posts a
requirements-breakdown comment, and routes it to 'claude-queue' (auto-fixable)
or 'interactive' (needs human steering).
2. Fix — unchanged; processes 'claude-queue' issues into PRs.
The triage breakdown is posted without the bot marker so the fix stage reads it
back as refined requirements.
PS 5.1 fixes found while validating:
- Send API bodies as UTF-8 bytes (Invoke-RestMethod mangled non-ASCII, e.g. the
em-dash in Claude's breakdown, so Forgejo rejected the JSON)
- Build the labels array body by hand (ConvertTo-Json unwraps a single-element
array to a scalar, which Forgejo rejects)
- Triage output via two plain files (label + markdown) instead of one JSON blob
(embedded-newline markdown broke ConvertFrom-Json)
- Read triage files as UTF-8; additive label POST + a guard so Set-IssueLabels
can never wipe an issue's labels
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.1 KiB
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 (label: portal) [git.pelagiamarine.com/shad0w/pelagia-portal]
│ polled every 10 min by Windows Scheduled Task "PelagiaClaudeIssueWatcher"
▼
TRIAGE (watcher phase 1) [dev PC, headless Claude Code, analysis only]
│ Claude reads the issue + repo, posts a requirements-breakdown comment,
│ and routes it: adds `claude-queue` (auto-fixable) or `interactive` (human)
▼
FIX (watcher phase 2, only for claude-queue) [headless Claude Code in C:\...\src\pelagia-autofix]
│ Claude implements + verifies fix; 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
interactive-routed issues stop after triage for a human to pick up (run with
Claude in a steered session). The triage breakdown comment is plain (no bot
marker) so, for claude-queue issues, the fix stage reads it back as refined
requirements.
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 only the portal label (triage routes it) |
| 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
portal ──(triage)──▶ claude-queue ─▶ claude-working ─▶ claude-pr | claude-failed
└────▶ interactive (stops here — handle with Claude interactively)
- A
portalissue with no decision label yet is triaged once (maxTriagePerRunper run). Triage addsclaude-queueorinteractiveand posts a breakdown. claude-queue→claude-working→claude-pr(PR opened) orclaude-failed.- To retry a failed issue, re-add
claude-queue. - To queue any manually-created issue for Claude (skipping triage), add
claude-queuedirectly. To force human handling, addinteractive. - Triage is skipped for issues that already carry any decision label.
Releasing
After merging a Claude PR (or any change) on master:
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-permissionsinside the dedicatedpelagia-autofixclone — never pointworkDirat 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) andclaude-watcher(write:issue + write:repository, used by the watcher). Both belong to theshad0wForgejo account. Rotate viadocker exec -u 1000 forgejo forgejo admin user generate-access-token .... -
Server-side env for the button lives in
~/pms/App/.envon pms1 (FORGEJO_URL=http://127.0.0.1:3001so it does not depend on the tunnel). -
Known Forgejo 10 bug: clicking Update branch on a PR (or pushing to its head branch) can make the page show "This pull request is broken due to missing fork information" even though the PR is fine (API still reports
mergeable: true). Fix: close and reopen the PR — via the UI, or:$h = @{ Authorization = "token <claude-watcher token>" } Invoke-RestMethod -Method Patch -Headers $h -ContentType application/json ` -Uri https://git.pelagiamarine.com/api/v1/repos/shad0w/pelagia-portal/pulls/<N> -Body '{"state":"closed"}' Invoke-RestMethod -Method Patch -Headers $h -ContentType application/json ` -Uri https://git.pelagiamarine.com/api/v1/repos/shad0w/pelagia-portal/pulls/<N> -Body '{"state":"open"}'Fixed upstream in newer Gitea/Forgejo — resolves itself if Forgejo is upgraded past v10.