I type /commit after finishing a feature. Claude scans the diff, counts eight changed files across two directories, checks that none of them touch auth or migrations, classifies the review as LIGHT, dispatches a code reviewer and a UI consistency checker in parallel on Sonnet, runs a three-agent simplify pass that catches a redundant API call, generates a commit message referencing the Linear ticket, pushes to origin, posts a threaded progress update under the implementation plan comment, applies an auto-detected frontend label, and sets the status to In Progress.
One command. Twelve steps. A review pipeline that would take me twenty minutes runs in about forty seconds.
/commit is 1,170 lines of markdown. Like /start
, it’s not a script - it’s a structured decision tree that Claude reads and executes. And like /start, every rule in it exists because something went wrong.
The Three-Level Review Triage
The first version of /commit ran a full code review on every commit. Five parallel agents analyzing every diff, even when the only change was a CSS color value. It was thorough and spectacularly wasteful.
The fix was triage. /commit now classifies every commit into one of three levels based on what actually changed:
NONE → Only docs, tests, config, CSS. No review agents. Just commit.
LIGHT → Source code changed, under 10 files. Code reviewer + selective agents.
FULL → 10+ files, shared packages, or sensitive paths. Full agent battery.
The classification isn’t a guess. Claude runs two bash commands in parallel - one counts files and lines, the other checks every file path against a set of pattern matchers:
=== Critical Files === middleware.ts, auth.ts, /auth/
=== Security Paths === payment, billing, webhook
=== Platform Packages === packages/*
=== Migrations === supabase/migrations/
Any hit on a critical path forces FULL review regardless of file count. A single-line change to middleware.ts gets the same scrutiny as a twenty-file feature.
The rule that matters most is that NONE never applies to source code. Even a one-file .tsx change gets at least LIGHT review. I added this after a “small” prop rename broke a component in production. The change looked trivial - rename isOpen to isVisible - but the prop was used in three other files that weren’t updated. A LIGHT review would have caught the missing references in seconds.
Signals are evaluated in priority order. Path-based overrides win over everything else:
1. Path-based override? (middleware, auth, migrations, payments, packages)
→ YES → FULL (regardless of file count)
2. All files non-source? (only .md, .css, .test.ts, config)
→ YES → NONE (no agents needed)
3. package.json changed? (NOT exempt - supply chain risk)
→ YES → At least LIGHT
4. File count under 10?
→ YES → LIGHT
→ NO → FULL
5. Over 200 lines changed?
→ YES → Bump one level up (LIGHT → FULL)
After determining the level, content-based signals select which agents run. .tsx files add a UI consistency reviewer. API routes and custom hooks add a silent-failure-hunter. Auth and migration changes add a security auditor at FULL level.
Agents That Fix vs Agents That Report
Before the review agents see the diff, a simplify pass runs. This is three agents dispatched in parallel - a code reuse checker, a code quality checker, and an efficiency checker. They look for different failure modes.
The reuse agent searches the existing codebase for utilities that could replace newly written code. I wrote a custom formatDate() helper in a component and the reuse agent pointed out that @platform/ui already exports one with identical behavior.
The quality agent catches redundant state, copy-paste with slight variation, and parameter sprawl. It found a component that accepted eight props when four of them could be derived from the other four.
The efficiency agent looks for unnecessary work - redundant computations, duplicate API calls, N+1 patterns, and independent operations that run sequentially when they could be parallel. It caught an action that fetched user data, then fetched the same user data again two functions deep.
The key distinction is that simplify agents fix the code. Review agents report on it. The simplify pass edits files directly, re-stages them, and proceeds. The review agents produce findings and a verdict - pass, warn, or fail - that determines whether the commit goes through. I separated these because the review agents were generating reports that said “you should extract this utility” but never actually doing it. The reports were accurate and completely ignored. Now the fixable stuff gets fixed before review, and the review agents focus on things that require human judgment - architectural decisions, security patterns, spec compliance.
The sequence is deliberate:
Step 2: Quality gates (type-check, lint)
Step 2.5: Simplify pass (3 parallel agents → fix issues → re-stage)
Step 3: Review triage (classify as NONE/LIGHT/FULL)
Step 4: Review dispatch (parallel agents → findings → verdict)
Step 4.1: Synthesis (merge agent findings → single pass/warn/fail)
Step 6: Stage and commit
Simplify runs before review because it changes the diff. If simplify extracts a utility, the review agents see the cleaner version. If review ran first, its findings would reference code that no longer exists after simplify fixed it.
The synthesis step (4.1) exists because multiple agents can disagree. The code reviewer might say PASS while the silent-failure-hunter says FAIL on a swallowed error. Synthesis produces a single verdict from the combined findings, deduplicates overlapping issues, and applies any review overrides from .claude/review-overrides.json - a file where I can suppress known false positives without editing agent prompts.
The Change Relevance Problem
I was on branch feature/INT-28-waitlist-entries building a waitlist feature. Partway through, I noticed some stale Claude command files and cleaned them up. I ran /commit. Claude staged everything - the waitlist code and the unrelated command file cleanup - and committed it all under the INT-28 ticket.
The Linear ticket now had a progress comment about changes to .claude/commands/ files that had nothing to do with waitlist entries. The git history for the ticket included commits with unrelated cleanup. It wasn’t harmful, but it made the history harder to follow.
Now /commit has a change relevance check at Step 5.75. After extracting the ticket ID from the branch name, it compares the changed files against the ticket’s purpose:
Changes appear related to ticket?
├─ YES → Continue to Step 6
├─ UNCLEAR → Ask user to confirm
└─ NO → Prompt with options
Red flags include .claude/ changes on an app feature ticket, different app directories than the ticket prefix suggests (an AUTM ticket but only medicaremagic/ changes), and config-only changes on an implementation ticket.
When unrelated changes are detected, the prompt gives three options: create a separate branch for the unrelated work, continue on the current branch anyway, or cancel and review what to commit. I almost always pick “create a separate branch” - it takes five seconds and keeps the git history clean.
There’s a complementary check at Step 5.5 - branch/ticket mismatch detection. If the session file says I’m working on AUTM-677 but I’m on branch feature/INT-28-waitlist-entries, that’s almost certainly a mistake. This catches the scenario where I switch worktrees, forget I’m in the wrong one, and try to commit. The mismatch prompt saved me from committing IntensityMagic changes to an AuthorMagic ticket at least three times.
Chain Mode: Multi-Ticket Commits
/start-chain INT-366 INT-367 INT-368 INT-369 kicks off a chain of related tickets. Claude works through them sequentially - implement, test, commit, advance to the next ticket. The /commit command needs to know when it’s inside a chain because the behavior changes in specific ways.
Chain detection happens in Step 0.5. /commit checks for a chain-state.json file and verifies that the current ticket matches the chain’s current index:
chain-state.json exists AND current ticket matches
chain.tickets[chain.currentIndex]?
├─ YES → Set IN_CHAIN = true
│ Display: "Chain mode detected (ticket 2/4)"
└─ NO → Set IN_CHAIN = false (standard commit)
When IN_CHAIN is true, three things change. The batch learning capture (Step 1.5) is skipped because chain commits happen rapidly and capturing after each one is noisy - learnings get captured at the end of the chain instead. The success output is abbreviated - no “next steps” section, no deploy hints, just the commit SHA and a “returning to chain orchestrator” message. And the chain-state.json is updated with the commit SHA and status for the completed ticket.
Everything else stays identical. Quality gates run. Simplify runs. Review triage runs at the appropriate level. I was tempted to skip reviews for chain commits because they happen in rapid succession and the context pressure builds - but that’s exactly when shortcuts cause problems. A chain of four tickets means four separate feature implementations, and each one deserves the same scrutiny as a standalone commit.
The tricky part is cross-repo chains. If /start-chain was invoked in magic3 but one of the tickets routes to ~/Code/companyos-intensitymagic, the chain-state.json lives in magic3 while the actual work happens in companyos. The per-ticket session file stores a chainInvokingDir field that points back to magic3:
Session file has chainInvokingDir set?
├─ YES → CHAIN_STATE_DIR = chainInvokingDir
│ Check: ls "$CHAIN_STATE_DIR/.claude-session/chain-state.json"
└─ NO → CHAIN_STATE_DIR = (current directory)
Check: ls .claude-session/chain-state.json
One important guardrail: /commit updates the per-ticket entry in chain-state.json (status, commitSha, branch) but does not update the summary counts. The chain orchestrator in /start owns the summary tracking and reads the updated ticket status after /commit returns. If both sides incremented summary.completed, the count would be wrong.
One Command, Twelve Repositories
/commit works in every repository I use - Magic Platform, CompanyOS, MagicEA, Freshell, Adventures in Claude, and seven more. Each has different conventions for branching, quality gates, review levels, and deployment. The first version of /commit was written for Magic Platform only. When I tried to use it in CompanyOS, it complained about not being on a feature branch (CompanyOS uses main) and tried to run pnpm run type-check (CompanyOS uses bash scripts/validate.sh).
The fix was the same one /start uses: Workflow Profiles. /commit detects the project from the working directory, reads the profile from that project’s CLAUDE.md, and adapts every step. The detection is a simple prefix match:
Working directory starts with ~/Code/magicea?
→ PROJECT = "magicea"
Working directory starts with ~/Code/content/aic?
→ PROJECT = "adventuresinclaude"
Working directory starts with ~/Code/magic*?
→ PROJECT = "magic-platform"
The profile drives everything downstream. Branch protection: direct_to_main is true for Adventures in Claude, so committing on main is allowed. Quality gates: Magic Platform runs type-check and lint, CompanyOS runs a single validation script, Adventures in Claude runs nothing. Review triage: Magic Platform can go up to FULL with five parallel agents, non-platform projects cap at LIGHT with a single code reviewer. Ship method: Magic Platform pushes and defers PR creation to /staging, MagicEA creates a PR immediately, Adventures in Claude just pushes.
Linear integration adapts too. The status update uses profile.ship.linear_status - “In Progress” for pipeline repos where the commit is a checkpoint, “Done” for direct-to-main repos where the commit is the final step. The progress comment format changes: pipeline repos say “Ready for staging deployment via /staging,” direct-to-main repos say “Committed to main and pushed.”
The final success message is entirely templated from the profile:
[HEADER - choose one:]
PR created: Committed and PR created!
direct_to_main: Done -- TICKET-XXX committed and marked Done
all others: Work committed for TICKET-XXX
[REVIEW - pipeline repos only:]
NONE: Review: Skipped (non-source only)
LIGHT: Review: LIGHT: code-reviewer PASS, ui-consistency-reviewer PASS
FULL: Spec Review: PASS (attempt 1/3)
[CORE - all templates:]
Branch: feature/INT-391-overlay-cleanup
Commit: abc1234 - feat: add overlay cleanup
[SHIP - choose one:]
PR created: PR: https://github.com/...
direct_to_main: Pushed: origin/main
all others: Pushed: origin/feature/INT-391-overlay-cleanup
[ALL:]
Linear: Status -> In Progress, comment added
[NEXT STEPS - most specific match wins:]
pipeline: - Deploy to staging: run /staging from magic0
PR repos: - Review and merge the PR
all others: - [deploy_hint from profile]
Adding a new project means writing a Workflow Profile. No changes to /commit itself. The same markdown file runs the same algorithm across twelve repositories, producing twelve different behaviors.
Auto-Labels and Threaded Comments
Two small features in /commit that I use constantly and almost didn’t build.
Auto-labeling detects what area of the codebase changed and applies Linear labels. .tsx and .css files get frontend. API routes and services get backend. Migrations get database. The detection runs on path patterns - simple grep checks against the file list. The labels are then merged with existing labels on the ticket, because Linear’s save_issue replaces labels rather than appending them. That gotcha cost me an afternoon of debugging silent label drops before I figured out the merge-first pattern.
Threaded comments were added because Linear tickets accumulate noise. Every /start posts an implementation plan comment. Every /commit posts a progress update. If I commit three times during a feature, the ticket has four top-level comments (plan plus three updates) and scanning for the actual discussion becomes tedious.
Now /commit checks for an existing “Implementation Plan” comment posted by /start. If it finds one, the progress update is posted as a reply threaded under it. The ticket timeline shows one expandable thread for all the automated activity, keeping top-level comments clean for human discussion.
const planComment = comments.find(
c => c.body.includes("## Implementation Plan")
);
mcp__linear__save_comment({
issueId: "<uuid>",
body: progressUpdate,
...(planComment ? { parentId: planComment.id } : {})
});
Both features are profile-gated. Auto-labeling only runs when profile.labels.auto_detect is true. Threading only happens when a plan comment exists. Neither is visible unless you go looking for it - they just make the project management side of development a little less noisy.
The Pattern: Separation of Concerns in Markdown
/start and /commit are two halves of a workflow. /start goes from ticket to implementation - fetch, plan, branch, code. /commit goes from implementation to ship - review, commit, push, update. They share session state through JSON files and share project configuration through Workflow Profiles, but neither knows the other’s internal logic.
This separation came from trying to put everything in one file. A 2,500-line /start that also handled committing was unmanageable - not because Claude couldn’t read it, but because every change to the review pipeline risked breaking the planning logic. Splitting them made each file independently iterable. I’ve rewritten the review triage three times without touching /start at all.
The integration contract is simple. /start creates a session file and a plan file. /commit reads them, updates the session status, and cleans up when done. If /start adds a new field to the session file, /commit ignores it until it has a reason to read it. If /commit adds a new review level, /start doesn’t need to know. They communicate through files with stable schemas - the same approach that makes Unix pipes composable.
The markdown-as-state-machine pattern from yesterday’s post
is the same one at work here. Decision trees, not prose. State on disk, not in memory. Step numbers, not transitions. The only difference is what the machine does - /start orchestrates the beginning of work, /commit orchestrates the end of it.
Subscribe via RSS to follow along. The source is always on GitHub .