Imported from ToRvaLDz/trailhead (
plugins/trailhead/skills/trailhead-work/SKILL.md). Install upstream withnpx skills add ToRvaLDz/trailhead --skill trailhead-work. Copyright stays with the author.
trailhead-work is the work cluster of the trailhead skill: the verbs that resolve tickets and manage a ticket's lifecycle. work takes the next frontier ticket (or the one you name) and resolves it with its type's engine; quick works one ticket whole, off the map, without splitting; pause checkpoints the ticket in play; resume picks it back up; split breaks an oversized ticket into children; auto runs the map's frontier autonomously, ticket by ticket, suspending the one-ticket-per-session rule until a stop condition or exhaustion. Everything lives on the GitHub Issues; the repo holds code only.
Load first, in order
Before doing anything, read ../_shared/load-first.md and follow it: the shared-core load contract (the six core files, in order, then the effective config). _shared/ is a sibling of this cluster's own directory (at ../_shared/), never a child of it; its absence from a listing of the cluster dir is expected, not a missing core.
Config is a property of the project/map, not the working directory: an isolation workspace inherits this config, never re-resolves it from the isolated copy's own root. For an explicit work <n> / quick <n>, the named-ticket Preflight below runs before this config load: never load config, or anything else, for a ticket that turns out closed.
Routing: verb to engine
The first word of the arguments is the verb; the rest is the text (a ticket number, a "text" seed, or a pause note).
work [ticket]to the Work the map engine below (Mode 2). It dispatches the chosen ticket to its type's engine inreferences/ticket-engines.md(decision/research/prototype/build/bug/task). Read that file when you resolve the ticket.quick [ticket | "text"]to the Work one ticket whole engine below. It runs the fullbuild/bugcycle fromreferences/ticket-engines.md, off the map, without splitting.pause [note]/resume [ticket]/split [ticket]to the teamwork lifecycle:../_shared/teamwork.md(multi-cluster, so it lives in_shared/). Read it and follow it.auto [map]to the autonomous run engine inreferences/auto.md: it runs the map's frontier ticket-by-ticket, suspending one-ticket-per-session, taking every advisory choice as delegate without the confirm gate and stopping only at the safety rail, fog, human-necessary decisions, or human interrupt. Read that file when you engage a run.
The cross-cluster situational references and the technique bodies these engines call live in _shared/; this cluster names each by its ../_shared/... path where it needs one (teamwork, out-of-scope, the gh cookbook, the techniques).
Preflight: reject a closed or missing named ticket (before anything else)
When the verb carries an explicit ticket number (work <n> or quick <n>), the very first action, before config load, before the map load, and before any claim, scope, or isolation setup, is a single cheap state lookup of that ticket: gh issue view <n> --json state,stateReason,title (one call, near-free). Branch on the result:
- Open → proceed into the engine (config load, selection, claim, the rest).
- Closed, or the issue does not exist → stop immediately and report it by name, with its close reason where useful (
stateReason, and thetrailhead:out-of-scope/trailhead:supersededlabel distinguishes resolved vs out-of-scope vs superseded). Suggest the sensible next step: for a defect in already-closed work open a newbugper the new-bug-vs-reopen rule (references/ticket-engines.md), otherwise pick a frontier ticket. Do not load config, load the map, claim, or set up any workspace: a stale or mistyped closed number must be a near-instant, near-free rejection, not a full ramp-up wasted.
This guard is the explicit-number paths only. Bare work selects from the live frontier (open by construction) and quick "<text>" opens a fresh ticket, so neither has a prior ticket to check and both skip it.
Work the map: work
The user invokes with a map (URL or number). A ticket is optional: without one, you pick the next decision.
- Load the map (the low-res view, not every ticket body). Pick which map if the repo has several open: use the one the user named, else the active map (
.trailhead/active-map); if neither and more than one is open, list them and ask. Set/refresh the active-map marker to the one you're working. See Multiple maps on one repo in../_shared/multi-map.md. Codebase issue check (greenfield, offer once): if the repo has notrailhead:codebaseissue (a greenfield map never got one) and now holds substantial code, don't interrupt the work to offer now: mark the offer pending and surface it at the handoff, just before the/clear-first next-step block (not as a line inside it: see the pending codebase offer rule in../_shared/session-handoff.md), exactly once. On the user's yes, generate it via the Codebase map technique, then pin it (one of the three permanent repo pins: dashboard, codebase, conventions) and link it from the map's Notes; if the user declines, don't nag again this session. A repo that already has the issue, or is still too thin to map, skips this. - Choose the ticket. If the user named a ticket, the Preflight above already confirmed it is open (a closed, superseded, out-of-scope, or nonexistent number stopped there, before any setup ran). If the user names one, use it (a named ticket may be a
trailhead:whiteboardone:work <n>works it whole, off the map, with the normal cycle and split allowed; usequick <n>for the no-split variant). Otherwise take a frontier ticket to act on, scoped to this map (add itstrailhead:map-<n>label to the frontier query when several maps are live) and never a whiteboard ticket (bareworkstays on the map; the whiteboard frontier is reached via/trailhead:whiteboardandquick). Bareworkmay act on the first ticket the query returns, but that order isgh's newest-first, not a priority: present several independent tickets as a set to choose from, not the graph's natural next step (see Frontier order carries no priority in../_shared/substrate.md). Claim it: assign it to yourself before any work, then re-read the assignee before starting; on a collision, stop and ask the user (see../_shared/teamwork.md). If the ticket proves too big once you're in it, propose a split rather than grind, but never split on your own: present the children you would create and wait for the user's go-ahead before creating anything (same file).- First, resolve the effective isolation mode. The
isolation:header defaults toauto(an absent key meansauto; the engine never writes the key). Underauto, at work-start before any file edit or diagnostic probe, ask the user once whether they will work more than one ticket at once on this machine this session. No (a lone ticket): the mode resolves to the current checkout, exactly likenone(the scope-free check below applies). Yes: resolve by repo shape, a plain repo ->worktree, a superrepo (a.gitmodulesat the checkout root declaring at least one submodule) ->clone; that "yes" is the clone gate's answer, so the resolvedclonepath proceeds without re-asking. Because trailhead resolves one ticket per session, this asks at most once per ticket work-start (so once per session in normal work; a within-session ticket switch re-asks at the new ticket's work-start), and an autonomous run (/trailhead:auto) never asks (sequential work, no concurrency) and resolves to the current checkout. If the header carries an explicitnone/worktree/clone, honour it as-is (never subject it to auto's ask): explicitclonekeeps its own gate, explicitworktreeis eager. See../_shared/teamwork.md. - When the resolved mode is the current checkout (
isolation: none, orautoanswered no), check the scope is free before starting (this is how a submodule/package gets serialised when worktrees don't fit): scan the other in-progress tickets'Scope:lines, and if one overlaps this ticket's scope, stop and tell the user the scope is busy (name the ticket + holder) and offer a disjoint-scope frontier ticket or to wait. See../_shared/teamwork.md. - Write the session-ticket marker (a cheap hint for tooling like a statusline): at the working root (the checkout, worktree, or clone you'll edit in), write
.trailhead/session-ticketas one line#<n> <ticket title>. Keep it gitignored (add.trailhead/session-ticketto that repo's.gitignoreif absent): it's per-session local state, never committed. It just lets an external tool show "what am I on" offline; the tracker stays the source of truth. Remove it at Resolve/handoff (see../_shared/session-handoff.md); on Pause it may stay (you'll return) or be cleared if you release the claim. The.trailhead/active-mapmarker (which map this session works) follows the same rules: gitignored, per-session, never committed; see Multiple maps on one repo in../_shared/multi-map.md. - When the resolved mode is
worktreeorclone(explicit, orautoanswered yes), set up the isolated workspace NOW, before any file edit or diagnostic probe (not later at Execute/Fix): if the repo has submodules (a.gitmodules),isolation: worktreeis escalated toclone(a superproject worktree cannot safely carry submodule state; see../_shared/teamwork.md). Forworktree(no submodules), create/enter this ticket'sgit worktreeon atrailhead/t<n>branch for the package/path the ticket'sScope:names (a submodule-bearing repo would have escalated tocloneabove), e.g.git worktree add ../<repo>-t<n> -b trailhead/t<n>; forclone, make an independent working copy at../<repo>-t<n>(branchtrailhead/t<n>) by whichever is faster:git clone(add--recurse-submoduleswhen the repo has submodules) + the install step, or a folder copy that bringsnode_modules(and the submodule working trees) to skip the reinstall (then verify the copy's git is independent,git -C <copy> rev-parse --absolute-git-dirresolves inside it, and the same inside every submodule, else commits leak back to the original); a clone with submodules is provisioned viagit submodule update --init --recursive(nested included), each scoped submodule pinned at the superproject's recorded commit. If thisclonewas reached viaauto's work-start ask (answered yes), that ask already served as the clone gate: do not re-ask, proceed to set up the clone. Under an explicitisolation: clone, run the gate as follows. Becausecloneis heavy (a full clone + an install), gate it, every time, before cloning: ask the user whether they expect to work more than one ticket at once on this machine this session. If no, skip the clone and work this ticket in the current checkout (isolation only exists to stop concurrent workspaces from colliding; a lone ticket has nothing to collide with, so the shared-checkout caveats apply and that is fine, andgit:still decides the branch as usual); if yes, clone as above. The gate isclone-only:worktreeis cheap, so set it up without asking. Do every subsequent step in whatever workspace you ended up in. This is the whole point of isolation: if you diagnose and edit in the shared checkout "just to start" while a concurrent workspace exists, you are already colliding. Set the workspace up silently (theclonegate-question is the one exception you voice); it's undone at Resolve when the branch integrates and the worktree/clone is removed. See../_shared/teamwork.md.
- First, resolve the effective isolation mode. The
- Brief the ticket and confirm scope, before the engine runs. With the ticket claimed, read it and give the user a brief, result-oriented summary of what resolving it will do (its goal and the shape of the work, not a plan yet), then ask whether they want to change anything. This is a HITL confirm gate: on assent, proceed to the engine. If the user asks for a change, hold it against the ticket. A change consistent with the ticket is folded in and the engine proceeds. A change that diverges from the ticket is surfaced as a mismatch, and the user is asked, never forced, whether to (a) work the ticket as-is, or (b) open a separate new ticket for the divergent request. Offer both and decide neither: open the separate ticket only if the user chooses it (via Scope that surfaces while working a ticket, served by the capture cluster, the same capture-or-split path used for scope that surfaces mid-work), never automatically. Under an autonomous run (
auto) this is a process confirm gate, so it follows the advisory boundary inreferences/auto.md:autoproceeds (takes the delegate option) without stopping to brief. - Resolve it with its type's engine in
references/ticket-engines.md, zoom as needed: fetch the full body of related/closed tickets on demand. If in doubt on adecisionticket, run Grilling + Domain vocabulary. If in doubt on abuild, stop and ask (see the Discuss step): never auto-grill. - Record the resolution: a comment with the answer,
gh issue close, add the pointer to Decisions so far. Then unblock dependents: for every ticket this one was blocking, if it was the last open blocker, remove itstrailhead:blockedlabel so it graduates onto the frontier. - Add newly-surfaced tickets (create-then-wire, labelling blocked ones
trailhead:blocked); graduate the fog that became specifiable, clearing the patch from Not yet specified. If the answer reveals a ticket sits beyond the destination, apply the out-of-scope vs deferred test (../_shared/out-of-scope.md): if it's really deferred on something outside the map, ask the user to route it to aseed/idea/todo; only if it's truly beyond the destination rule it out of scope (label, then drop its native sub-issue edge to reclaim a slot, then close: the eager drop in../_shared/substrate-commands.md, 404-tolerant, references intact) instead of resolving it. If the decision invalidates other parts of the map, update or delete them. - Hand off. Close the session with the Session handoff ritual (
../_shared/session-handoff.md): confirm the ticket is resolved by name, then the scannable next-step block with/clearfirst (never the next command without it), naming the next frontier ticket and giving its number (or, when several mutually independent tickets sit on the frontier, presenting them as a set to choose from rather than crowning the first-listed), or/trailhead:mapif the frontier is empty/ambiguous. This holds in any later follow-up too, not only at the moment of resolution. If this resolution leaves the map exhausted (no open tickets and no fog left: the destination is reached), say so; but before closing it out, if Out of scope holds any deferred line (apply the out-of-scope-vs-deferred tells in../_shared/out-of-scope.md), stop and ask the user what to do with each (aseedgated on its trigger, anidea, or atodo, in this or another map), so nothing wanted-later is lost when the map goes quiet. Only once the deferred items are routed (or the user says leave them), ask the user whether to close the map issue now that the destination is reached:gh issue closeit on a yes (it stays the record), leave it open on a no; never close a map unprompted. Either way refresh the dashboard so an exhausted/closed map drops off (a structural event). Maps aren't pinned, so there's no pin to free; the dashboard, codebase, and conventions stay pinned regardless.
Work one ticket whole, off the map: quick
A streamlined way to just get one ticket done, without map ceremony. quick "<text>" opens a whiteboard ticket (a build, or a bug when the text is clearly a defect) and works it end to end now; quick <n> works an existing ticket (whiteboard or map) the same way. For quick <n>, the Preflight above runs first: a closed, out-of-scope, superseded, or nonexistent ticket stops immediately, before config, claim, or isolation setup. Claim it first; still one ticket per session. Bare /trailhead:quick (no argument, e.g. offered in a Session handoff) asks for the piece to work, like a capture missing its text, then proceeds as quick "<text>".
It runs the full engine (the build/bug cycle in references/ticket-engines.md: Discuss → Plan → Execute → Verify → Resolve), differing from work on these points:
- Briefs and confirms scope first, exactly as Work the map step 3. With the ticket claimed, summarise what resolving it will do and ask whether to change anything, applying the same consistency rule (a divergent request is offered as work-as-is vs a separate new ticket, opened only if the user chooses it, never forced; via Scope that surfaces while working a ticket). A just-written
quick "<text>"needs only a one-line confirmation;quick <n>on an existing ticket gets the full brief. - Grill only if needed (Discuss): never auto-grill; start from Plan and stop to ask only if a blocking ambiguity surfaces, exactly as the
buildDiscuss. The user may ask to grill at any time. - Never splits. If the ticket turns out large, work it whole anyway: that is the point of
quick. Do notsplitit and do not spin children. (New scope that surfaces mid-work is still captured out, never folded in, see Scope that surfaces while working a ticket, served by the capture cluster.) - No map narrative book-keeping. A whiteboard ticket has no map, so there is no
Decisions so farupdate, no fog to graduate, no frontier re-scan. Everything else holds: atomic commits withRefs: #<n>, TDD / Code review / Acceptance testing per the cycle, the resolution comment +gh issue close, and the Session handoff (../_shared/session-handoff.md). Exception,quick <n>on a map ticket: dependent-unblocking still runs (removetrailhead:blockedfrom any dependent whose last blocker just closed, Work the map step 5 above); skipping it would strand those dependents off the frontier for good. Only the narrative book-keeping above and the split are skipped, so preferwork <n>for a map ticket that has dependents or belongs in the map record. - Same isolation setup as
work.quickwrites the.trailhead/session-ticketmarker at the working root in every isolation mode (exactly as Work the map step 2, so tooling like the statusline shows what's in play; no active-map marker, though: a whiteboard ticket has no map). Underautoit runs the same work-start concurrency ask before any edit and resolves to current-checkout / worktree / clone by the same rule; under an explicitisolation: worktree/cloneit sets up the ticket'strailhead/t<n>workspace before any edit (including the submodule-forces-cloneescalation and submodule provisioning) and re-checks the claim; underisolation: none(orautoanswered no), the scope-free check applies too. - Refreshes the dashboard at the handoff, not mid-work. A
quick "<text>"whiteboard ticket is born and resolved in the same session, so its dashboard refresh rides with the session's end, not with ticket creation: do not refresh right after the ticket exists (that lands a structural pinned-issue write ahead of the workquickexists to just-get-done, and only duplicates the resolution refresh a moment later). Refresh at Resolve (the handoff), as a full regeneration from the live tracker (never a partial patch that leaves other sections knowingly stale), so the whiteboard ticket lands on the dashboard as the session closes. If the session pauses before Resolve (/trailhead:pause), refresh there instead, so a deliberately-unfinished quick still shows (the read-only renders never rewrite the dashboard body, so nothing else would surface it).quick <n>on a map ticket does not refresh (map ticket progress shows via the native bar). See The dashboard and its freshness rule in../_shared/substrate.md. - Honours
config.effort.quickis the natural home for the cost-aware path: undereffort: leanit runs the same size triage and ask-to-skip offer defined canonically under effort in../_shared/configuration-reference.md(the same one thebuild/bugengine runs,references/ticket-engines.md), at the start before planning. On a trivial/small ticket it surfaces the one consolidated offer (the plan pass, TDD, cross-AI review, the full code review, the goal-backward verify) and goes lean only on an explicit yes. The correctness spine (atomicRefs:commits, tests/criterion, post-commit self-check, Resolve) is never skipped. Undereffort: standard(default), and on any non-yes answer, the full cycle runs, exactly as today.
Type follows the ticket: quick "<text>" defaults to build (bug if a defect); quick <n> uses the existing type and its matching engine.
Ensure the whiteboard label exists before applying it (a repo that adopted trailhead before the whiteboard existed never created it): gh label create trailhead:whiteboard --color C5DEF5 --description "Map-less ticket: lives on the whiteboard" 2>/dev/null || true. See The whiteboard in ../_shared/substrate.md.
Pause, resume, split
pause [note]/resume [ticket]: checkpoint the ticket in play so it can be resumed later, and pick it back up from its latestPAUSEDcheckpoint. Read../_shared/teamwork.md(Pausing & resuming) and follow it: thePAUSEDcheckpoint contents, release-vs-keep the claim, and (underisolation: worktree/clone) committing the work-in-progress in the ticket's isolated workspace and recording its path in the checkpoint, then re-entering it on resume.split [ticket]: split the named (or in-play) ticket into children that replace it. Read../_shared/teamwork.md(Splitting a ticket) and follow it: create the children (each wired to the map and to the split origin), re-point the original's blockers onto the right children, then supersede & close the original (trailhead:superseded).