One line above the prompt with where the agent is working now and on which branch, as two chips: the worktree or repository with the path beneath it, and the…

Personal collection of Claude Code and Codex skills.
Each top-level folder containing a SKILL.md is one skill. This repo is the single source of truth; skills become active by linking them into each agent's skill directory, where Claude Code and Codex discover them.
| Skill | What it does |
|---|---|
thegraph-codex | Codex-native, explicitly invoked version of thegraph's end-to-end discipline: establish the task from repository guidance and primary sources, make routine implementation decisions autonomously, ask only for consequential product or scope decisions, prove behavioural tests can fail, then run verification proportionate to the changed surface. It does not require Claude Code platform skills or create external tracker records without an explicit request. |
to-html | Convert a markdown file — or content already in the conversation — into a single self-contained, styled HTML file: inline CSS, sticky table of contents, callouts, premium layout blocks. No build step. |
brief | Explain the work in flight to the person who has to decide about it — state, the next action, what it changes and what it leaves alone, and what they must decide (including "nothing", which is information). Publishes a one-screen Artifact with its own briefing layout; falls back to chat when there is no structure worth laying out. Status claims are queried with gh/git rather than copied out of the conversation, and measured facts are marked apart from inferred ones. Vocabulary is governed by a structural check — every noun must point at something the user can see or do — not by a banned-word list, because the words that lose someone next are never the ones already on the list. A view, never a source: nothing may exist only in a briefing. Invoked only (/brief, /brief <issue>, /brief session) — judging whether someone understands you is not yours to do. |
gate | The Acceptance Gate — review an AFK agent's finished work on an issue, render a verdict (Pass / Rework / Respec / Escalate) with the matching next action, and run it on the tracker once you approve. Delegates the diff review to the code-review skill. |
to-deck | Retired — no longer installed; moved to retired/, see ADR-0059 and ADR-0027. Turned a study PDF into a reusable JSON question bank through an interactive Korean Q&A session: keeps the workbook's own questions in order (origin: workbook) and adds generated extras (origin: extra) — a mix of explanatory and single-term recall questions — each a plain {question, answer} pair. Offers a one-at-a-time session or batch extraction; no question-type labels — a downstream tool classifies them. |
grill-code | Retired — no longer installed; moved to retired/, see ADR-0058 and ADR-0027. Grilled code along one chosen mode — security, common-component extraction, refactoring, performance, or learning — scanning the scope relentlessly. Defect modes score each finding on severity + effort and rank them by priority (P0–P3); the non-defect learning mode explains AI-written code as Lessons ranked by Value. Report only; standalone (no dependency on code-review/audit). Can save the report as HTML via to-html. |
img-to-pdf | Combine a folder of images (or an explicit file list) into one PDF, one image per page, in natural-sort order (2.jpg before 10.jpg). Default: a searchable PDF — the page image plus an invisible, selectable/searchable text layer via macOS Vision OCR (Korean + English). --image-only instead embeds JPEG/PNG losslessly with no text layer (HEIC/HEIF/WEBP pre-converted via sips; img2pdf auto-installed into a cached venv). OCR mode is macOS-only; the Swift helper compiles once and caches. |
theflow | Retired — no longer installed; moved to retired/, see ADR-0027. The seven-step working discipline for a substantive change to a boundary-defined codebase, succeeded by thegraph which carries every rule it held (ADR-0019). Kept readable because the docs/agents/theflow.md bindings it read still exist in the repos it onboarded — nothing reads them now, grill-the-graph's path for compiling one having gone with ADR-0067. |
grill-the-flow | Retired — no longer installed; moved to retired/, see ADR-0027. The setup half of theflow — it interrogated a maintainer to author the repo's docs/agents/theflow.md bindings. Succeeded by grill-the-graph, which asks a much smaller question and writes a much smaller file. Nothing reads the bindings docs it wrote, that path having gone with ADR-0067. |
read-it | Read the situation before touching anything, and come back with a route. The first act of a change and the only one whose mistakes nothing downstream catches — a wrong proof method fails at the proof and a wrong placement fails at the diff, while a wrong reading of the issue passes everything, a run that solves the wrong problem well having still solved something well. Four reads, three of them conditional on something you check rather than weigh: the build (short, and answering little on purpose); the cluster, where one tracker query finds a parent (spine — the anchor's root is a hypothesis to test); the territory map where the repo keeps one; and always the real sources plus the hidden state (firsthand, one reflex rather than two — the source list widens what you read, it never decides whether you read, and with no outside source named you still enumerate the hidden state of the code you are touching). Doing one of the four and doing all four are both ordinary; what is not is skipping one and being unable to say why. Then restates the issue in its own words and stops to ask — the only guaranteed stop, since every later one is conditional and a run where nothing goes wrong first reaches a person with the work already done and nowhere to have redirected it. Returns a route label, never a skill name, so what runs next stays the caller's to decide. Everything it finds is a candidate; nothing reaches a tracker unasked. |
make-it | Write the change. Calls tdd for the tests and redden after every one of them — a green from a test nobody watched fail is not evidence, which is the one thing tdd does not do. Keeps the narrow exception where compilation itself enforces completeness and a round-trip test replaces strict RED, and the rhythm a whole-suite-at-the-end habit loses: typecheck and the nearby tests often, everything once at the end. Prose changes walk it too, with the test section conditional. Carries the rule that a comment says what the code is, and follows the repo's CLAUDE.md for where the rest goes rather than holding its own copy — comments are written on every edit, and most edits never pass through make-it. Its substance is the four signals that only appear while your hands are in the code, since you cannot schedule the moment you will feel one: "this is a bit odd, I'll just fix it up here" — the tell that the problem is one step further in, tempting because it works and costs nothing visible, until the day somebody fixes the real thing and every patch like it becomes silently wrong; a new file with no obvious home; writing against an outside reference; and a call that is the maintainer's. What you notice and do not fix is carried, never filed. |
check-it | Measure a finished change before anyone calls it done — the hand that shaped a thing is the worst judge of whether it is finished. Eight checks, ordered by cost rather than ceremony, each conditional: the gates bare (bare), whether new or moved files landed where the layout rule said, proof against something real rather than a fake, the diff read for shape (assay), for cost (silt) and for exposure (security-review) at the same time, the whole corpus read for what is missing (lens), and the surfaces that describe the behaviour (sweep). A prose change runs the last two and nothing else. One question runs through all of it — a red, a defect, a defensive line: is it mine? Answered by re-running against the tree without this change and comparing the failure, never the exit code, since a baseline red for its own reason makes every red compare 1 to 1 and inverts the attribution silently. And one shape is hunted assuming the writer left no help at all, because whoever did not notice could not have written it down: for each defensive line, what would have to be true for this to be unnecessary? — if the answer names something you did not change, that is where it belongs. Fixes nothing; measures, attributes, hands on. |
pub-ready | Bring a Dart or Flutter package to the point where only pub publish is left — the maintainer's release bar, held once instead of in each package's CLAUDE.md, where it had been copied into at least seven packages and drifted (ADR-0086). Reads what pub.dev already has first, raw from its API, since that settles whether the version is new, which CHANGELOG entries are frozen and what changed since the last release. Then pubspec (version size against the diff, the 0.x caret trap, the SDK floor, fields checked by pana itself rather than a copied limit), CHANGELOG (an entry per behaviour change, a published one never rewritten), README (every API name it shows grepped in lib/), the CI steps plus the gates CI does not reach — pub get before dart format, the example, the dry-run at 0 warnings on a clean tree with its file list read — all run bare. Never publishes: it ends with the command written out for the maintainer. A package's own guidance adds to it and, where they differ, wins. Runs only when a release is being prepared, in a repo that chose it: grill-the-graph asks, recommending it for an open-source package, and records pub-ready or None. in the build, which this reads first. |
decant | Sort source comments paragraph by paragraph under one policy: a comment says what the code is, and the why, the trap, the measured value and the history move to the repo's map. Six bins; the unit is the paragraph; the drop question is asked last, because a wrong move costs a lookup and a wrong drop costs a measurement. Checks every DESIGN against the code and then its note, and reports what the map is missing (notes owed) and where note and comment give different reasons (two grounds). Runs on a whole repository, territory by territory in an order derived by counting, when a maintainer asks — never per change, because a check after the fact is undone by the next edit made outside it; the write-time rule lives in the repo's CLAUDE.md. Reports and stops — unless the maintainer hands it the writes, and then it moves sentences rather than rewriting them, accounts for every one, and re-points whatever quoted them. The measurements behind it are ADR-0076. In a repository you contribute to, upstream's guidance and closest merged PR replace that policy, and only the comments the branch adds or changes are read (ADR-0079). |
winnow | Cut an always-loaded agent document — CLAUDE.md, AGENTS.md, a skill body — down to what every run needs. Five bins per paragraph: rule stays, detail moves behind a pointer that names its trigger, number stays only where a decision turns on it, roster becomes the command that derives it, history leaves for version control. History is asked last, and an incident story has its rule extracted before the story goes. Shows the table — every history part quoted, not counted — and stops for the maintainer; on approval writes, then proves every rule still has a home by reading the original from version control, and repoints references to moved sections. The case it was cut from is ADR-0077. |
assay | Test what a change is made of, along two axes kept apart because a change can pass one and fail the other: does the code hold up, and does it do what was asked. The first carries this repo's own documented standards — which always outrank it — plus Fowler's twelve code smells as labelled heuristics, never hard violations, covering cohesion (Divergent Change), coupling (Feature Envy, Shotgun Surgery, Message Chains) and duplication (Duplicated Code, Data Clumps, Primitive Obsession); and it opens the module the change landed in, since "edited for several unrelated reasons" is a claim about a module and a diff shows one slice of it. The second asks what the issue wanted and got, what it wanted and did not, and what arrived that nobody asked for. Findings are graded violation / judgement / already-decided — the third is why the records get read, since a finding that contradicts a recorded decision re-opens a call somebody made. Never merged, never ranked across axes, one closing line per axis. |
silt | Read a change for what will make it slow, and refuse to call anything a defect without a number. Silt is what a river drops: nothing dramatic, nothing wrong on any given day, and one season later the channel is shallow — which is the shape of the performance defects that actually cost, since the ones that announce themselves get found. Carries seven shapes with, for each, the cheapest way to find out whether it is real here: registered and never released, a container that only grows, I/O inside a loop, a large structure copied by value, recomputed every time, blocking the thread that must not block, a lookup with nothing to make it fast. A suspicion arrives with the way to settle it or it does not go out at all — that bar is what stops this becoming a list of things that make somebody uneasy. And a number with nothing beside it is not a number: measure the same thing on the tree without the change. Proposes no optimisation, does not widen, and does not accept "it's fine, it's small" — if it is small the measurement is cheap. |
ask-it | Put everything that has been carried in front of a person, once, and file only what they keep — the one door, since nothing may evaporate into a pull-request body or a code comment, where it is buried on merge. envelope owns the collecting and the seven columns; what this adds is that the items arrive from different places with dispositions that share no vocabulary — something to file, something to decide, something belonging to another repository, something a release will oblige — and interleaved, the reader classifies before answering, which is the sorting that presenting one at a time was bought to remove. byartifact searches before anything is opened, and only for items actually being filed. A follow-up is parented through the tracker's own relation in the same act as filing it, and whether it also joins the cluster is a separate judgement: provenance and root are different claims, and the error is not symmetric — missing a sibling costs a reconstruction, counting an adjacent one corrupts the roster that somebody later copies almost verbatim. A release's obligations are the consumer tests that actually broke, named; a purely additive release obliges nothing, and that is an answer worth writing. |
thegraph | Take one change from an issue to done, in a codebase whose identity is a boundary — a core that stays correct by not absorbing the concerns of the things that consume it. Four skills in order — read-it → make-it → check-it → ask-it — with one guaranteed stop before any of them run: the reading of the issue, confirmed by the person who holds it, since a change where no signal fires otherwise reaches a person for the first time at the very end, with the work done and nowhere to have redirected it. Two routes after that, and the one that gets missed is expensive: an issue asking for a choice gets the adversarial read over the options before any proposal, or the enumeration lands after approval and its costs get demoted into follow-ups. Everything else is a signal you cannot schedule — the urge to patch around something one step further in, a test just written, an outside reference about to be relied on, a call that is the maintainer's — acted on where it arrives rather than noted for later. What holds throughout: nothing reaches a tracker unasked; what can be counted is counted; a derivation falls to a better derivation and a judgement falls only to the person who made it, so a record that does not say which it holds reads as the first. Human-invoked (/thegraph). |
grill-the-graph | The setup half of thegraph. Writes one short prose file, docs/agents/thegraph.md, holding what a person already knows and no file in the repo answers — which is very nearly just the real sources the project is built against, each marked how it works or where files go, raw or summarized, an example to follow or a spec that binds. A line earns its place only by clearing three conditions together: no file here answers it, a person already knows it, and something reads it. The second is the one that used to be missing — "only a person could decide it" and "a person already knows it" are different sets, and everything this skill once asked for (sacred paths, the tie-breaker, the seams, a proof method per layer) cleared the first and failed the second, so the answers came back drafted by the builder and ratified by someone who could not have produced them. Generates no scripts and no agents, carries no build stamp, and copies nothing the repo already states; the build grows as runs ask for what it lacks. Run again, it updates: where docs/agents/thegraph.md exists it keeps the build, asks nothing already answered, and brings what it installed — the comment rule, the map hub, the attribution pieces — and the lines pointing at them up to its current text; /grill-the-graph init rebuilds from scratch instead, written only on the maintainer's approval (ADR-0085). An older generated build — scripts/thegraph/, .claude/agents/thegraph-* — is handed to salvage rather than read or updated. A project built against nothing outside itself yet answers none yet, and the file records that the question was put rather than inventing a row. Where the repo has no territory map, it makes the map's folder with a one-line hub instead of building one — the rule needs a destination on disk, and an empty map is a young repo's honest state; notes arrive as edits need them or all at once through grill-map. Then it writes the comment rule into the repo's CLAUDE.md: a comment says what the code is, with the rest sent to the map, the decision records or the commit message, naming only destinations that exist on disk. That is the one place the rule is written, because comments are written on every edit and a pass that checks them afterwards is undone by the next one. In a repository you contribute to rather than own — named by the argument (/grill-the-graph local, or own for your own), and asked first when there is none, with viewerPermission on the receiving repository as the recommendation — it commits nothing: the same file and a rule deferring to upstream's own guidance go in untracked, excluded files, no map is made, and a hook blocks Claude attribution on the way out (ADR-0079). In your own repository attribution stays unless you turn it off when asked, and off installs the same check, locally. In your own repository holding a Dart package for pub.dev, it asks whether pub-ready is the release bar — recommending it when the package is open source — records the answer, and on a yes offers to replace each older copy of the bar with a pointer, keeping what only that package has (ADR-0086). |
salvage | Clear an older grill-the-graph build out of a repo without losing the answers a person gave it. The whole job is telling two things apart: what somebody sat down and answered, which has to survive, and what was copied out of the repository, which is why that build went. Finds what is ours by the build stamp every generated file carries and by the build document's own manifest, and reads both — a stamped file the manifest does not name is the case that matters, the manifest being a copy like any other. Four rings: ours (build doc, generated agents and scripts, the ignore line, and the run-state cache version control never shows you); somebody else's file pointing at ours, where only the line comes out — a repo left with a hook invoking a deleted script cannot commit, so clearing the first ring and reporting the second is a break with a note attached; produced by our rules but not written by us, shown and asked, and any generated file a later commit touched, because that made it theirs; and outside, untouched — decision records describe the world when they were written, broken links included. Lifts the reference list into the short file the current skill defines, drops the rest with a reason each, and re-reads the tree afterwards, since a cleanup reporting success without looking again has checked nothing. |
tickets | Retired — no longer installed; moved to retired/, see ADR-0069 and ADR-0027. Put back what slicing dropped — the third producer in the thegraph family. A general slicer keeps a ticket thin because it cannot know who will execute it; here the executor was known, so the calls a maintainer had already made travelled with the ticket instead of staying behind in the spec. Ran right after the tickets were cut, in the same unbroken window as the grilling: transcribed the settled calls into each ticket, enrolled the parent as the relation the tracker actually renders (which is still why spine's roster is a query and not a body parse), and marked each entry human / derived / unknown. It read thegraph's "What the issue must supply" at runtime rather than hardcoding it, and that is the one thing it demonstrably bought: the schema went from five slots to six and the producer needed zero edits. Retired on the maintainer's call, with the falsification ADR-0045 had written in advance already fired — on the only run anyone measured, every contract entry was marked derived, which licenses nothing. The schema it filled is live; thegraph reads a contract wherever one is written, and a ticket that answers none of it was always the ordinary case. |
| spine | Read the cluster before the ticket, extracted from
hooks/register.tsx 124 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Head } from '../types'
5import { fit, gitdirOf, headOf, locationOf, parentOf, sepOf } from './where'
6
7const where = atom({ plugin: 'path-band', key: 'where' } as const, null)
8
9/** Blank cells at the line's left and right, as usage-band keeps, so the bands share their edges. */
10const LEFT = 2
11const RIGHT = 1
12/** A checkout made outside the session (another terminal) shows within this. */
13const TICK_MS = 30_000
14/** Inside the hook of the call that ran a `cd`, the session's directory still reads the old one; it is read again this much later. */
15const SETTLE_MS = 1_000
16
17// The folder the line was last worked out for, and the working tree found above it.
18let dirSeen: string | null = null
19let top: string | null = null
20let headPath: string | null = null
21// HEAD's modification time when last read, so an unchanged HEAD is not read again.
22let headSeen = -1
23let head: Head | null = null
24let home: string | null | undefined
25
26const statOf = ($: EngineInterface, path: string) => $.fs.stat(path).catch(() => null)
27
28// The working tree holding `dir`: the nearest folder above it with a `.git`, a folder in a
29// repository's own tree, a file naming the gitdir in a worktree.
30const findTop = async ($: EngineInterface, dir: string): Promise<{ top: string; gitdir: string } | null> => {
31 for (let d: string | null = dir; d !== null; d = parentOf(d as string)) {
32 const dotGit = `${d.replace(/[\\/]+$/, '')}${sepOf(d)}.git`
33 const st = await statOf($, dotGit)
34 if (st === null) continue
35 if (st.kind === 'dir') return { top: d, gitdir: dotGit }
36 const text = await $.fs.read(dotGit).catch(() => '')
37 const gitdir = gitdirOf(text, d)
38 return gitdir === null ? null : { top: d, gitdir }
39 }
40 return null
41}
42
43// Works the line out again when the folder or HEAD has changed since it was last drawn.
44const check = async ($: EngineInterface) => {
45 // The session's directory follows a shell `cd`, /cd and worktree moves; its root does not
46 // follow a shell `cd`, and CwdChanged does not fire for one (ADR-0081).
47 const dir = await $.session.cwd()
48 if (home === undefined) home = (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME')) ?? null
49 if (dir !== dirSeen) {
50 dirSeen = dir
51 const found = await findTop($, dir)
52 top = found?.top ?? null
53 headPath = found === null ? null : `${found.gitdir}${sepOf(found.gitdir)}HEAD`
54 headSeen = -1
55 head = null
56 }
57 if (headPath !== null) {
58 const st = await statOf($, headPath)
59 const mtime = st?.mtimeMs ?? -1
60 if (mtime !== headSeen) {
61 headSeen = mtime
62 head = st === null ? null : headOf(await $.fs.read(headPath).catch(() => ''))
63 }
64 }
65 const next = { path: locationOf(dir, top, home), head }
66 await update($, where, w => (w !== null && w.path === next.path && w.head?.branch === head?.branch && w.head?.sha === head?.sha ? w : next))
67}
68
69export const register: Register = on => {
70 on('session.start', async ($, e, next) => {
71 const result = await next(e)
72 await check($)
73 $.clock.every(TICK_MS, () => void check($).catch(() => undefined))
74 return result
75 })
76
77 // /clear, /resume and /branch start $.state over without a session.start.
78 on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
79 const result = await next(e)
80 dirSeen = null
81 await check($)
82 return result
83 }).catch(($, e, next) => next(e))
84
85 // A checkout, a worktree entered, a `cd`: each happens under a tool call.
86 on('tool.call', async ($, e, next) => {
87 const result = await next(e)
88 if (e.agentId === undefined) {
89 await check($)
90 $.clock.after(SETTLE_MS, () => void check($).catch(() => undefined))
91 }
92 return result
93 }).catch(($, e, next) => next(e))
94
95 // One line over whatever the other bands draw: the working location and its branch, two chips.
96 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
97 const below = await next(e)
98 const w = await read($, where)
99 if (w === null || e.props.hasSurvey) return below
100
101 const { Box, Text } = $.ui.resolve(e)
102 const shown = fit(w, e.props.bodyColumns - LEFT - RIGHT)
103 const isDetached = w.head !== null && w.head.branch === null
104 const line = (
105 <Box flexDirection="row" paddingLeft={LEFT} paddingRight={RIGHT}>
106 <Text wrap="truncate-end">
107 <Text backgroundColor="suggestion" color="inverseText">{` ${shown.path} `}</Text>
108 {shown.branch !== null && <Text> </Text>}
109 {shown.branch !== null && (
110 <Text backgroundColor="subtle" color={isDetached ? 'warning' : 'text'}>{` ${shown.branch} `}</Text>
111 )}
112 {isDetached && <Text dimColor> detached</Text>}
113 </Text>
114 </Box>
115 )
116 return (
117 <Box flexDirection="column">
118 {line}
119 {below}
120 </Box>
121 )
122 })
123}
124hooks/where.ts 112 lines1import type { Head, Where } from '../types'
2
3/** The longest a branch chip's name runs before it is cut (thegraph-panel's run-name limit). */
4export const BRANCH_MAX = 40
5
6// Hangul, CJK and full-width forms take two cells in a terminal.
7const isWide = (c: string) => /[ᄀ-ᅟ⺀-가-힣豈-︰-﹏-⦆¢-₩]/.test(c)
8
9export const cells = (s: string) => [...s].reduce((n, c) => n + (isWide(c) ? 2 : 1), 0)
10
11/** `s` cut at its end to `w` cells, the last of them `…`. */
12export const cutEnd = (s: string, w: number): string => {
13 if (cells(s) <= w) return s
14 if (w <= 0) return ''
15 let out = ''
16 let n = 0
17 for (const c of s) {
18 const k = isWide(c) ? 2 : 1
19 if (n + k > w - 1) break
20 out += c
21 n += k
22 }
23 return `${out}…`
24}
25
26/** `s` cut at its start to `w` cells, the first of them `…`: a path keeps the folder it ends in. */
27export const cutStart = (s: string, w: number): string => {
28 if (cells(s) <= w) return s
29 if (w <= 0) return ''
30 const cs = [...s]
31 let out = ''
32 let n = 0
33 for (let i = cs.length - 1; i >= 0; i--) {
34 const c = cs[i] ?? ''
35 const k = isWide(c) ? 2 : 1
36 if (n + k > w - 1) break
37 out = c + out
38 n += k
39 }
40 return `…${out}`
41}
42
43export const sepOf = (path: string) => (path.includes('\\') ? '\\' : '/')
44
45const trimSep = (path: string) => (/^([A-Za-z]:)?[\\/]$/.test(path) ? path : path.replace(/[\\/]+$/, ''))
46
47/** The folder holding `path`, or null at a root. */
48export const parentOf = (path: string): string | null => {
49 const p = trimSep(path)
50 const cut = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'))
51 if (cut < 0) return null
52 const parent = p.slice(0, cut) || p.slice(0, 1)
53 const root = /^[A-Za-z]:$/.test(parent) ? `${parent}${sepOf(p)}` : parent
54 return root === p ? null : root
55}
56
57export const baseName = (path: string) => trimSep(path).split(/[\\/]/).at(-1) ?? path
58
59const isAbsolute = (path: string) => /^([A-Za-z]:)?[\\/]/.test(path)
60
61// Windows paths compare without regard to case.
62const sameStart = (path: string, top: string) => {
63 const fold = (s: string) => (sepOf(s) === '\\' ? s.toLowerCase() : s)
64 const p = fold(trimSep(path))
65 const t = fold(trimSep(top))
66 return p === t || p.startsWith(t + sepOf(top)) || p.startsWith(`${t}/`)
67}
68
69/** Where a `.git` file's `gitdir:` line points, resolved against the folder holding it; null if it has none. */
70export const gitdirOf = (text: string, dir: string): string | null => {
71 const m = /^gitdir:\s*(.+?)\s*$/m.exec(text)
72 const target = m?.[1]
73 if (target === undefined) return null
74 return isAbsolute(target) ? target : `${trimSep(dir)}${sepOf(dir)}${target}`
75}
76
77/** What `HEAD` names: a branch, or a commit when detached; null when it is neither. */
78export const headOf = (text: string): Head | null => {
79 const t = text.trim()
80 const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(t)
81 if (ref?.[1] !== undefined) return { branch: ref[1], sha: null }
82 if (/^[0-9a-f]{40,64}$/.test(t)) return { branch: null, sha: t.slice(0, 7) }
83 return null
84}
85
86/**
87 * The working location (GLOSSARY): the working tree's folder name and the path
88 * beneath it; outside any working tree, the path with `home` shortened to `~`.
89 */
90export const locationOf = (dir: string, top: string | null, home: string | null): string => {
91 const slash = (s: string) => s.replace(/\\/g, '/')
92 if (top !== null && sameStart(dir, top)) {
93 const rel = slash(trimSep(dir).slice(trimSep(top).length)).replace(/^\/+/, '')
94 return rel === '' ? baseName(top) : `${baseName(top)}/${rel}`
95 }
96 if (home !== null && sameStart(dir, home)) {
97 const rel = slash(trimSep(dir).slice(trimSep(home).length)).replace(/^\/+/, '')
98 return rel === '' ? '~' : `~/${rel}`
99 }
100 return slash(dir)
101}
102
103/** The text in each chip at `width` cells: the branch whole up to BRANCH_MAX, the path cut at its start. */
104export const fit = (where: Where, width: number): { path: string; branch: string | null } => {
105 const head = where.head
106 const name = head === null ? null : cutEnd(head.branch ?? head.sha ?? '', BRANCH_MAX)
107 // A chip is its text with a space either side; a detached head adds ` detached` after its chip.
108 const branchCells = name === null ? 0 : 1 + cells(name) + 2 + (head?.branch === null ? cells(' detached') : 0)
109 const path = cutStart(where.path, Math.max(1, width - branchCells - 2))
110 return { path, branch: name }
111}
112types/index.d.ts 14 lines1/** What `HEAD` names: a branch, or the short SHA of a detached commit. */
2export type Head = { branch: string; sha: null } | { branch: null; sha: string }
3
4/** What the line draws: the working location, and its head when it is inside a working tree. */
5export type Where = { path: string; head: Head | null }
6
7declare module 'claude-code' {
8 interface PluginState {
9 'path-band': {
10 where: Where | null
11 }
12 }
13}
14