Agent harness for Claude Code: spec-driven lanes, an orchestrator with st-* subagents, commit guard, handoff, and forked design and navigation tools.

An agent harness for Claude Code. Stratum runs every change through a lane sized by its impact: a main session (the orchestrator) plans with you, hands the work to st-* subagents, checks what they return, and stops at a few clear gates for your decision.
Stratum merges two workflows and adds its own layer:
The full design and the reason for each choice are in DESIGN.md.
Stratum is a Claude Code plugin and its own marketplace.
claude plugin marketplace add HyuseCS/stratum
claude plugin install stratum@stratum --scope project
Then, in a new session in your project:
/stratum:st-init
st-init creates the project's .stratum/ files and specs/, grills you for the project's rules (the constitution), sets the statusline and asks for its theme, shape and Token Weather line, turns off a global ponytail or Token Weather plugin for this project, offers to upgrade graphify, installs its post-commit hook, and checks your tools.
| Tool | Used for |
|---|---|
git, python3 | git guard, scripts |
node | ponytail hooks |
bunx (Bun) | statusline bar (falls back to markers only) |
graphify 0.9.74 or later (uv tool install graphifyy) | code navigation graph, Dart support |
gh | optional, for st-issues and st-pr |
Describe the work. Stratum picks the lane:
/stratum:st add a parent notice when the bus is 10 minutes away
It answers with one line, for example Lane: full (privacy and access: new notice to parents). Say full, fast, or quick to override.
The lane follows the highest impact area the change touches.
| Impact area | Examples | Lane |
|---|---|---|
| Privacy and access | access rules, data model, contracts, sign-in, personal data | Full |
| New user story or feature | anything not in a current spec | Full |
| Core runtime | background services, offline queue, workers, notices | Fast |
| Screens | layout, components, navigation | Fast (Quick for a one-file visual fix) |
| Text and docs | labels, typos, wording | Quick |
Full lane: five phases and three gates.
| # | Phase | What happens | Gate |
|---|---|---|---|
| 1 | Define | Grilling, then the spec, then clarify (if needed) and checklist | You agree the spec |
| 2 | Plan | st-plan writes the plan and tasks. Open decisions come to you one at a time. | Asked: make GitHub issues? |
| 3 | Check | st-check checks the files agree, st-validate checks the plan can be built | You OK the build |
| 4 | Build | Per task: failing test, build, check, commit. Per story: review, ponytail review, design review. | None unless blocked |
| 5 | Close | Fix spec drift, optional gap report, proposed lessons | Push only when you say "push" |
Fast lane: a short change plan in specs/<feature>/changes/, your OK, test-first build, ponytail review, commit, drift fix.
Quick lane: one file, no gate, related tests, commit. It moves up a lane if it needs more.
| Skill | What it does | ||||
|---|---|---|---|---|---|
st <task> | Main entry. Picks the lane and runs it. | ||||
st-full, st-fast, st-quick | Force a lane. | ||||
st-define, st-plan, st-check, st-build, st-close | Run or resume one Full-lane phase. | ||||
st-status | Feature, phase, lane, tasks done, git guard options, next gate, missing tools. | ||||
| `st-shape arrow\ | rounded\ | slanted\ | blocks\ | flat` | Set the statusline shape. |
st-theme <name> | Set the color theme of the statusline and Token Weather. | ||||
st-handoff | Write the session handoff. | ||||
st-init | Set up a project. | ||||
st-template <name> | Copy a template into the project to edit it. | ||||
st-constitution | Amend the project rules. | ||||
st-issues | Turn tasks into GitHub issues. | ||||
st-pr [base] | Open a draft PR from git facts and the spec; ready when CI passes. | ||||
st-sync | Maintainers: port upstream changes into Stratum's forks. | ||||
st-grill | Grilling interview, one question at a time. | ||||
st-ui-ux, st-impeccable, st-frontend-design | Design build rules, design review, visual direction. | ||||
st-graphify | Build or query the code graph. | ||||
st-ponytail, st-ponytail-review, -audit, -debt, -gain, -help | Minimal-code mode and its reviews. |
All skills are called as /stratum:<name>.
| Agent | Model | Job |
|---|---|---|
st-plan | Opus | Plan, research, data model, contracts, quickstart, tasks |
st-check | Sonnet | Spec, plan, and tasks agree (read-only) |
st-validate | Opus | Setup, test coverage, breaking changes, security |
st-build | Opus | Builds tasks; loads design rules for screens |
st-test | Sonnet | Writes each test and shows it fails first |
st-review | Opus | Reviews each finished story |
st-debug | Opus | Takes over after 2 failed tries |
st-fast | Sonnet | Fast-lane change plans |
st-quick | Sonnet | Quick-lane edits |
st-close | Sonnet | Drift fixes, gap report, lessons |
st-git | Sonnet | Commits each finished task, exact paths only |
Every agent reads the SR-OPUS-5 communication contract first and finds files graph first, then search, then read. Code-writing agents carry the ponytail rule and add no explanatory comments.
.stratum/
├── constitution.md project rules (committed)
├── state.json current feature, phase, lane (committed)
├── templates/ optional overrides of plugin templates (committed)
├── handoff.md session handoff (git-ignored)
├── powerline.json statusline theme, shape, colors (git-ignored, per machine)
├── git-guard.json git guard modes for this repo (git-ignored, per machine)
└── weather.json Token Weather growth and compact point (git-ignored)
specs/NNN-feature/ spec, plan, research, data model, contracts, quickstart, tasks, changes/
AGENTS.md, CLAUDE.md point every tool at the constitution
Templates and scripts stay in the plugin. A file in .stratum/templates/ overrides the plugin's copy for that project only.
/advisor, the orchestrator consults it at three points: on the plan before the build gate, when a test fails twice, and on the full diff before calling the build done. Its notes are checked against the source like any finding. No advisor, no change.commit, worktree_remove, worktree_prune, branch_delete, reset_hard, clean (clean -f), discard (checkout ., restore .), and force_push. Each is auto (runs), ask, or deny (blocked). The guard reads the repo's .stratum/git-guard.json first (st-init writes it), then the plugin option of the same name from /config or /plugin config, which is global to the machine. The default is ask. A commit that asks or is blocked shows a staged-file summary. Push, rebase, and commit --amend always ask. git config writes, git add -A, git add ., and --no-verify are always blocked. Remote branch delete is blocked: you delete remote branches yourself.st-handoff writes the goal, decisions, open questions, and next step. A hook adds a facts block (branch, phase, tasks done, last commits, uncommitted files) at session end and before compaction. The next session starts by reading it.Stratum, project path, and session length on the left, git branch on the right. Line 2: model with its thinking level, and ponytail on the left, the commit option on the right. Each line fits the terminal width: the path becomes the folder name, other segments get shorter, then drop. The branch and the commit option always stay. See Statusline colors.☂ Showers ━━━━━━━━━━━━──────── 600k/1M about 4 turns left.scripts/st-worktree.sh <branch> <name> makes a sibling worktree that shares this project's local settings and Claude memory. Worktrees and branches stay until you delete them.The defaults are in statusline/powerline.json (rose-pine). To change them for one project, create .stratum/powerline.json. Its keys merge over the defaults. Git ignores it, so each person keeps their own look.
Pick a theme: rose-pine, nord, tokyo-night, gruvbox, dark, or light. All segments and the Token Weather line follow it. Pick a shape: arrow (default), rounded, slanted, blocks, or flat (colored text, no backgrounds). arrow, rounded, and slanted need a Nerd Font. /stratum:st-shape <shape> and /stratum:st-theme <theme> set them for you.
Claude Code draws the statusline a few columns narrower than the terminal and cuts what does not fit with …. Each line leaves reserve columns free (default 8). If the right side is still cut, raise it: { "reserve": 10 }.
To hide the Token Weather line, set { "weather": false }.
The Stratum label starts with a Nerd Font layers icon (). Set "logo" to another character, or to "" for none. The font itself comes from your terminal settings.
{ "theme": "nord", "shape": "rounded" }
Or set your own colors. Keys you leave out keep the rose-pine color.
{
"theme": "custom",
"colors": {
"custom": {
"git": { "bg": "#1f1d2e", "fg": "#9ccfd8" },
"ponytail": { "bg": "#2a273f", "fg": "#eb6f92" },
"stratum": { "bg": "#191724", "fg": "#ebbcba" },
"commit": { "bg": "#1f1d2e", "auto": "#9ccfd8", "ask": "#f6c177", "deny": "#eb6f92" }
}
}
}
Other keys: directory (project folder), model, and metrics (session length). The stratum label uses the model colors unless you set it. Other color options from the claude-powerline docs also work here.
If ponytail, impeccable, ui-ux-pro-max, graphify's skill, or grilling are also installed globally, their hooks or skills can run twice. st-init turns off a global ponytail or Token Weather plugin in the project's .claude/settings.json. st-status lists the rest. Turn the global ones off for projects that use Stratum.
Stratum owns modified copies of its sources. vendor.lock pins each upstream commit. Once a month, run /stratum:st-sync in this repo: it lists upstream changes since the pin, drafts the ports, and waits for review before updating vendor.lock.
python3 tests/test_git_guard.py
bash tests/test_session_hooks.sh
bash tests/test_statusline.sh
node tests/test_token_weather.mjs
claude plugin validate .claude-plugin/plugin.json
Stratum is MIT licensed. It contains modified copies of GitHub Spec Kit, vibecode-pro-max-kit, ponytail, mattpocock/skills, ui-ux-pro-max, impeccable, frontend-design, graphify's skill, Token Weather, and the theme colors of claude-powerline, each under its own MIT or Apache-2.0 license. See THIRD_PARTY_NOTICES.md and licenses/.
hooks/token-weather.mjs 86 lines1// Copyright 2026 Anthropic PBC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Token Weather: a live forecast of the context window, drawn by the
5// statusline as its third line (statusline/st-statusline.sh).
6//
7// turn.complete: after each main-loop turn, read the context window's fill
8// from $.session.usage() (the same figures the status line shows) and keep
9// the last HISTORY readings.
10// session.start: take a first reading, so the line shows before any turn.
11// Each reading writes .stratum/weather.json: the auto-compaction threshold
12// and the mean growth of the recent growing turns. The statusline takes the
13// fill from its own input; it cannot ask for the threshold or keep a history.
14//
15// The host reads on(...) and $.noun.method(...) from source, so they are
16// spelled literally, and helpers that take $ are top-level functions.
17
18const HISTORY = 12;
19const GROWTH_TURNS = 5;
20const STATE_FILE = ".stratum/weather.json";
21
22// Readings: { tokens, window, percent, compactAt }, oldest first.
23let readings = [];
24
25export function register(on) {
26 on("session.start", async ($, e, next) => {
27 const result = await next(e);
28 readings = [];
29 await takeReading($);
30 return result;
31 });
32
33 on("turn.complete", async ($, e, next) => {
34 const result = await next(e);
35 if (e.agentId) {
36 return result;
37 }
38 await takeReading($);
39 return result;
40 });
41}
42
43async function takeReading($) {
44 try {
45 if (!(await $.fs.exists(".stratum"))) {
46 return;
47 }
48 const { context } = await $.session.usage();
49 if (!context || !context.window) {
50 return;
51 }
52 const tokens = context.tokens ?? 0;
53 const percent = Math.round(context.percent ?? (tokens / context.window) * 100);
54 const compactAt = await compactThreshold($, context.window);
55 // The session.start reading is 0 before any response; drop it once real readings arrive.
56 readings = readings.filter((r) => r.tokens > 0);
57 readings.push({ tokens, window: context.window, percent, compactAt });
58 if (readings.length > HISTORY) {
59 readings = readings.slice(-HISTORY);
60 }
61 await $.fs.write(STATE_FILE, JSON.stringify({ compactAt, growth: growth() }) + "\n");
62 } catch {
63 // No reading this turn; the file keeps the last one.
64 }
65}
66
67async function compactThreshold($, window) {
68 try {
69 const { context } = await $.session.usage({ breakdown: "summary" });
70 const b = context.breakdown;
71 if (b?.isAutoCompactEnabled && b.autoCompactThreshold) {
72 return b.autoCompactThreshold;
73 }
74 } catch {
75 // No breakdown; count turns to a full window instead.
76 }
77 return window;
78}
79
80// Mean growth of the recent turns that grew; null until there is one.
81function growth() {
82 const recent = readings.slice(-(GROWTH_TURNS + 1));
83 const grew = recent.slice(1).map((r, i) => r.tokens - recent[i].tokens).filter((d) => d > 0);
84 return grew.length ? Math.round(grew.reduce((a, b) => a + b, 0) / grew.length) : null;
85}
86