- 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> |
||
|---|---|---|
| .. | ||
| claude-issue-watcher.ps1 | ||
| claude-issue-watcher.sh | ||
| README.md | ||
| register-watcher-task.ps1 | ||
| run-watcher.cmd | ||
| watcher.config.example.json | ||
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 nvmclaudepath) - Work clone:
~/pelagia-autofix(separate from the deployed~/pms) - Logs:
~/issue-watcher/logs/(watcher-<date>.log, per-issueclaude-*.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
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.