pelagia-portal/automation
Hardik 3e711a171c feat(automation): port issue watcher to bash for pms1 (cron, 24/7)
- automation/claude-issue-watcher.sh: Linux port of the watcher (curl + jq +
  flock). Same triage + fix phases. On Linux the PS 5.1 encoding/array quirks
  don't apply, so it's simpler.
- Auth preflight: no-ops until Claude Code is signed in on the host (or an
  ANTHROPIC_API_KEY is set), so cron can be enabled before sign-in.
- Runs on pms1 under cron every 10 min; Windows scheduled task is disabled so the
  two machines don't race the Forgejo queue.
- .gitattributes pins *.sh to LF.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 04:32:06 +05:30
..
claude-issue-watcher.ps1 feat(automation): add triage phase to issue watcher 2026-06-19 04:20:21 +05:30
claude-issue-watcher.sh feat(automation): port issue watcher to bash for pms1 (cron, 24/7) 2026-06-19 04:32:06 +05:30
README.md feat(automation): port issue watcher to bash for pms1 (cron, 24/7) 2026-06-19 04:32:06 +05:30
register-watcher-task.ps1 feat(automation): issue-to-deploy pipeline — Report Issue button, Claude watcher, tag-triggered deploy 2026-06-11 16:39:43 +05:30
run-watcher.cmd feat(automation): add manual run-watcher launcher for desktop shortcut 2026-06-19 03:28:12 +05:30
watcher.config.example.json feat(automation): add triage phase to issue watcher 2026-06-19 04:20:21 +05:30

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 (active) automation/claude-issue-watcher.sh on pms1 Bash port; runs 24/7 via cron. Config + logs under ~/issue-watcher/
Issue watcher (Windows, disabled) automation/claude-issue-watcher.ps1 PowerShell original. PelagiaClaudeIssueWatcher task is disabled (pms1 is the sole worker; two pollers would race)
Forgejo helper App/lib/forgejo.ts Needs FORGEJO_URL, FORGEJO_REPO, FORGEJO_TOKEN env (token scope: write:issue)
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

Where the watcher runs (pms1)

The watcher runs on pms1 under cron (every 10 min), polling Forgejo over the local loopback (http://127.0.0.1:3001).

  • Script: ~/issue-watcher/claude-issue-watcher.sh (source: automation/claude-issue-watcher.sh)
  • Config: ~/issue-watcher/watcher.config.json (gitignored; holds the token + claudeExe = the nvm claude path)
  • Work clone: ~/pelagia-autofix (separate from the deployed ~/pms)
  • Logs: ~/issue-watcher/logs/ (watcher-<date>.log, per-issue claude-*.log, cron.log)
  • Crontab: */10 * * * * PATH=<nvm bin>:... ~/issue-watcher/claude-issue-watcher.sh >> ~/issue-watcher/logs/cron.log 2>&1

Auth: Claude Code must be signed in on pms1 (ssh in, run claude, complete the login → writes ~/.claude/.credentials.json). The watcher has a preflight that no-ops until those credentials exist, so cron can be enabled before sign-in and activates automatically once signed in. (An ANTHROPIC_API_KEY env var also satisfies it.)

The Windows variant (.ps1 + register-watcher-task.ps1) is the portable fallback; re-enable its task only if pms1 is unavailable, and disable one before enabling the other.

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.