Files
agent-skills/skills/tracker-common/COMMON.md
T
Miguel Palhas 0900e5fea1
ci / nix (push) Successful in 10s
ci / lint (push) Successful in 12s
perf(gate): widen per-slot jobs to (cores-2)/slots
The old cores/SLOTS/2 left a full test suite on 2 of 10 cores even with
the box idle. Reserve two cores for the agent sessions and split the
rest across slots.

Also retire the CPU framing on the subagent fan-out cap. The number
stays 3, but the binding constraints are memory per worktree and the
shared rate-limit window; subagents are mostly idle waiting on the API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 13:56:55 +01:00

16 KiB

Tracker Common - Shared Config & Setup

This document is referenced by the /work and /yolo skills. Do not invoke it directly.

Project config

Look for the tracker config in the current git repo root (git rev-parse --show-toplevel), trying these paths in order and using the first that exists:

  1. .claude/tracker.json (preferred, generic name)
  2. .claude/linear.json (legacy name, still supported)

If none exists, run First-time setup below, then continue. When creating the file in setup, prefer the generic .claude/tracker.json name.

Schema

GitHub example (tracker: github):

{
  "tracker": "github",
  "defaultBranch": "main",
  "commitScope": "contracts",
  "buildCommand": "pnpm test",
  "contextFiles": ["AGENTS.md"],
  "prReviewers": [],
  "labels": []
}

Gitea example (tracker: gitea):

{
  "tracker": "gitea",
  "defaultBranch": "main",
  "commitScope": "maestro",
  "remoteHost": "gitea",
  "remoteBaseUrl": "https://git.naps.pt",
  "buildCommand": "cargo test",
  "contextFiles": ["CLAUDE.md"],
  "prReviewers": [],
  "labels": []
}

Linear example (tracker: linear):

{
  "tracker": "linear",
  "org": "ern",
  "team": "Ern",
  "project": "Contracts v2",
  "defaultBranch": "main",
  "commitScope": "platform",
  "buildCommand": "forge test",
  "setupCommands": ["bun install"],
  "contextFiles": ["docs/README.md"],
  "prReviewers": [],
  "labels": []
}
Field Required Description
tracker no Where issues live: linear (default), gitea, or github. See Tracker backend below.
linearMcp no MCP server name when tracker: linear. Default linear-server. Set a per-workspace name (e.g. linear-acme) when a project has its own server.
org linear only Linear organization slug
team linear only Linear team name (for listing/creating issues)
project no Linear project name (narrows issue search). Ignored when tracker: gitea.
defaultBranch no Base branch, default main
commitScope no Conventional commit scope, e.g. platform -> feat(platform): ...
buildCommand no Command to verify the build. Run after implementation.
setupCommands no Commands to run inside a new worktree (install deps, etc.)
contextFiles no Files to read before coding (specs, architecture docs)
prReviewers no Reviewer usernames to request reviews from (/work only)
labels no Default issue labels for ad-hoc issues
remoteHost no github (default) or gitea. Selects which API /work uses for PR creation and review polling. /yolo is unaffected. Implied gitea when tracker: gitea, github when tracker: github.
remoteBaseUrl no Required when remoteHost: gitea or tracker: gitea. Base URL of the Gitea instance (e.g. https://git.naps.pt).

The config file is named .claude/tracker.json (preferred) or .claude/linear.json (legacy); either configures whichever tracker tracker selects.

Tracker backend

tracker selects where issues live. Every instruction below that refers to "the issue" applies to the configured backend; where the two differ (selection, status changes, branch naming) the gitea-specific steps are called out explicitly.

  • github: issues live in the repo's own GitHub issue tracker — no Linear MCP involved. The repo (owner/repo) is derived from git remote get-url origin; all issue and PR operations use the gh CLI (must be authenticated — gh auth status). org/team/project are ignored. GitHub has no workflow states, so WIP is signalled by assigning the issue to yourself (like gitea); the PR's Closes #N closes the issue on merge.
  • gitea: issues live in the repo's own Gitea issue tracker — no Linear MCP involved. The repo (owner/repo) is derived from git remote get-url origin; the API uses remoteBaseUrl + $GITEA_TOKEN (load via source ~/.env.claude if needed), exactly like the /work Gitea PR variant. org/team/project are ignored.
  • linear (default when omitted, for compatibility): issues live in Linear, accessed via the MCP server named by linearMcp (default linear-server; a workspace with its own server sets its own name). Requires org + team. Tool calls use the mcp__<linearMcp>__* prefix.

First-time setup

If no tracker config exists:

  1. Ask the user which tracker (linear, gitea, or github). For linear, also ask org, team, and optionally project; for gitea, ask remoteBaseUrl.
  2. Ask which optional fields they want. Show the table above.
  3. Write the file to .claude/tracker.json (preferred). Suggest they commit it or gitignore it depending on preference.
  4. Continue with the task.

Remote host (/work only)

/work opens PRs and iterates on reviews. The forge it talks to is selected by remoteHost:

  • github (default): uses gh for gh pr create, gh pr view, and the GitHub GraphQL review-thread API. No extra config needed. Requires an authenticated gh (gh auth status).
  • gitea: uses Gitea's REST API via curl. Requires remoteBaseUrl (e.g. https://git.naps.pt) and $GITEA_TOKEN in the environment (typically loaded from ~/.env.claude). See /work's "Gitea variant" section for the specific endpoints.

/yolo doesn't care about remoteHost — push and merge are plain git, which works against any remote.

Linear MCP auth (tracker: linear only)

Skip this section entirely when tracker: gitea.

The Linear MCP server (named by linearMcp, default linear-server) supports one org at a time. Before making Linear API calls, verify the current auth matches the configured org. If it doesn't (or if auth fails), tell the user to re-authenticate via mcp__<linearMcp>__authenticate and stop. Don't try to work around auth issues silently.

Task selection

Based on $ARGUMENTS. The steps differ by tracker.

tracker: linear

Linear issue ID provided (e.g. ERN-347)

  1. Fetch the issue via the configured Linear MCP (get_issue).
  2. Read the full description, acceptance criteria, and comments.

Ad-hoc task description provided (free text, not matching an issue ID pattern)

  1. Create a new Linear issue in the configured team (and project if set).
  2. Apply any configured default labels.
  3. Use the provided text as the issue title. If it's long, summarize for the title and use the full text as description.

No argument (auto-pick)

  1. List issues in the configured team/project that are unstarted (Backlog, Todo, Ready, or equivalent).
  2. Pick the highest-priority unblocked issue.
  3. If none found, tell the user and stop.

Then:

  • Move the issue to "In Progress" immediately.
  • Note the issue ID, title, and gitBranchName for later use.
  • Use gitBranchName from the Linear response for branch naming (auto-links in Linear).

tracker: gitea

Set BASE=$remoteBaseUrl, REPO=<owner>/<repo> (from git remote get-url origin), and send Authorization: token $GITEA_TOKEN on every call.

Issue number provided (e.g. 23 or #23)

  1. GET $BASE/api/v1/repos/$REPO/issues/23.
  2. Read the title, body, and comments (GET .../issues/23/comments).

Ad-hoc task description provided (free text, not a bare number)

  1. Create the issue:
    curl -sS -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
      "$BASE/api/v1/repos/$REPO/issues" \
      -d "$(jq -nc --arg title "<title>" --arg body "<full text>" '{title:$title, body:$body}')"
    
  2. Apply any configured default labels (resolve label names to IDs via GET .../labels, pass labels:[ids]).
  3. Title from the text (summarize if long; full text → body). Save the returned number.

No argument (auto-pick)

  1. GET $BASE/api/v1/repos/$REPO/issues?state=open&type=issues&limit=50 (the type=issues filter excludes PRs).
  2. Prefer an unassigned issue; if priority labels exist (e.g. priority/*), pick the highest, else the oldest open one.
  3. If none found, tell the user and stop.

Then:

  • Gitea has no workflow states — signal WIP by assigning the issue to yourself:
    curl -sS -X PATCH -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
      "$BASE/api/v1/repos/$REPO/issues/$N" -d '{"assignees":["<your-gitea-username>"]}'
    
    Optionally add a status/in-progress label if one already exists in the repo. Don't invent label schemes.
  • Note the issue number and title; derive the branch name (see Worktree setup).

tracker: github

Uses the gh CLI against the origin repo (REPO=<owner>/<repo> from git remote get-url origin). Requires an authenticated gh — run gh auth status first; if it fails, tell the user to run gh auth login -h github.com and stop.

Issue number provided (e.g. 23 or #23)

  1. gh issue view 23 --json number,title,body,comments.
  2. Read the title, body, and comments.

Ad-hoc task description provided (free text, not a bare number)

  1. Create the issue:
    gh issue create --title "<title>" --body "<full text>" [--label <l1> --label <l2>]
    
    Apply any configured default labels (each as a --label flag; only labels that already exist in the repo).
  2. Title from the text (summarize if long; full text → body). Save the returned issue number (parse from the URL gh prints).

No argument (auto-pick)

  1. gh issue list --state open --limit 50 --json number,title,labels,assignees.
  2. Prefer an unassigned issue; if priority labels exist (e.g. priority/*), pick the highest, else the oldest open one.
  3. If none found, tell the user and stop.

Then:

  • GitHub has no workflow states — signal WIP by assigning the issue to yourself:
    gh issue edit $N --add-assignee @me
    
    Optionally add a status/in-progress label if one already exists in the repo. Don't invent label schemes.
  • Note the issue number and title; derive the branch name (see Worktree setup).

Worktree setup

Already in a worktree

Detect by checking git worktree list — if the current working directory is not the main worktree, you're already in one.

If already in a worktree: stay here. Check out the issue branch if the current branch doesn't match.

Not in a worktree

  1. Create a worktree at worktrees/<branch-name> relative to the repo root.
    • tracker: linear: use gitBranchName from Linear (auto-links in Linear). If unavailable, derive a concise name from the issue title.
    • tracker: gitea: derive <issue-number>-<slug> from the issue title (lowercase, hyphenated, ~5 words), e.g. 8-daemon-activity-state.
    • tracker: github: derive <issue-number>-<slug> from the issue title (lowercase, hyphenated, ~5 words), e.g. 12-onchainid-erc3643-fixture.
    • Do NOT include "claude" in branch names.
  2. cd into the worktree.
  3. Run each command in setupCommands from the config.
  4. Set kitty tab title (silently skip if kitty isn't available):
    kitty @ set-tab-title "<repo>/<branch>" 2>/dev/null || true
    

Gather context

  1. Read all contextFiles from the config.
  2. Re-read the tracking issue description (with project context you'll understand it better now).
  3. Read CLAUDE.md / AGENTS.md at the repo root or .claude/ if they exist, for project conventions.
  4. Check recent git history: git log --oneline -20 to understand current patterns.
  5. Locating the code to change — delegate when the area is unfamiliar or spread out. If the issue clearly points at one or two known files, just read them inline. But if you'd otherwise open many files to answer "where does X live / what calls Y / where are the similar cases", spawn the cavecrew-investigator subagent instead (Agent tool, subagent_type: caveman:cavecrew-investigator). It returns a compressed file:line map, keeping the main thread's context lean for the implementation and review that follow. Skip it for small, obvious, single-file tasks — the spawn overhead isn't worth it there.

Implementation guidelines

  1. Work methodically through the requirements.
  2. Commit after each logical step using conventional commits:
    • With scope if configured: feat(scope): description
    • Without: feat: description
    • Prefixes: feat, fix, refactor, test, docs, chore
  3. Follow existing code patterns. Match the style of surrounding code.
  4. Write tests appropriate to the project (match existing test patterns and coverage level).
  5. Never amend commits — always create new ones.
  6. Keep the tracking issue updated (Linear, Gitea, or GitHub, per tracker) if scope changes significantly.

Local verification budget (shared machine)

Several autonomous sessions (/yolo, /nightshift, /work, /blitz) run on one box at once, each with subagents. If every one runs the full suite whenever it feels like it, the machine OOMs and all of them die. If instead everything gets pushed for CI to check, CI is clogged and feedback is slow. So: scoped checks locally, full suite once per push, heavy commands through the gate.

Tiers

When Run How
Inner loop, after each change typecheck + lint + tests for the touched module only direct, no gate
Once, right before push/PR the project's buildCommand (full suite) through gate.sh
Anything else nothing let CI do it

Never run the full suite twice for the same push. Never run it "to be sure" after a green scoped run of the same code.

The gate

Any command that compiles the whole project or runs the whole suite goes through the machine-wide semaphore:

<skills-root>/tracker-common/scripts/gate.sh -- <buildCommand>

It bounds concurrency machine-wide (default nproc/4 slots), caps the command's memory and CPU via a systemd scope, and pins test/build parallelism env vars (CARGO_BUILD_JOBS, RUST_TEST_THREADS, VITEST_MAX_*, MAKEFLAGS, GOMAXPROCS, node heap) so the suite doesn't fan out to every core.

  • Exit 75 = no slot or too little free RAM within the wait budget. It did not run. That is a normal outcome, not an error: push and let CI cover it, and say so in the commit/PR body. Do not retry in a loop, do not bypass the gate by running the command directly.
  • Exit 137 = killed by the memory cap, NOT a test failure. Do not go hunting for a bug that isn't there. Re-run once with AGENT_GATE_MEM_MAX=6G if the suite genuinely needs more.
  • gate.sh --status shows slots busy and free RAM. Cheap; check it before deciding to run anything heavy.
  • If the file isn't executable on this install, prefix with bash.
  • Knobs (env): AGENT_GATE_SLOTS, AGENT_GATE_WAIT (default 600s), AGENT_GATE_MEM_MAX (3G), AGENT_GATE_MEM_FLOOR (2000MB), AGENT_GATE_JOBS.

Subagents

  • Subagents never run the full suite, ever. They run scoped checks on what they touched. The session that dispatched them runs the full suite once, at the end.
  • Cap concurrent subagents at 3 per session, 2 if their tasks build or test. The binding constraint is memory per worktree and the shared rate-limit window, not cores — subagents are mostly idle waiting on the API. gate.sh --status showing no free slots is a signal to dispatch fewer, not to wait.

Pushing

  • One push per finished unit of work. Not per commit, not per milestone.
  • Don't push to trigger CI as a substitute for the local scoped checks — that is how CI gets clogged.
  • When the gate was skipped (exit 75), the push is doing real verification work: arm the one-shot CI watcher so a red build surfaces.

Rules

  • Never ask the user during autonomous work unless you hit a genuine blocker (architectural contradiction, missing credentials, ambiguous requirements that could go very wrong).
  • If the build fails, fix it before pushing.
  • Respect existing project conventions from CLAUDE.md, AGENTS.md, etc.