Skip to content
FIELD NOTE · · · ENGINEERING

Building Waypoint: A Plan Pane for Claude Code

Why I built a Claude Code mod that draws the session's plan, the PRs it touches, and the decisions only I can make as a live pane, and what it took to make a 48-column side panel trustworthy.

The Problem

Most of my significant work in Claude Code starts in plan mode. I described that habit a while ago: I almost never accept the first version of a plan, and once I agree to one, the agent writes it to a file before any code exists. The trouble starts after that.

A long session drifts. The plan file sits in ~/.claude/plans/ with a random slug for a name. The agent works through it, opens a pull request for one step, waits on CI for another, and somewhere in the middle stops to ask me something. An hour later I scroll back through the transcript trying to answer three questions: which step are we on, which PRs belong to this plan and what does each one need, and what is the agent waiting on me for. The answers exist, spread through thousands of lines of tool output and diffs.

The decision questions were the worst of it. An agent that needs me to choose an approach, merge a PR, or supply a credential asks in prose, at the end of a turn, in the same voice as everything else it says. Nothing distinguished “I am blocked on you” from “here is what I did.”

Waypoint is the fix I wanted: a pane, docked beside the session, that draws the plan as progress, lists the PRs the plan touches with what each one needs, and shows a “Waiting on you” note when the agent stops on me. It is a Claude Code mod, written in TypeScript against the mod engine, and the source is at github.com/phatjam98/claude-mods.


A Marketplace First

The first commit was not Waypoint. It was a marketplace: a repository whose .claude-plugin/marketplace.json lists mods, with each mod in its own folder under mods/. Waypoint is the only entry today, but I did not want a repository per mod with conventions drifting between them. One repo means one CLAUDE.md, one CI workflow, and one set of rules every mod has to follow. Installing is two lines:

/plugin marketplace add phatjam98/claude-mods
/plugin install waypoint@claude-mods

The rules in the repo’s CLAUDE.md are mostly about what a mod is allowed to do, because a mod runs inside someone’s coding session with access to their tool calls:

  • Hooks observe only. A mod never denies or rewrites a tool call, prompt, or attachment, unless its README says it is a guard. It may append advisory text the model reads, never change what a tool writes. Waypoint watches gh and glab commands go by; it never changes them.
  • Every $.process.run is wrapped so it never throws, and runs read-only CLIs only. Waypoint’s only external commands are gh pr view --json and glab mr view -F json, through one function that turns every failure into an error class instead of an exception.
  • Render hooks do no I/O and never write state. They read state and draw a model that was computed elsewhere. A render that reaches for the file system or the network is a render that can stall the terminal.
  • Each mod’s README has a “What it can reach” section stating what it reads, observes, runs, stores, and sends. Waypoint’s runs to eighteen bullets, down to the clipboard (only when you press a key) and the fact that it makes no network requests of its own.
  • SemVer per mod. The version in each mod’s plugin.json is mirrored in the marketplace entry, the root changelog has one line per mod release, and tags are per mod, like waypoint--v0.3.0.

CI installs one exact Claude Code version, @anthropic-ai/claude-code@2.1.296, and the workflow file says why: that is the engine API the mods compile against, and it gets bumped on purpose, together with the Requirements line in each mod’s README. Mods are early access, and I would rather CI test against the version I tested than against whatever was published that morning. The workflow runs claude plugin validate --strict on the marketplace and on each mod, then claude plugin test on each mod.


The Plan Is a Contract

For a pane to draw a plan, the plan needs a shape the pane can read and the agent can maintain. Waypoint’s shape is small. A step is a heading whose text starts with a marker:

MarkerMeaning
[ ]pending
[~]in progress
[x]done
[!]blocked on you
[-]skipped

A heading like ### [~] WP2: Remove the old field is step WP2, in progress. The parser takes a dash, a colon, or a period between the label and the title. Child checkboxes (- [ ] lines) under a step become that step’s progress, so the current step can show 2/4 and list its items. Plans written with no markers at all still work when their headings look like steps (### WP1, ## Step 1), and prose sections such as Context, Decisions, and Verification are recognized and left out.

The format only matters if the agent keeps it current, so Waypoint asks. While a plan is tracked, the mod adds a short section to the system prompt: edit a step’s heading to [~] when starting it and [x] when finishing, keep one [~] at a time, and add a - PR: <url> line under a step when it opens a PR. In plan mode, a different section asks the agent to write the markers into the plan from the start. The prompt section is deliberately static for a given plan path and mode: it never includes counts or progress, because a section that changed with every plan edit would invalidate the prompt cache on every turn.

The decision problem got its own channel. Waypoint registers one tool, mcp__waypoint__handoff, and the system-prompt section asks the agent to call it before it stops on me: a two-to-four-sentence summary of where things stand, plus one item per thing I must do, decide, or answer. Each item is a decision, an action, or an answer, flagged blocking when the plan cannot move without it, with an optional PR reference. The note shows in the pane, on a one-line band above the prompt, and on the status line, and it raises a toast. When work resumes, the agent calls the tool with clear: true. The note is stored per plan, so a session that resumes the plan the next day shows it again, marked as left by an earlier session.

The tool records only what the agent passes it (a summary and up to ten items, with lengths capped and control characters stripped); it runs nothing and reads nothing.

FIG. ESSAY-04 / WAYPOINT · PLAN TO PANE
SHIPPED · v0.3.0 · SHEET ESSAY-WPT
INPUTS SHELL · TOUCHES $ PURE CORE · NO $ OUTPUTS INJECTED CONTRACT + PLAN STYLE GUIDE KEEP MARKERS CURRENT · ADD - PR: LINES EDITS MARKERS GH · GLAB BASH · INCL. SUBAGENTS HANDOFF TOOL mcp__waypoint__handoff WRITE · EDIT + ExitPlanMode HOOKS PLAN FILE MTIME POLLED EVERY 5S THIN SHELL register.tsx ONLY on() SITE ONLY FILE TOUCHING $ READ-ONLY RUNS gh pr view --json glab mr view -F json NEVER THROW INJECTS THE CONTRACT VIA prompt.compose PLAN PARSER [ ] [~] [x] [!] [-] STEPS · CHECKBOXES · PR LINES VIEW MODEL WIDE · NARROW · COMPACT PURE FUNCTIONS · NO $ PR REFS + CLASSIFY WHAT EACH PR NEEDS CI FAILING · CHANGES REQUESTED CONFLICTS · READY TO MERGE PANE DOCKED · 48 COLS STEPS · PRS · WAITING ON YOU PROMPT BAND ABOVE THE PROMPT WHEN THE PANE IS NOT PLACED STATUS LINE ONE SHORT SEGMENT 3/9 WP2 · needs you THE MODEL EDITS THE PLAN FILE

Here is roughly what the docked pane looks like at its default 48 columns, partway through a plan:

⚑ Rate limiter for the publ…  WP3 · 2/6 · 33%
███████████████▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░

 ✓ WP1 Token bucket core and tests
 ✓ WP2 Config schema and defaults
 ! WP3 Middleware rollout     2/4  needs you
       ✓ register the middleware
       ✓ per-route overrides
       · load test at twice peak traffic
       · turn the default on
 · WP4 Dashboards and alerts
 · WP5 Docs and changelog
 · WP6 Remove the old throttle

── Pull requests · 2 open · polled 4s ago ─────
 ✗ #41 feat: rate limiter middleware  ci failed
 ○ #40 feat: token bucket core  awaiting review

── Waiting on you · 3m ago ────────────────────
 The middleware is wired behind a flag and #41
 is open, but its load test fails at the
 current default burst size. I need a limit…
 ! decision Default burst: 50 or 100?
 ! action   Review #40 so #41 can rebase    #40

───────────────────────────────────────────────
 ~/.claude/plans/rate-limiter.md
 r refresh · p path · c close

The bar has three spans: finished steps, the current step’s own share, and what remains. It answers “how far along” and “where are we” at once, which a plain percentage does not.


Pure Core, Thin Shell

The mod engine hands every hook a $ object: the process runner, the store, the file system, the UI, the session. The validator that checks a mod attributes $ calls only to top-level functions in hooks/register.tsx. That constraint shaped the whole codebase, and I think for the better.

Everything that touches $ lives in register.tsx, which is also the only file that calls on() to subscribe to events. Everything else is a pure module that never takes $: the plan parser (plan.ts), PR reference extraction (prs/refs.ts), status classification (prs/classify.ts), the polling schedule and store pruning (prs/poll.ts), the view model that decides what the pane shows (view.ts), plan tracking and resume rules (track.ts), handoff validation (handoff.ts), and the plan style guide and its linter (style.ts). The render hook in pane.tsx draws a precomputed model and does no I/O.

The split pays off in tests. The suite is 277 tests across 12 files, and it passes both locally and in CI. Seven of those files test pure modules only, with no engine mocks at all: 149 tests covering the parser, ref extraction, classification, polling, the view model, tracking, and the linter. The view model alone has 46. When a question is “given this plan text and these PR states, at this width, what rows does the pane draw,” the answer is a function call and an expect, not a session.

The remaining files exercise the wiring through the engine’s testing mocks: events into state, discovery precedence, resume, commands, the handoff tool, the lint note and conform, and the pane at different widths.


Not Tracking the Wrong PR

The PR section is only useful if it lists the right PRs. Waypoint discovers them two ways: by observing Bash calls that mention gh or glab, and by reading references out of the plan. The first version was too generous on both, and most of the hardening commits were about that.

The first round came from an independent agent review that I commissioned: a separate agent pass, reporting to me, over the first version. The fix commit reads like a checklist. Only the positional argument after the subcommand counts as a PR number, so flag values and URLs inside flags no longer do. A URL in the command’s output is used only for gh pr create and a bare gh pr view, and only the first one. The store is pruned on every write and stale keys are swept at session start. Merged and closed PRs leave the pane. The same commit is where CI got pinned to a specific Claude Code version. It added fifteen tests, for 141 in total.

The second round came from running the mod on its own development. The commit message records what happened: Waypoint, tracking the plan it was being built from, turned example numbers in the plan’s own prose and a quoted scratch command from a subagent into phantom PR entries. The rules that came out of that:

  • A bare #123 in prose is ignored. Short references (#13, !6677, group/project!6677) count only on a - PR: or - MR: line under a step, or as a markdown link target. Full PR and MR URLs still count anywhere in a step.
  • A --repo value or PR number carrying quotes, commas, $, or braces, which is what a gh call quoted inside a script looks like, yields nothing.
  • An entry the CLI reports as not found three polls running is pruned.

That still left one gap: a reference with a bogus host never reports “not found,” it just fails. So an entry that has never had a successful poll is dropped after five failed polls. The first version of that rule counted every failure, and the next commit narrowed it: authentication, rate-limit, and missing-binary failures say nothing about whether the PR exists, so they do not count toward the five. Without that, a session that started before I logged in to gh could drop PRs it never had a chance to confirm.

None of these is clever. A status pane I have to audit is worse than no pane.


Fitting in 48 Columns

The 0.2.0 release was mostly about layout. A dock beside a coding session has to be narrow enough that the session keeps most of the terminal, so the docked pane asks the engine for 48 columns. That is the paneColumns option, clamped to between 40 and 200, or 0 to take the engine’s default share.

There are three layouts. From 64 columns up, the pane draws the wide layout: full indents, each PR row with its step and copy · untrack buttons, and a one-row footer. From 44 to 63 columns, it keeps the same sections but adapts: one-space indents, a shorter header, PR rows without the step or the buttons, and the footer split across two rows. Below 44 columns, or inline in the transcript, it switches to a compact layout of eight rows at most and no footer.

When rows run short, the order things give up space is explicit. The “Pull requests” and “Waiting on you” sections keep their rows before the step list gives up its own, because those are the parts that need me. First, the current step’s checkboxes fold from one per line back into a single row. Then the sections give up the blank lines between them and the footer rule, then the “+n more” row of handoff items. Last, the sections fold to one line each, the handoff first and then the PRs. On the step list itself, leading finished steps collapse into a single ✓ n done row, and the current step is always shown.

The width itself had a surprise. The engine keeps one dock width for every pane, in ~/.claude.json, and a width you have ever set by hand wins over paneColumns in every session after. The README now says so, with how to resize and reset it. The second issue: the engine’s open call on a pane that is already open only changes its title. A hot reload or a /waypoint command never re-applied the requested width. The fix is a small re-seat step: if a width is configured and the pane is open, close it first, then open it again.


Teaching the Shape

The first plan Waypoint misread after release was the one I used to write this article. It had a ## Context section followed directly by marked steps (### [x] WP1, a dash, then the title), with no ## Work packages heading in between. The parser treated every heading under a prose section as prose, found no steps, fell back to the plan’s seventeen - [ ] lines, and the pane read “0 of 17.” The fix, 0.2.1, is one rule: a marker is the author’s explicit word, so a marked heading is a step wherever it sits. The regression test copies that plan’s shape.

The fix still did not show. After a plugin reload and /waypoint refresh, the pane said 0 of 17, because a refresh saw the file’s content hash unchanged and stopped. In 0.2.2 a forced refresh, and every reload, always re-parses, and the hash carries a parser version so an old parse never compares equal. Both fixes shipped that day.

That bug was the parser being too strict. The bigger problem was plans being too loose: of 21 plans on my machine, 19 had no status markers, so the parser was guessing. 0.3.0 tells the model the shape up front. A one-page Plan Style Guide (a skeleton and four rules, chief among them that every unit of work is a numbered ### heading starting with a marker and that checkbox lines are never steps) is appended to the engine’s full plan-mode reminder. When the model writes or edits a plan file, Waypoint lints it and leaves a short hidden note: what the parser saw and what to fix, from “steps were guessed, not read from markers” down to a step title over 45 characters, which the pane will truncate.

For plans written before the guide, /waypoint lint prints the same report, and /waypoint conform submits an instruction the model carries out with its own Edit tool once the session is idle, showing the diff. By default it is additive: markers on existing step headings, a ## Steps section pointing at existing sections when there are none, no prose deleted or reordered. conform rewrite may also reshape headings. All of it is advisory: Waypoint appends text the model reads and never changes what a tool writes. A styleGuide option, on by default, gates the plan-mode text and the lint note.


Where It Stands

Five releases so far, all in the changelog: 0.1.0 on 2026-10-12, the plan pane with PR and MR tracking; 0.2.0 on 2026-10-13, the redesign and the handoff note; 0.2.1 and 0.2.2 the same day, the parser fixes above; and 0.3.0 on 2026-10-15, the style guide, the lint note, and the two commands. The first two landed in one pull request and each later release in its own, all four merged with CI green; the suite is now 277 tests.

The limits are worth stating plainly:

  • The mod engine is early access. Waypoint requires Claude Code 2.1.296 or later, and the API it compiles against can change. That is why CI is pinned and why the pin moves only on purpose.
  • GitLab support is verified on gitlab.com only. Self-hosted GitLab hosts are untested.
  • Type-checking runs locally, not in CI. The engine’s TypeScript typings are written into the mod the first time it loads in an interactive session, and that folder is gitignored, so a clean CI runner has nothing to type-check against. CI validates and runs the tests; tsc is a local step after each change.
  • It depends on the agent following the prompt. If the agent forgets a marker or skips the handoff tool, the pane is wrong until it catches up. The style guide and lint note narrow that gap, but they still ask; they cannot enforce.

The broader point for me is the same one behind the team Claude Code platform I work on: the agent is far more useful when the structure it works inside is explicit. A plan with markers, a PR line under each step, and a tool for “I need you” are small conventions. Together they turn a long session from something I reconstruct out of the scrollback into something I can read at a glance.