Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .claude/skills/triage/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
name: triage
description: Run autonomous discovery + triage on the Iridium repo. Use when asked to "triage", run the morning loop, scan for regressions/bugs/stale work, or summarize CI failures or recent commits. Reads recent commits, open issues, and the latest CI run, then files actionable findings as `triage`-labeled GitHub issues (deduping against existing ones). GitHub Issues is the loop's memory — there is no state file. Invoked on a schedule by .github/workflows/triage.yml and runnable by hand in any session.
---

# Triage loop

You are the discovery + triage stage of a [loop-engineered](../../../docs/loop/README.md)
workflow. You run unattended on a schedule. Your job is **not** to fix things —
it is to find what is worth doing and record it where the next run, a human, or
Ralph can pick it up. Execution happens elsewhere.

**GitHub Issues is your memory.** There is no state file. You forget everything
between runs; the issue tracker does not. This makes you naturally idempotent:
before filing anything you search existing issues, so re-running never
duplicates. Read the tracker first, write to it last.

Use the `gh` CLI when available (it is, in CI, authenticated via `GH_TOKEN`),
otherwise the GitHub MCP tools.

## Procedure

1. **Load memory + steering.**
- Open findings: `gh issue list --label triage --state open -L 50 --json number,title,body,labels`.
This is the list you dedup against — never re-file anything already here.
- Operator steering: look for an open issue labeled `triage-meta` titled
"Triage loop — operator notes". If it exists, read it and obey it (focus
areas, paths to ignore). It is the human's steering wheel; honor it over
your own judgement.

2. **Establish the window.** Find the previous run so you only look at what is
new: `gh run list --workflow=triage.yml --status success -L 2 --json createdAt`
→ use the prior run's `createdAt` as your "since" time. If there is no prior
run, use the last 24h.

3. **Gather signal** (cheap reads first; stop when you have enough — be frugal
with tokens):
- **Commits since the window:** `git log --oneline --no-merges --since="<ts>"`.
- **Latest CI:** `gh run list --workflow=ci.yml --branch=main -L 5`. For any
failure, pull only the failing step: `gh run view <id> --log-failed`. Never
download whole logs.
- **Stale/duplicate issues:** from the list in step 1, flag issues a recent
commit already resolved, or obvious duplicates.
- **Cheap health probes** (only if commits touched the relevant area):
`bun run typecheck`, `bun run lint`. Do **not** run e2e here — that is CI's job.

4. **Classify each candidate.**
- **actionable** — concrete, scoped, with a clear done-condition (a failing
test, a type error, a dead route, a missing validation). File it.
- **watch** — real but not ripe (a test that flaked once, a TODO). File only
if it recurs; a single flake is not yet an issue.
- **noise** — already tracked, already fixed, or out of scope. Skip silently.

5. **File actionable findings** as issues, labeled `triage` (create the label
first if missing: `gh label create triage --color FBCA04 --description "Filed by the triage loop"`):
- **Dedup first.** Compare against the open `triage` issues from step 1 and
`gh issue list --search "<keywords>" --state open`. If a near-duplicate
exists, add a comment with the new evidence instead of opening a second issue.
- Title: imperative and specific — `Fix type error in app/models/thread.server.ts`.
- Body: the evidence (log excerpt, commit SHA, `file:line`), a proposed
done-condition, and a line noting it was filed by the triage loop.
- `gh issue create --label triage --title "..." --body "..."`.

6. **Close the loop on resolved items.** If a `triage` issue is clearly fixed by
a commit in the window, close it with a reference:
`gh issue close <n> --comment "Resolved by <sha>."`.

7. **Report, don't persist.** Print a one-paragraph summary of the run (window,
commits scanned, issues filed/closed). Commit nothing — the tracker is the
record, not the repo. A triage run produces zero file changes.

## Discipline (this is what keeps the loop honest)

- **One source of truth.** A finding is an open `triage` issue until it's closed.
Never mirror it into a file or a second issue.
- **Evidence or it didn't happen.** Every issue cites a commit SHA, a CI run, or
a `file:line`. No vibes-based issues.
- **Token frugality.** `--log-failed` over full logs, `git log` over reading
diffs, targeted `Grep` over whole files. A run that reads the whole codebase
is a bug.
- **Stay in your lane.** You discover and record. You do not refactor, fix, or
open PRs. Hand execution to the next stage.
- **Quiet on nothing.** If nothing is actionable, file nothing and say so. An
empty run is a successful run.
59 changes: 59 additions & 0 deletions .github/workflows/triage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Triage Loop

# The "heartbeat" of the loop-engineered workflow (see docs/loop/README.md).
# Runs the `triage` skill on a schedule: it discovers work and files actionable
# items as `triage`-labeled GitHub issues — the issue tracker is the loop's
# memory, so this job writes nothing back to the repo. It does NOT fix anything;
# execution is a separate stage.

on:
schedule:
# 13:00 UTC on weekdays (~8am ET). Adjust or delete to change cadence.
- cron: '0 13 * * 1-5'
workflow_dispatch:

# Only one triage run at a time; a new run cancels nothing but waits its turn.
concurrency:
group: triage-loop
cancel-in-progress: false

permissions:
contents: read # checkout + git log only; the loop commits nothing
issues: write # file actionable findings (the loop's memory)
actions: read # read CI run results + prior triage run timestamps
pull-requests: read

jobs:
triage:
name: Discover + triage
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 50 # enough history for `git log` since last run

- uses: oven-sh/setup-bun@v2

# Install so the skill can run cheap health probes (typecheck/lint)
# when a finding needs confirming. e2e is left to the CI workflow.
- name: Install dependencies
run: bun install --frozen-lockfile

- name: Generate Prisma client
run: bunx --bun prisma generate

- name: Run triage loop
uses: anthropics/claude-code-action@v1
env:
# gh CLI (used by the skill) authenticates via this token.
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# Sonnet keeps the recurring token cost low; triage is a
# reading/judgement task, not heavy generation.
prompt: '/triage'
claude_args: |
--model claude-sonnet-4-6
--allowedTools "Bash,Read,Write,Edit,Glob,Grep,mcp__github"
18 changes: 18 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Build artifacts & generated output
/node_modules/
/.react-router/
/build/
app/generated/prisma
/test-results/
/playwright-report/
/blob-report/

# Lockfiles
bun.lock
skills-lock.json

# Vendored / tool-generated agent & spec-kit scaffolding (not project source)
.agents/
.claude/
.specify/
.github/agents/
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ Shared chrome is extracted into `SiteHeader` and `SiteFooter` (`app/components/`
Prettier with: 80 char width, 4-space indentation, single quotes, semicolons, tailwindcss plugin for class sorting. ESLint with typescript-eslint and react-hooks plugin.

<!-- SPECKIT START -->

For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan

<!-- SPECKIT END -->
68 changes: 68 additions & 0 deletions docs/loop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# The Iridium loop

This repo is set up for **loop engineering**: instead of prompting an agent turn
by turn, you run small systems that prompt the agents for you. The pieces below
already exist in the repo; this directory adds the one that was missing — the
scheduled **heartbeat**.

## The pieces (and where they live)

| Loop pillar | In this repo |
| -------------- | ---------------------------------------------------------------------------- |
| **Heartbeat** | `.github/workflows/triage.yml` — scheduled discovery + triage |
| **Memory** | **GitHub Issues** (label `triage`) — queryable, merge-safe, outlives any run |
| **Skills** | `.claude/skills/` — incl. `triage` (the loop's brain) and `iridium-form` |
| **Sub-agents** | `.claude/agents/` — `staff-engineer`, `security-auditor`, `prisma`, … |
| **Worktrees** | `scripts/ralph/ralph.sh` (run in one) + subagent `isolation: worktree` |
| **Connectors** | GitHub, Linear, context7 MCP servers |
| **Execution** | `scripts/ralph/` — the autonomous loop that _does_ the work |

**Why Issues and not a state file?** The loop's memory has to be queryable,
merge-safe under parallel runs, and visible to humans. A markdown file is none of
those — the agent would rewrite it whole every run and two runs would collide.
GitHub Issues already gives us all three for free, and the loop is naturally
idempotent because it dedups by _searching open issues_ before filing. (If you
later turn on heavy autonomous multi-agent execution and want a dependency
"ready-queue" to feed Ralph, [`beads`](https://github.com/steveyegge/beads) is
the purpose-built upgrade — but Issues is the right call until then.)

## How the heartbeat works

```
schedule (weekday mornings)
triage.yml ──runs──▶ /triage skill
reads: recent commits · latest CI run · open `triage` issues
dedups → files / comments / closes GitHub issues (label `triage`)
writes nothing back to the repo
```

Triage **discovers and records only** — it never fixes code or opens PRs.
Execution is a deliberately separate stage so the maker is never the checker.

## Operating it

- **Run it now:** Actions tab → _Triage Loop_ → _Run workflow_ (`workflow_dispatch`).
- **Run it locally / in a session:** invoke the `triage` skill (`/triage`).
- **See its memory:** the open issues labeled `triage` _are_ the loop's state.
- **Steer it:** open an issue labeled `triage-meta` titled
"Triage loop — operator notes". The loop reads and obeys it (e.g. "ignore the
legacy/ dir", "focus on auth"). It is the human steering wheel.
- **Change cadence:** edit the `cron` in `triage.yml`.
- **Turn it off:** delete `triage.yml`, or disable _Triage Loop_ in the Actions tab.

## Required setup

The workflow needs an `ANTHROPIC_API_KEY` repository secret. The built-in
`GITHUB_TOKEN` covers reading CI and filing/closing issues.

## A note on cost and quality

Scheduled agents spend tokens whether or not they find anything, so this loop
runs on Sonnet, prefers cheap reads (`git log`, `--log-failed`) over reading the
whole tree, and is capped at 15 minutes per run. It also does not replace you:
every issue it files is a _claim_, and every fix that follows still needs a human
review before it ships. Build the loop — stay the engineer.
45 changes: 24 additions & 21 deletions scripts/ralph/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,24 @@ You are an autonomous coding agent working on a software project.

```json
{
"project": "MyApp",
"branchName": "ralph/feature-name",
"description": "One-line summary of the feature",
"userStories": [
{
"id": "US-001",
"title": "Story title",
"description": "As a ..., I want ... so that ...",
"acceptanceCriteria": ["...", "Typecheck passes (`bun run typecheck`)"],
"order": 1,
"status": "pending",
"blockedReason": "",
"notes": ""
}
]
"project": "MyApp",
"branchName": "ralph/feature-name",
"description": "One-line summary of the feature",
"userStories": [
{
"id": "US-001",
"title": "Story title",
"description": "As a ..., I want ... so that ...",
"acceptanceCriteria": [
"...",
"Typecheck passes (`bun run typecheck`)"
],
"order": 1,
"status": "pending",
"blockedReason": "",
"notes": ""
}
]
}
```

Expand Down Expand Up @@ -68,7 +71,7 @@ When all stories in `prd.json` have `status` of `"done"`, write a file at
`.ralph-status.json` (sibling of this CLAUDE.md) containing:

```json
{"status": "complete"}
{ "status": "complete" }
```

Do not rely on free-form text in your reply — the ralph.sh loop looks at the
Expand All @@ -78,7 +81,7 @@ normally.
If you cannot proceed and want the loop to stop instead of retrying, write:

```json
{"status": "blocked", "reason": "short human-readable explanation"}
{ "status": "blocked", "reason": "short human-readable explanation" }
```

## Progress Report Format
Expand Down Expand Up @@ -118,10 +121,10 @@ Before committing, check whether nearby agent-instruction files should learn fro
1. Identify directories with edited files.
2. Look for an existing `CLAUDE.md` or `AGENTS.md` in those directories or their parents.
3. Add genuinely reusable knowledge:
- API patterns or conventions specific to that module
- Non-obvious requirements or gotchas
- Cross-file dependencies
- Testing setup specific to that area
- API patterns or conventions specific to that module
- Non-obvious requirements or gotchas
- Cross-file dependencies
- Testing setup specific to that area

Do not add story-specific implementation details, temporary debugging notes, or anything already in `progress.txt`.

Expand Down
Loading
Loading