pelagia-portal/automation/README.md
Hardik 080dafb473 feat(automation): add triage phase to issue watcher
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>
2026-06-19 04:20:21 +05:30

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 portal issue with no decision label yet is triaged once (maxTriagePerRun per run). Triage adds claude-queue or interactive and posts a breakdown.
  • claude-queueclaude-workingclaude-pr (PR opened) or claude-failed.
  • To retry a failed issue, re-add claude-queue.
  • To queue any manually-created issue for Claude (skipping triage), add claude-queue directly. To force human handling, add interactive.
  • 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-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).

  • 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.