@marcomigozzi/trailhead 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.it.md +12 -9
- package/README.md +12 -9
- package/bin/lib/codex-projection.js +5 -4
- package/bin/lib/codex-projection.test.js +3 -1
- package/bin/trailhead.js +9 -5
- package/bin/trailhead.test.js +130 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/commands/prune.md +6 -0
- package/plugins/trailhead/hooks/hooks.json +4 -0
- package/plugins/trailhead/hooks/lib/gh-subcommand.js +102 -0
- package/plugins/trailhead/hooks/lib/gh-subcommand.test.js +99 -0
- package/plugins/trailhead/hooks/trailhead-body-guard.js +221 -0
- package/plugins/trailhead/hooks/trailhead-body-guard.test.js +222 -0
- package/plugins/trailhead/hooks/trailhead-issue-injection-scanner.js +61 -35
- package/plugins/trailhead/hooks/trailhead-issue-injection-scanner.test.js +76 -0
- package/plugins/trailhead/hooks/trailhead-secret-guard.js +4 -15
- package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +31 -0
- package/plugins/trailhead/skills/_shared/choices.md +2 -1
- package/plugins/trailhead/skills/_shared/configuration-reference.md +1 -1
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/multi-map.md +1 -1
- package/plugins/trailhead/skills/_shared/principles.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +3 -3
- package/plugins/trailhead/skills/_shared/substrate-commands.md +41 -5
- package/plugins/trailhead/skills/_shared/substrate.md +10 -6
- package/plugins/trailhead/skills/_shared/teamwork.md +19 -12
- package/plugins/trailhead/skills/trailhead/SKILL.md +3 -3
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -0
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +12 -2
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +5 -4
- package/plugins/trailhead/skills/trailhead-manage/references/pruning.md +48 -0
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +4 -2
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +13 -11
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +1 -1
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +3 -3
|
@@ -21,26 +21,31 @@ Whoever claims a ticket **owns it end to end**: they work it and close it themse
|
|
|
21
21
|
Claiming stops two sessions from working the *same* ticket; it does nothing about two sessions working *different* tickets in the **same checkout**. The working tree and the git index are process-global: two agents editing and committing in one clone corrupt each other's state regardless of ticket type (a `bug` is no more or less exposed than a `build`). The claim is the ticket-level lock; the **worktree is the filesystem-level one**.
|
|
22
22
|
|
|
23
23
|
The repo's `trailhead:conventions` `isolation:` header decides:
|
|
24
|
-
- **`
|
|
24
|
+
- **`auto`** (default): resolve the mode at a ticket's **work-start, before any edit**. Ask once whether more than one ticket will be worked at once on this machine this session (this **is** the `clone` gate below, generalised to the whole mode, so a `clone` resolution never re-asks). **No** (a lone ticket): work in the current checkout, exactly like `none`. **Yes**: resolve by repo shape, a plain repo -> `worktree`, a **superrepo** (a `.gitmodules` at the checkout root that declares at least one submodule; equivalently `git submodule status` lists one) -> `clone` (the superproject clone + submodule provisioning below). Because trailhead resolves **one ticket per session**, the ask fires at most once per ticket work-start (so once per session in normal one-ticket-per-session work; a within-session ticket switch, discouraged, re-asks at the new ticket's work-start); the autonomous run (`/trailhead:auto`) **never asks** (it works tickets sequentially, so there is no concurrency) and resolves `auto` to the current checkout. `auto` is a **runtime interpretation**: the engine never writes `isolation: auto` into the conventions.
|
|
25
|
+
- **`none`**: one session at a time per checkout. Simplest, and correct for a repo worked one ticket at a time. Two concurrent sessions here must use **separate clones**, or they collide.
|
|
25
26
|
- **`worktree`**: each executing ticket runs in its own `git worktree` on a `trailhead/t<n>` branch (`git worktree add ../<repo>-t<n> -b trailhead/t<n>`), so concurrent sessions never share a working tree or index. Because git refuses to check out one branch in two worktrees, **worktree isolation implies a branch per ticket even under `git: main`**: the trunk is never checked out twice. At **Resolve**, integrate the branch to the trunk per `git:` (fast-forward or merge into `main`, or open the PR when `git: pr`), then `git worktree remove` it. On **Pause**, the `PAUSED` checkpoint records the worktree path and branch so a resume (by anyone, once released) lands back in it.
|
|
26
|
-
- **`clone`**: each executing ticket works in its own **independent working copy** at `../<repo>-t<n>`
|
|
27
|
-
- **`git clone` + the project's install step** (clean git; pays the install cost), or
|
|
28
|
-
- **a folder copy that brings `node_modules`** (`cp -r` the repo or superproject) to **skip the reinstall**, usually faster for a big `node_modules`. **Catch (git independence):** a raw copy can leave the copy's `.git` (or a submodule's `.git` file) pointing back at the **original**, so its commits would leak there. Verify with `git -C <copy> rev-parse --absolute-git-dir`
|
|
27
|
+
- **`clone`**: each executing ticket works in its own **independent working copy** at `../<repo>-t<n>` on a `trailhead/t<n>` branch (for a submodule-bearing superrepo the clone is of the **superproject** with `--recurse-submodules`, never the submodule alone, so the gitlink bump has a home and each ticket gets independent submodule state; see **Git submodules force `clone`** below). That is the point: it has its own installed dependencies, so it **builds and UATs like the real thing**: the isolation for **path-bound apps a worktree can't run** (React Native/Expo, native toolchains, a local `node_modules`). **A clone is heavy (a full clone + an install), so gate it every time: before cloning, ask whether more than one ticket will be worked at once on this machine this session. If not, skip the clone and work this ticket in the current checkout** (isolation only guards concurrent workspaces; a lone ticket has nothing to collide with). If yes, set it up at work-start by **whichever is faster**:
|
|
28
|
+
- **`git clone` + the project's install step** (clean git; pays the install cost; add `--recurse-submodules`, or follow with `git submodule update --init --recursive`, when the repo has submodules so nested ones are provisioned too), or
|
|
29
|
+
- **a folder copy that brings `node_modules`** (`cp -r` the repo or superproject, submodule working trees included) to **skip the reinstall**, usually faster for a big `node_modules`. **Catch (git independence):** a raw copy can leave the copy's `.git` (or a submodule's `.git` file) pointing back at the **original**, so its commits would leak there. Verify with `git -C <copy> rev-parse --absolute-git-dir` **and the same inside every submodule (nested included)**: each must resolve **inside the copy's own tree**, not the original or another ticket's copy. If any points outside, re-point it, or use the `git clone` route and copy only `node_modules` across.
|
|
29
30
|
|
|
30
|
-
At **Resolve**, push the branch and integrate per `git:` (into `main`, or a PR); for
|
|
31
|
+
At **Resolve**, push the branch and integrate per `git:` (into `main`, or a PR); for submodule changes follow **publish-before-point** in **Git submodules force `clone`** below (push or merge the submodule commit first, only then bump the superproject gitlink), because the clone is disposable and a gitlink must never reference a commit that lives only in it. Record the copy's path in the `PAUSED` checkpoint and delete it once integrated.
|
|
31
32
|
|
|
32
|
-
Prefer `worktree` whenever more than one session may run on a single clone. On a **monorepo** it is doubly worth it: `git worktree add` shares the object store, so it is far cheaper than a second full clone of a large tree.
|
|
33
|
+
With the default `auto` this choice is made for you at each ticket's work-start; set `worktree` explicitly to force an eager worktree regardless. Prefer `worktree` whenever more than one session may run on a single clone. On a **monorepo** it is doubly worth it: `git worktree add` shares the object store, so it is far cheaper than a second full clone of a large tree.
|
|
33
34
|
|
|
34
|
-
**If the header never set `isolation:` at all
|
|
35
|
+
**If the header never set `isolation:` at all**, it resolves to **`auto`** (the default) at runtime: the work-start concurrency ask decides the mode by repo shape (above), and **nothing is written** into the conventions. This supersedes the earlier behaviour where an absent key silently meant `none` with a one-time collision sanity-check that wrote a key; that nudge-and-write flow is **gone**. A header that already carries `isolation:` (any value, including `none` or `auto`) was chosen deliberately and is honoured as-is.
|
|
35
36
|
|
|
36
37
|
**When a worktree can't build, use `clone`: path-bound tooling.** A worktree isolates *source*, but it is a fresh directory at a new path with no installed dependencies. For a package or submodule whose build/run is **bound to a fixed path** (React Native/Expo, native toolchains, anything with a local `node_modules`, a Metro/bundler cache, or absolute-path config), the code checks out but the app **won't build or UAT from the worktree** without reinstalling and reconfiguring. For those set **`isolation: clone`** (a dedicated clone installs its own deps and runs like the real app), or, if you don't want per-ticket clones, keep **`isolation: none`** and **serialise** that package (one session at a time on it; the other takes a disjoint `Scope:`). Worktree isolation shines where any checkout builds from any path (most libraries, Go/Rust/Python source trees, docs); `clone` is its heavier counterpart for path-bound app toolchains.
|
|
37
38
|
|
|
38
|
-
**Git submodules
|
|
39
|
+
**Git submodules force `clone`, never `worktree`.** A submodule is its own repo, gitlink-tracked by the superproject. When a superrepo **has submodules** (a `.gitmodules` file) and isolation is requested, **`isolation: worktree` is escalated to `isolation: clone`**: superproject worktrees share the one real submodule git dir (`.git/modules/<sub>`), so two worktrees that check a submodule out to different commits corrupt each other, whereas a clone gives each ticket a fully independent submodule git dir. Whether a given ticket will *write* a submodule cannot be told up front, so worktree is disallowed outright here rather than allowed-only-if-read-only. `isolation: none` is unchanged (single checkout, overlapping-`Scope:` tickets serialise); the escalation applies whenever isolation would be `worktree`, and the same rule makes the default **`auto`**'s superrepo branch resolve straight to `clone` (see the `auto` bullet above), never `worktree`, for the same reason. Handling is end-to-end:
|
|
40
|
+
- **Provision at setup.** Stand the clone up with `git clone --recurse-submodules` (or a follow-up `git submodule update --init --recursive`; nested submodules need `--recursive`), so every submodule the ticket's `Scope:` names is populated and pinned at the superproject's recorded commit **before any build or test runs**. The folder-copy shortcut must bring the submodule working trees too and pass the git-independence check inside each of them (above).
|
|
41
|
+
- **Integrate at Resolve, publish before you point.** A submodule change lives in the sub-repo, so commit it **inside the submodule on a branch** (never the default detached HEAD, or the commit is lost when the clone is torn down). A gitlink must **never** reference a commit that exists only in the disposable clone, so **publish the submodule commit first** (push it to the submodule's canonical remote, its configured upstream / `.gitmodules` url; under `git: pr`, open and *merge* the submodule PR), **whatever the isolation mode** (this holds under `none` too, not just per-ticket clones), and **only then bump the superproject gitlink** to that published commit and integrate the superproject per `git:` (`main`, or a PR). **Nested submodules integrate leaf-first:** commit and push the deepest submodule, bump-commit-push its parent's gitlink, up through every level, superproject last. Each commit carries a `Refs:` trailer for this ticket, but a submodule commit is in a **different repo**, so its trailer must be **fully qualified at the superrepo ticket**, `Refs: <owner>/<superrepo>#<n>` (a bare `Refs: #<n>` would resolve against the submodule's own issue tracker); only superproject commits use the bare `Refs: #<n>`.
|
|
42
|
+
- **Verify inside each submodule.** Run `git status` **inside every submodule the ticket touched** (nested included), not just the superproject: the superproject only ever shows a submodule as "modified", never that submodule's own dirty working tree. Every submodule path a ticket touches is named in its `Scope:` so setup, integration, and Verify all know which sub-repos to walk.
|
|
43
|
+
- **`isolation: none`** is the escape hatch when you want neither per-ticket clones nor the escalation: commit inside the submodule (on a branch) + push it + bump the gitlink in the current checkout, accepting that the submodule then **serialises** across sessions (overlapping `Scope:` holds at claim).
|
|
39
44
|
|
|
40
45
|
**A ticket that spans two or more submodules.** First ask whether it should be **split**: if the work decomposes into per-submodule pieces wired by a blocker (the gitlink bump waits on the code), prefer that. It stays one ticket only when the change is genuinely atomic across them (a shared contract both sides must adopt together). Then:
|
|
41
|
-
- **Commits are coordinated, never one cross-repo commit** (git can't span repos): commit inside each submodule on its branch **first** (the objects must exist before anything can point at them), then a **single superproject commit that bumps all the gitlinks together** is the atomic "these versions ship as a set" record.
|
|
46
|
+
- **Commits are coordinated, never one cross-repo commit** (git can't span repos): commit inside each submodule on its branch and **publish it first** (push, or merge its PR under `git: pr`; the objects must exist on a remote before anything durable can point at them), then a **single superproject commit that bumps all the gitlinks together** is the atomic "these versions ship as a set" record. Each submodule commit's `Refs:` is fully qualified at the superrepo ticket (`Refs: <owner>/<superrepo>#<n>`); the superproject commit uses the bare `Refs: #<n>`.
|
|
42
47
|
- Its **`Scope:` lists every submodule path it touches**, so the coupling check treats a shared submodule path as overlap and flags a second session touching either one.
|
|
43
|
-
-
|
|
48
|
+
- When isolation is requested, the submodules force **`clone`** (worktree is escalated, above), so each touched sub-repo is already independent in the clone; under `none`, work them in the current checkout and accept that each touched submodule serialises against other sessions.
|
|
44
49
|
|
|
45
50
|
## Pausing & resuming
|
|
46
51
|
Switching away from a ticket mid-work (to clear a blocker, chase an urgent bug, or just stop) must not lose the thread.
|
|
@@ -59,7 +64,7 @@ A repo can hold **more than one live map** at once: you park one to work another
|
|
|
59
64
|
A ticket is sized to one ~100K-token session. When a claimed ticket turns out too big, or fans into distinct pieces, **split it**: don't grind through an oversized ticket. **Splitting is never autonomous.** Whether it was asked for explicitly (`/trailhead:split`) or surfaced mid-work ("this is too big once I'm in it"), **stop and ask the user before creating anything** (the split's shape, which children, is an advisory choice that **offers** the delegate option, per the injection recipe in `choices.md`):
|
|
60
65
|
1. **Propose the split and wait for a go-ahead.** Present the children you would create (each one's title and why it's a distinct session-sized piece) and how the original's blockers would re-point, then **wait for the user's confirmation**. Create nothing until they say go: this is a stop-and-ask moment like the rest of the engine's, never the agent's own call. (An explicit `/trailhead:split` is the user asking *to* split, not a go-ahead on *how*: you still propose the decomposition and confirm it.)
|
|
61
66
|
2. **Create the child tickets** (create-then-wire): each carries `Parent: <map>`, a `Split from: <original name+link>` line, its type label, the map's `trailhead:map-<n>` label + native sub-issue edge (like any ticket), and `trailhead:blocked` if it has an open blocker. **When children block each other** (B builds on A), wire B's blocker (both the `## Blocked by` line and the native dependency) at the **sibling child's real id (A)**, never at the split-origin: that original is being superseded and closed, so nothing may point at it as a live blocker. Describe A by role if you like, but *link its actual number*.
|
|
62
|
-
3. **Supersede the original**: post a comment listing the children (`split into <names+links>`), label it **`trailhead:superseded`**, and **close it**. The children fully replace it.
|
|
67
|
+
3. **Supersede the original**: post a comment listing the children (`split into <names+links>`), label it **`trailhead:superseded`**, **drop its native sub-issue edge from the map** (the eager drop at close in `substrate-commands.md`: label first, then the 404-tolerant DELETE, then close, so a slot is reclaimed while the label, `trailhead:map-<n>`, `Parent:` and split-origin links stay intact), and **close it**. The children fully replace it.
|
|
63
68
|
4. The superseded ticket stays **out of `Decisions so far`** (it wasn't a decision walked; its children are). If other tickets were blocked by the original, re-point their `## Blocked by` at the relevant child and re-evaluate their `trailhead:blocked`.
|
|
64
69
|
|
|
65
70
|
Splitting is not resolving: a superseded ticket delivered nothing itself. (If instead the ticket sits beyond the destination, that's *out of scope*, not a split.)
|
|
@@ -69,7 +74,9 @@ Two frontier tickets can be *unblocked of each other* yet still collide: they to
|
|
|
69
74
|
|
|
70
75
|
**Path scope sharpens this (monorepo).** When tickets carry a `Scope:` line (the package/dir(s) they work in, see the ticket anatomy in SKILL.md), use it as the first filter: **disjoint scopes parallelize safely** (two tickets in unrelated packages can't share a file), so the check narrows to tickets whose scopes overlap or that both touch a shared lib / root config. Absent a `Scope:` line, fall back to the file/seam judgement above. Scope is a *hint that rules coupling out fast*, never a guarantee it exists: overlapping scopes still need the shared-writable-resource test before you flag.
|
|
71
76
|
|
|
72
|
-
**Serialising a scope (`isolation: none`).** This is how you keep two sessions off the same submodule/package when worktrees don't fit (a path-bound app, see the isolation section). Under `isolation: none` the working tree is shared, so **two in-progress tickets whose `Scope:` overlaps cannot run at once**, whatever they touch inside it. So make the scope check a **hold at claim time, not an advisory flag**: right after claiming (before any work), scan the other in-progress tickets' `Scope:` lines; if one **overlaps** this ticket's scope, **stop and tell the user** the scope is busy (name the ticket and its holder), and offer to take a **disjoint-scope** frontier ticket instead or to wait. That overlap-holds-at-claim rule *is* "serialise `app/`": two `Scope: app/` tickets can't proceed together, no extra config needed, and the conventions prose only has to name which scope is serialised for humans. Under `isolation: worktree` this reverts to advisory (each ticket has its own
|
|
77
|
+
**Serialising a scope (`isolation: none`).** This is how you keep two sessions off the same submodule/package when worktrees don't fit (a path-bound app, see the isolation section). Under `isolation: none` the working tree is shared, so **two in-progress tickets whose `Scope:` overlaps cannot run at once**, whatever they touch inside it. So make the scope check a **hold at claim time, not an advisory flag**: right after claiming (before any work), scan the other in-progress tickets' `Scope:` lines; if one **overlaps** this ticket's scope, **stop and tell the user** the scope is busy (name the ticket and its holder), and offer to take a **disjoint-scope** frontier ticket instead or to wait. That overlap-holds-at-claim rule *is* "serialise `app/`": two `Scope: app/` tickets can't proceed together, no extra config needed, and the conventions prose only has to name which scope is serialised for humans. Under `isolation: worktree` or `clone` this reverts to advisory (each ticket has its own workspace), and disjoint scopes always run free (the same-submodule case below is the one overlap `clone` still hard-holds).
|
|
78
|
+
|
|
79
|
+
**Submodules count as shared scope (superrepo).** Treat **the same submodule path as overlapping scope**, exactly like the same superproject file. Two tickets that touch one submodule contend on its single gitlink tree entry and on the submodule's own shared history, so they **hard-serialise at claim** (a hold, like `isolation: none`), and this **holds even under `clone`**: the escalation gives each ticket an independent submodule *working copy*, but they still integrate to one shared submodule remote and one shared gitlink, so the same-submodule overlap is the one case where `clone` does **not** revert to the advisory-only treatment (unlike disjoint scopes, which run free). Two tickets touching *different* submodules each add a superproject commit that bumps a gitlink; those touch different index paths so git merges them cleanly, but they still **race at superproject push time** (two gitlink-bump commits pushed to `main` concurrently: git rejects the second, which pulls or rebases, re-verifies the gitlink, and retries), so coordinate the gitlink bumps rather than treating different-submodule tickets as needing no coordination at all.
|
|
73
80
|
|
|
74
81
|
## Concurrent edits to the map
|
|
75
82
|
The map issue's **body** is a shared mutable document: two sessions appending to `Decisions so far` at once will clobber each other (GitHub doesn't merge bodies). So:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: trailhead
|
|
3
3
|
description: "Start and drive a large project (more than one agent session can hold) as a map of decision tickets on GitHub Issues, resolving one at a time until the way to the destination is clear. Self-contained (zero external-plugin dependencies: trailhead ships its own `trailhead-*` engine agents and needs only an authenticated gh CLI): map-based onboarding and a discuss→plan→execute→verify engine with atomic commits, with everything (map, discussion, plan, verification) living on the Issues. Invoke only when the user explicitly runs `/trailhead` (or a `/trailhead:<verb>` command) or asks to chart, adopt, or work a trailhead map, and do not auto-invoke for generic planning, project, or task-tracking requests."
|
|
4
|
-
argument-hint: "[new|adopt|work|quick|whiteboard|ticket|inbox|map|dashboard|config|grill|split|pause|resume|auto|update|bug|todo|idea|seed|note] [text | ticket]"
|
|
4
|
+
argument-hint: "[new|adopt|work|quick|whiteboard|ticket|inbox|map|dashboard|config|grill|split|pause|resume|auto|update|prune|bug|todo|idea|seed|note] [text | ticket]"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
`trailhead` charts a large project as a **shared map on GitHub Issues**, then works its **tickets** one at a time until the way to the destination is clear. This skill is the **entry point**: bare `/trailhead` runs **smart entry** (detect the repo state and propose the next move); a verb runs that verb's engine. Everything lives on the Issues; the repo holds code only.
|
|
@@ -15,7 +15,7 @@ Before doing anything, read `../_shared/load-first.md` and follow it: the shared
|
|
|
15
15
|
The **first word** of the arguments is the verb; the rest is the text or a ticket number. `/trailhead <verb>` and `/trailhead:<verb>` are equivalent.
|
|
16
16
|
|
|
17
17
|
- **No verb** to **smart entry** (below): this skill handles it directly.
|
|
18
|
-
- **A verb** to hand off to the skill that owns it, passing the whole argument string. **The skill-split migration ([map #53](https://github.com/ToRvaLDz/trailhead/issues/53)) lands the cluster skills one at a time.** The **chart cluster is built (#59)**: `new`, `adopt`, `ticket`, `inbox`, and `grill` route to the **`trailhead-chart`** skill (call the Skill tool with skill name `trailhead-chart` and arguments `<verb> <rest>`). The **work cluster is built (#60)**: `work`, `quick`, `pause`, `resume`, `split`, and `auto` route to the **`trailhead-work`** skill (call the Skill tool with skill name `trailhead-work` and arguments `<verb> <rest>`). The **view cluster is built (#61)**: `map`, `dashboard`, and `whiteboard` route to the **`trailhead-view`** skill (call the Skill tool with skill name `trailhead-view` and arguments `<verb> <rest>`). The **capture cluster is built (#62)**: `bug`, `todo`, `idea`, `seed`, and `note` route to the **`trailhead-capture`** skill (call the Skill tool with skill name `trailhead-capture` and arguments `<verb> <rest>`). The **manage cluster is built (#63)**: `config` and `
|
|
18
|
+
- **A verb** to hand off to the skill that owns it, passing the whole argument string. **The skill-split migration ([map #53](https://github.com/ToRvaLDz/trailhead/issues/53)) lands the cluster skills one at a time.** The **chart cluster is built (#59)**: `new`, `adopt`, `ticket`, `inbox`, and `grill` route to the **`trailhead-chart`** skill (call the Skill tool with skill name `trailhead-chart` and arguments `<verb> <rest>`). The **work cluster is built (#60)**: `work`, `quick`, `pause`, `resume`, `split`, and `auto` route to the **`trailhead-work`** skill (call the Skill tool with skill name `trailhead-work` and arguments `<verb> <rest>`). The **view cluster is built (#61)**: `map`, `dashboard`, and `whiteboard` route to the **`trailhead-view`** skill (call the Skill tool with skill name `trailhead-view` and arguments `<verb> <rest>`). The **capture cluster is built (#62)**: `bug`, `todo`, `idea`, `seed`, and `note` route to the **`trailhead-capture`** skill (call the Skill tool with skill name `trailhead-capture` and arguments `<verb> <rest>`). The **manage cluster is built (#63)**: `config`, `update`, and `prune` route to the **`trailhead-manage`** skill (call the Skill tool with skill name `trailhead-manage` and arguments `<verb> <rest>`). Then carry out the target skill's instructions.
|
|
19
19
|
- **Unrecognised verb** to treat the whole string as `idea` (route to `trailhead-capture` as `idea <text>`).
|
|
20
20
|
|
|
21
21
|
The namespaced `/trailhead:<verb>` commands reach their target skill directly through their own command wrapper; this routing table only covers the bare `/trailhead <verb>` form.
|
|
@@ -26,7 +26,7 @@ With no verb, detect the repo state and **propose**, do not dead-end:
|
|
|
26
26
|
|
|
27
27
|
- **No map** to offer *chart* (`/trailhead:new`) or *adopt* (`/trailhead:adopt`); or point at `/trailhead:whiteboard` if the repo has loose `trailhead:whiteboard` tickets but no map.
|
|
28
28
|
- **Map present with a workable frontier** to route to *work*. Bare `/trailhead:work` takes one ticket to act on, but present the frontier as a **set to choose from** when its tickets are mutually independent, never a crowned first-listed (see **Frontier order carries no priority** in `../_shared/substrate.md`).
|
|
29
|
-
- **Map present but its frontier is empty** (everything left is gated or still fog, or the map is complete-except-gated) to lay out the sensible moves as a short list, and **always include `/trailhead:quick "<text>"` right after `/trailhead:map`** as the way to do a discrete piece of work off the map now (`/trailhead:quick <n>`
|
|
29
|
+
- **Map present but its frontier is empty** (everything left is gated or still fog, or the map is complete-except-gated) to lay out the sensible moves as a short list, and **always include `/trailhead:quick "<text>"` right after `/trailhead:map`** as the way to open and do a *new* discrete piece of work off the map now (an existing loose ticket is worked anchored on `/trailhead:work <n>`, with `/trailhead:quick <n>` as the no-split alternative), alongside `/trailhead:map` for the full view, `/trailhead:whiteboard` when loose work waits, `/trailhead:new` to chart the next map, the captures (`todo` / `idea` / `bug` / `seed`) for new work, and graduating a fog line into a ticket when it is sharp enough.
|
|
30
30
|
|
|
31
31
|
**Self-heal the pinned dashboard** before proposing: ensure the repo's `trailhead:dashboard` index exists and is pinned. Ensure the label, then **create + pin only if missing, re-pin if the pin was dropped, and never rewrite the body on this render** (the dashboard model is in `../_shared/substrate.md`; the exact `gh` commands live in the substrate cookbook, at `../_shared/substrate-commands.md`). So a repo that adopted trailhead before the dashboard existed gets one on the next bare `/trailhead`.
|
|
32
32
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Capture without breaking flow, but with the **tracker** as destination, not scattered files. The verbs are named in this cluster's `../SKILL.md`; here is the detail of each. One capture = one action + one confirmation line; it resolves nothing.
|
|
4
4
|
|
|
5
|
-
**Confirmation line.** Confirm the capture by the ticket's name (not a bare `#number`) and where it landed. **If you point at working it later, lead with `/clear`**: the confirmation is a place you point at the next ticket, so the [Session handoff](../../_shared/session-handoff.md)'s `/clear`-first rule applies: a compact, future-conditional one-liner is fine (a capture is not a resolution, so no full labelled block), along the lines of *"to work it later: `/clear`, then `/trailhead:work 45`"* (in the user's conversation language). Never a bare `/trailhead:work 45` with no `/clear
|
|
5
|
+
**Confirmation line.** Confirm the capture by the ticket's name (not a bare `#number`) and where it landed. **If you point at working it later, lead with `/clear`**: the confirmation is a place you point at the next ticket, so the [Session handoff](../../_shared/session-handoff.md)'s `/clear`-first rule applies: a compact, future-conditional one-liner is fine (a capture is not a resolution, so no full labelled block), along the lines of *"to work it later: `/clear`, then `/trailhead:work 45`"* (in the user's conversation language). Never a bare `/trailhead:work 45` with no `/clear`, and (the same post-`/clear` invariant the [Session handoff](../../_shared/session-handoff.md) states) **never place after `/clear` an action the agent must perform that depends on context `/clear` erases**: a capture confirmation points only at user-runnable commands, which satisfies this by construction, so keep it that way. **Whenever the confirmation does point at working the ticket (a `seed` parked on its trigger, or a fog `note`/`idea` that is not a ticket yet, is the case where it does not), anchor the next step to `/trailhead:work <n>`, always:** it is the recommendation (the full engine, which can split and keeps the map record), and the confirmation names it first. `/trailhead:quick <n>` is **only** the off-map / no-split alternative (one alternative, described two ways: `quick` is the same work done off the map, without splitting), offered *alongside* `work`, **never in its place**. This holds for a **whiteboard** capture too, where both apply (`quick` is the no-split variant of `work`): the anchor stays `/trailhead:work <n>` for consistency. Same spirit as chart's codified close (#147).
|
|
6
6
|
|
|
7
7
|
**Language (do not skip):** the captured ticket (title + body) is written in `config.ticket.language` (default `en`), **regardless of the language you are chatting in**. A capture fired in Italian still lands as an English ticket. This is the easiest place to slip, because a capture echoes the user's one-line phrasing; translate it, do not mirror it. See the *Ticket language* rule in `../../_shared/ticket-language.md`.
|
|
8
8
|
|
|
@@ -41,3 +41,5 @@ Run a standalone **Grilling** (plus **Domain vocabulary**) session on a decision
|
|
|
41
41
|
## First-time config offer (end of chart / adopt)
|
|
42
42
|
|
|
43
43
|
Charting hand-resolves nothing, so a fresh chart or adopt closes with this offer. When a project has a map but **no `.trailhead/config.json` yet**, put an **explicit two-way choice** to the user and wait for their pick: **(a) accept as-is** (continue on the current or proposed values, which writes an empty `{}`), or **(b) run `/trailhead:config`** to configure everything through the guided menu (models / design / TDD / ...). **Always name both paths and let the user choose; never silently take one.** You may show the values you would default to, but frame the close as this binary: **do not** pre-fill the config keys and ask for an open-ended inline edit, and **do not** interview the user key-by-key inline (that is `/trailhead:config`'s job). Ask this **only once**: running config writes the file, and *accept as-is* writes an empty `{}`, so once `.trailhead/config.json` exists it is never offered again (a `{}` file means "reviewed, using defaults").
|
|
44
|
+
|
|
45
|
+
After this offer, the chart / adopt close renders the codified **next-step block** (`/trailhead:work`-anchored, `/clear` first). Its authoritative wording is single-sourced in **`references/charting.md`** (Mode 1 step 6, reached by Mode 1-bis), so read it there rather than restating its rules here.
|
|
@@ -8,11 +8,21 @@ The user invokes with a loose idea.
|
|
|
8
8
|
0. **Ensure a tracker repo exists (offer to create one if missing).** Everything trailhead writes lives on a GitHub repo's Issues, so before charting, confirm the current directory has one. Detect it with `gh repo view`: on success a GitHub repo with a remote is present, so say nothing and proceed. If there is **no GitHub repo** (no `origin` / GitHub remote, or `gh repo view` fails), **stop and ask the user** whether to create one, offering **private (default)** or **public**. On their choice: if the directory is not yet a git repo, `git init` and make an initial commit first; then `gh repo create <name> --private|--public --source=. --remote=origin --push` (take `<name>` from the directory, confirmed with the user), so the repo exists on GitHub with `origin` set. Charting continues only once a GitHub repo is in place: every step below writes to its Issues. If the user declines to create one, there is nowhere to chart, so stop and say so. This never fires when a repo already exists (`/trailhead:adopt` and re-charting on an existing repo skip straight past it).
|
|
9
9
|
1. **Name the destination.** Run the **Grilling** + **Domain vocabulary** techniques to pin down what this map tends toward. The destination fixes the scope, so it's settled first. Default: a working artifact.
|
|
10
10
|
2. **Map the frontier.** Grill again, **breadth-first**: **enumerate every functional area the destination implies** and **capture each one (headed for a fog entry under *Not yet specified* at minimum) before deep-diving any single area**. Fan out across the whole space rather than deep on one thread, surfacing the open decisions and the first steps takeable now. **The deliverable is the coverage the map will carry, not the grilling transcript: every area the destination implies must land on the map at step 3, as fog or as a ticket, so nothing is silently missing.** **If no fog surfaces** (the way is already clear, the whole thing fits one session) you don't need a map: stop and ask how to proceed. **Harvest parked work:** from **earlier maps on this repo**, gather the artifacts their *Out of scope* `→ future map` breadcrumbs point at (**parked seeds/todos**, see **Out of scope**) **and the ideas** left in their *Not yet specified* fog, then **ask the user which belong in *this* map**; pull the chosen seeds/todos in as tickets and the chosen ideas in as fog (or as tickets, if now sharp enough), and leave (or update the destination on) the rest.
|
|
11
|
-
3. **Create the map** (`trailhead:map`): Destination and Notes filled in, Decisions-so-far empty, the fog sketched into *Not yet specified*. **Every functional area enumerated in step 2 must appear here, as a fog entry or a ticket, before moving on: the map is the coverage deliverable.** Run the **first-use repo setup** (see **Substrate** → `../../_shared/substrate-commands.md`) now: labels **and** the label-guard check/install. Once the map issue has its number `<n>`, **create its per-map label `trailhead:map-<n>`** (every ticket of this map carries it and is a native sub-issue of the map; the map issue itself is found by its number, so it does **not** carry the label; see **Multiple maps on one repo**). **If this repo already has another open `trailhead:map`**, tell the user the new map runs concurrently (its own scoped frontier) and set it as the active map (`.trailhead/active-map`). Then set up **conventions**: if no `trailhead:conventions` issue exists, run a **brief conventions brainstorming** (breadth-first, in the spirit of grilling but short: a handful of questions, not a full session) to surface the project's way of working: `git:` `main`|`pr`, `release:` `command`|`auto`, and `isolation:` `none`|`worktree` (
|
|
11
|
+
3. **Create the map** (`trailhead:map`): Destination and Notes filled in, Decisions-so-far empty, the fog sketched into *Not yet specified*. **Every functional area enumerated in step 2 must appear here, as a fog entry or a ticket, before moving on: the map is the coverage deliverable.** Run the **first-use repo setup** (see **Substrate** → `../../_shared/substrate-commands.md`) now: labels **and** the label-guard check/install. Once the map issue has its number `<n>`, **create its per-map label `trailhead:map-<n>`** (every ticket of this map carries it and is a native sub-issue of the map; the map issue itself is found by its number, so it does **not** carry the label; see **Multiple maps on one repo**). **If this repo already has another open `trailhead:map`**, tell the user the new map runs concurrently (its own scoped frontier) and set it as the active map (`.trailhead/active-map`). Then set up **conventions**: if no `trailhead:conventions` issue exists, run a **brief conventions brainstorming** (breadth-first, in the spirit of grilling but short: a handful of questions, not a full session) to surface the project's way of working: `git:` `main`|`pr`, `release:` `command`|`auto`, and `isolation:` `auto`|`none`|`worktree`|`clone` (default **`auto`**, which decides per ticket at work-start by asking *"will more than one ticket be worked at once on this machine this session?"* and picking `worktree` for a plain repo or `clone` for a submodule superrepo; a project can force `none`/`worktree`/`clone` explicitly), **plus** the narrative a new contributor would need: release/deploy specifics (environments, manual steps, OTA vs full build, versioning), tooling that must be driven a certain way (a specific MCP/CLI, e.g. a Lovable project piloted via its MCP), code & style rules, testing/UAT norms. Distil into the issue (header + prose); link it from the map's Notes. Don't invent: leave out or ask what you can't determine. If one already exists, just link it (reuse across maps). Finally, **ensure the repo's `trailhead:dashboard` index issue exists** (see **The dashboard**): create and pin it if missing (the fixed 3rd pinned slot, alongside codebase + conventions), then **write it to list this map** (charting a map is a structural event); if it already exists, refresh it.
|
|
12
12
|
4. **Create the tickets you can specify now** as child issues (each with `trailhead:ticket` + its type + this map's `trailhead:map-<n>` label, **plus `trailhead:blocked` when the mapped dependency graph already shows it has a blocker**, and set as a **native sub-issue** of the map, see **Child→map link** under Substrate), then wire the blocking in a **second pass** (issues need ids before they can reference each other): for each ticket **read its Question and wire a blocker for every still-open ticket whose output it consumes** (don't stop at the first/obvious one) by wiring its `## Blocked by` line + native dependency together (moves 1-2 of the three-move wiring; the `trailhead:blocked` label, move 3, already rode in at creation, see **Substrate**). **Set the blocked label at creation, not via a post-creation `--add-label` pass:** a bulk `--add-label` burst spawns one repo-side label-guard run per ticket and reddens the Actions history with sheddable no-op runs; riding the label in the ticket's `issues.opened` event avoids that entirely. Wiring sorts them into frontier (unblocked) and blocked; the rest stays fog.
|
|
13
13
|
5. **Fire the research subagents.** For each `research` ticket created, run the **Research** technique in parallel, findings on a `research/<name>` branch with a pointer from the ticket.
|
|
14
14
|
6. **Offer config, then stop.** Charting is one session's work; it hand-resolves nothing. Before ending, make the **first-time config offer** (see **First-time config offer** in `../SKILL.md`): the project now has a fresh map and no `.trailhead/config.json`, so put the **two-way choice** to the user, **accept as-is** (writes `{}`) or **run `/trailhead:config`** for the guided menu, and let them pick. Name both paths; don't pre-fill the config keys and ask for an inline edit, and don't interview them key-by-key here (that's `/trailhead:config`'s job). Once the file exists it's never offered again. **No `trailhead:codebase` issue is created here**: greenfield has no code to map yet. It's created later, once the repo has substantial code (see Mode 2 step 1 and the **Codebase** row under Substrate), so at chart two of the three permanent pins (the dashboard + conventions) are filled and the codebase slot fills in later. Maps are never pinned.
|
|
15
15
|
|
|
16
|
+
**Next-step block (the close of chart, and of adopt).** Charting hand-resolves nothing, so the **Session handoff** ritual (`../../_shared/session-handoff.md`) does not fire here; render this codified next-step block instead, **right after the first-time config offer above**, styled on the handoff's own next-step block and written in the user's conversation language. Newly charted tickets are **map-frontier** tickets, so the primary move is `/trailhead:work` (the full engine: it can split and keeps the map record), with `/trailhead:quick` only as the off-map alternative. Every bullet below is a thing to do **after** the `/clear`, which leads; never point at the next command without it, and never demote `/clear` to a trailing parenthetical.
|
|
17
|
+
|
|
18
|
+
> **Prossimo passo:**
|
|
19
|
+
> - `/clear`: per liberare il contesto
|
|
20
|
+
> - poi `/trailhead:work <numero>` per lavorare **<nome del primo ticket di frontiera>** (è un ticket di mappa: `work` lo porta avanti col motore completo, può splittarlo, tiene il record della mappa)
|
|
21
|
+
> - `/trailhead:map` per la vista completa o `/trailhead:dashboard` per l'indice di tutte le mappe e del whiteboard, se vuoi guardare prima di scegliere
|
|
22
|
+
> - in alternativa `/trailhead:quick "<il pezzo>"` solo per lavorare un pezzo fuori mappa
|
|
23
|
+
|
|
24
|
+
Same rules as the handoff block: **name the ticket, put its number (never the title) in the `/trailhead:work` command**; when several **mutually independent** frontier tickets sit on the frontier, present them as a **set to choose from** rather than crowning one (see **Frontier order carries no priority** in `../../_shared/substrate.md`). **If charting left no unblocked frontier ticket** (everything landed as fog or blocked), point the block at `/trailhead:map` to pick which fog to specify next, exactly as the empty-frontier case in the handoff. This block is **Mode 1's close, single-sourced here**; **Mode 1-bis (adopt) reaches it through its "As Mode 1, steps 4–6" reference**, so it applies to both protocols with no duplicate block.
|
|
25
|
+
|
|
16
26
|
## Mode 1-bis: Adopt an existing project, `/trailhead:adopt`
|
|
17
27
|
When the code already exists (project in progress). Like Mode 1, but start from **reality**, not a blank page. One heavy step at entry, then all lean.
|
|
18
28
|
|
|
@@ -21,4 +31,4 @@ When the code already exists (project in progress). Like Mode 1, but start from
|
|
|
21
31
|
3. **Backfill the decisions already made.** Choices already embodied in the code (now visible from the map) or stated by the user go into `Decisions so far` as **ticket-less** lines (they're already closed), so the map reflects reality and doesn't pretend greenfield. Link to code/commits where useful.
|
|
22
32
|
4. **Map the frontier of the remainder**, **breadth-first as in Mode 1 step 2**: enumerate every functional area still standing between the code and the destination and **land each on the map (fog in *Not yet specified* at minimum) before deep-diving any one**, so the map makes every remaining section visible; the map, not the transcript, is the deliverable to check for that coverage. This yields → tickets specifiable now + fog in *Not yet specified*, then wire the blocking and fire the research. **Harvest parked work** (todos *and* ideas) from earlier maps on this repo (their `→ future map` *Out of scope* lines and their *Not yet specified* fog), asking the user which belong here, as in Mode 1 step 2. As Mode 1, steps 4–6.
|
|
23
33
|
|
|
24
|
-
The tracker is the existing repo's (`gh` in its directory); run the **first-use repo setup** (see `../../_shared/substrate-commands.md`) on first use (missing `trailhead:*` labels **and** the label-guard check/install (adopting an existing repo is exactly when the guard tends to be missing)) and set up the `trailhead:conventions` issue via a **brief conventions brainstorming** (see Mode 1 step 3), but here, **seed the questions from what's discoverable** so you confirm rather than ask blind: an existing `CLAUDE.md`/`AGENTS.md`, CI workflows, the PR-vs-`main` git history, package scripts, deploy config. **Detect a monorepo** (a `pnpm-workspace.yaml`, a `workspaces` field, an `nx.json`/`turbo.json`, or several packages under `packages/`|`apps/`) and, if found,
|
|
34
|
+
The tracker is the existing repo's (`gh` in its directory); run the **first-use repo setup** (see `../../_shared/substrate-commands.md`) on first use (missing `trailhead:*` labels **and** the label-guard check/install (adopting an existing repo is exactly when the guard tends to be missing)) and set up the `trailhead:conventions` issue via a **brief conventions brainstorming** (see Mode 1 step 3), but here, **seed the questions from what's discoverable** so you confirm rather than ask blind: an existing `CLAUDE.md`/`AGENTS.md`, CI workflows, the PR-vs-`main` git history, package scripts, deploy config. **Detect a monorepo** (a `pnpm-workspace.yaml`, a `workspaces` field, an `nx.json`/`turbo.json`, or several packages under `packages/`|`apps/`) and, if found, keep the default `isolation: auto` (it picks `worktree` for such a plain/monorepo checkout at work-start) and note that tickets should carry a `Scope:` line, **unless the buildable units are path-bound** (see below). **Detect git submodules** (a `.gitmodules` file) and, if found, add the prose rule that submodule work is committed **inside the submodule (on a branch), published, then a gitlink bump in the parent**, and that a **submodule-bearing superrepo resolves to `clone`** under the default `auto` (a superproject `worktree` cannot safely carry submodule state, so worktree is escalated to `clone`); a submodule path is a natural `Scope:` (see **Working as a team**). **For path-bound tooling** (a React Native/Expo app, native toolchains, a package whose build depends on a local `node_modules`/bundler cache/absolute-path config): those don't build from a worktree, so for them prefer an explicit **`isolation: clone`** (a per-ticket clone that installs its own deps and builds), or `isolation: none` + **serialize** if the user doesn't want per-ticket clones. Worktree is for units that build from any path. Propose the values you inferred and let the human correct/complete them; create the issue if absent, link it from the map's Notes. Also **ensure the repo's `trailhead:dashboard` index issue exists** (see **The dashboard**): create and pin it if missing (the fixed 3rd pinned slot, alongside codebase + conventions) and write it to list this map.
|
|
@@ -14,7 +14,7 @@ gh issue list --state open --json number,title,author,labels \
|
|
|
14
14
|
```bash
|
|
15
15
|
gh issue list --label trailhead:fog --state open --search "sort:updated-desc"
|
|
16
16
|
```
|
|
17
|
-
This is **how you notice a fog has cleared**: you don't watch each thread; you run the inbox (as you would to triage new issues), and the parked-fog section surfaces the parked items that moved. For each, decide: **graduate** (adopt in place, see Fog of war), **keep parked** (still vague), or **drop** (turned out out-of-scope/dead → `trailhead:out-of-scope`
|
|
17
|
+
This is **how you notice a fog has cleared**: you don't watch each thread; you run the inbox (as you would to triage new issues), and the parked-fog section surfaces the parked items that moved. For each, decide: **graduate** (adopt in place, see Fog of war), **keep parked** (still vague), or **drop** (turned out out-of-scope/dead → `trailhead:out-of-scope`, drop any native sub-issue edge to reclaim a slot, then close: the eager drop in `../../_shared/substrate-commands.md` is 404-tolerant, so a fog with no edge is a no-op). Optionally, a participant can nudge readiness by commenting on the thread; you confirm at graduation.
|
|
18
18
|
|
|
19
19
|
**Out-of-scope lines actually deferred?** Scan the map's *Out of scope* section for lines that are **deferred**, not truly ruled out: ones that **name a gate or trigger** (*gated by X*, *quando X*, *dipende da X*), carry a **this-only scope qualifier** (*per questo slice*, *for now*, *in this milestone*, not for the product), describe a **feature wanted later** (already annotated `→ future map`, or plainly still on the roadmap), or **are the gate of an existing `trailhead:seed`**. That last one is a **near-deterministic check**: cross-reference every open `trailhead:seed`'s trigger / `## Blocked by` against the *Out of scope* lines; a line some seed waits on is a **live gate**, so it cannot be "ruled out" and the cluster is **mis-wired** (the gate was left behind), re-route it. A line **decided against for the product** (won't be built, wrong approach, no "for this slice" qualifier) stays put; and a line **already properly parked** (annotated `→ future map` with its dependent seeds wired to a live trigger) is settled, skip it. Only **un-parked** deferred lines surface here. For each deferred line, **ask the user** (never decide silently) how to re-route it, `seed` / `idea` / `todo`, and **which map** it belongs to (this one, or a future / other map), then apply the **out-of-scope-vs-deferred** rules (see `../../_shared/out-of-scope.md`): **re-route the gate first**, tie its dependents to the gate's new form with a **live trigger** (never a dead "ruled out" line), and **move the whole cluster together**. Update or remove the *Out of scope* line to match where each item went (a future-map item stays as an annotated `→ future map` line; an item pulled into this map leaves *Out of scope* entirely).
|
|
20
20
|
|
|
@@ -22,7 +22,7 @@ For each issue in the first two sections (incoming issues, parked fog), decide,
|
|
|
22
22
|
|
|
23
23
|
- **Adopt → a ticket.** Integrate it *in place* (same issue number, so the reporter stays credited): append a trailhead block to the body: `Parent: <map name+link>`, `Adopted-from: opened by @<author>`, a reframed `## Question` (the decision/build the *map* will act on), and `## Blocked by`, **without erasing the reporter's original text above it**. Apply `trailhead:ticket` + the right type label (`bug`/`decision`/`build`/`research`/`task`) as a write-access maintainer (which also satisfies Trust & provenance), plus the map's `trailhead:map-<n>` label + native sub-issue edge (like any ticket, so it joins the right map's frontier); add `trailhead:blocked` if it has an open blocker. It lands on the frontier. Comment to the reporter that it's been picked up.
|
|
24
24
|
- **Fog.** In scope but not sharp enough to ticket → **keep the issue open and label it `trailhead:fog`** (parked: still visible and tracked, but the `trailhead:*` label drops it out of the inbound query, so it won't re-surface at every triage). Then use the open issue as a **clarification space**: post a comment with the questions that would make it concrete, in the spirit of **Grilling**, but async ("what would *done* look like? which users? where does it show?"), and invite the reporter and anyone interested to refine it in the thread. The fog dissipates through that conversation as much as through other work landing; others can carry the discussion with the reporter without you in the loop. **You don't lose it**: `gh issue list --label trailhead:fog` is the durable list, and the reporter follows their own open issue. When the thread (or the wider work) has made the question sharp, **adopt it in place**: swap `trailhead:fog` for `trailhead:ticket` + the type label, add `Parent:` and the now-answerable `## Question`, and it lands on the frontier: same issue, same reporter, the clarifying discussion preserved above.
|
|
25
|
-
- **Out of scope.** Beyond the destination → label `trailhead:out-of-scope`, close with the why.
|
|
25
|
+
- **Out of scope.** Beyond the destination → label `trailhead:out-of-scope`, **drop its native sub-issue edge** to reclaim a slot (the eager drop in `../../_shared/substrate-commands.md`, 404-tolerant, so an un-adopted inbound issue with no edge is a no-op), then close with the why.
|
|
26
26
|
- **Duplicate.** Link the existing ticket and close.
|
|
27
27
|
- **Needs info.** Ask the reporter for a repro or specifics; leave it open.
|
|
28
28
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: trailhead-manage
|
|
3
3
|
user-invocable: false
|
|
4
|
-
description: "trailhead manage cluster: the admin verbs that keep a trailhead install healthy. Show the effective config or change it through a guided menu (config); check for a newer trailhead and install it where that is safe (update). A cohesion cluster of the trailhead skill split, loading the shared `_shared/` core; reached through the `/trailhead:<verb>` command wrappers and the bare `/trailhead` dispatcher. Not auto-invoked: it runs only when one of these verbs is dispatched."
|
|
5
|
-
argument-hint: "[config|update] [get|set …]"
|
|
4
|
+
description: "trailhead manage cluster: the admin verbs that keep a trailhead install healthy. Show the effective config or change it through a guided menu (config); check for a newer trailhead and install it where that is safe (update), and prune a map's sub-issue edges to reclaim slots under GitHub's 100-cap (prune). A cohesion cluster of the trailhead skill split, loading the shared `_shared/` core; reached through the `/trailhead:<verb>` command wrappers and the bare `/trailhead` dispatcher. Not auto-invoked: it runs only when one of these verbs is dispatched."
|
|
5
|
+
argument-hint: "[config|update|prune] [get|set … | map]"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
`trailhead-manage` is the **manage cluster** of the trailhead skill: the admin verbs that keep a trailhead install healthy. `config` shows the effective config or changes it (a guided menu, or `get` / `set <key> <value>`); `update` checks for a newer trailhead and installs it where that is safe. Everything lives on the GitHub Issues; the repo holds code only.
|
|
8
|
+
`trailhead-manage` is the **manage cluster** of the trailhead skill: the admin verbs that keep a trailhead install healthy. `config` shows the effective config or changes it (a guided menu, or `get` / `set <key> <value>`); `update` checks for a newer trailhead and installs it where that is safe; `prune` reclaims a map's native sub-issue slots under GitHub's 100-cap. Everything lives on the GitHub Issues; the repo holds code only.
|
|
9
9
|
|
|
10
10
|
## Load first, in order
|
|
11
11
|
|
|
@@ -15,9 +15,10 @@ These admin verbs resolve no ticket and set up no isolation workspace.
|
|
|
15
15
|
|
|
16
16
|
## Routing: verb to engine
|
|
17
17
|
|
|
18
|
-
The **first word** of the arguments is the verb (`config` or `
|
|
18
|
+
The **first word** of the arguments is the verb (`config`, `update`, or `prune`); the rest is that verb's argument (`get`, `set <key> <value>`, a map, or empty).
|
|
19
19
|
|
|
20
20
|
- **`config [get|set …]`** to **`../_shared/configuration-reference.md`**: the config keys, their semantics, and the guided menu setup. With no argument it runs the guided setup; `config get` prints the merged config read-only (project `.trailhead/config.json` over global over defaults, showing which source wins each key); `config set <key> <value>` writes one key directly (to the project `.trailhead/config.json`, or to `~/.claude/trailhead/config.json` with `--global`). Read that file and follow it.
|
|
21
21
|
- **`update`** to **`references/updating.md`**: the update protocol (detect the install channel, compare the installed version with the latest from the matching source, install the newer one where it is safe). Read that file and follow it.
|
|
22
|
+
- **`prune [map]`** to **`references/pruning.md`**: reclaim native sub-issue slots on a map near or at GitHub's 100 sub-issue cap by dropping the edges of closed and orphan tickets and re-adding missing ones, keeping every reference (Parent line, Decisions-so-far pointer, map label). With no argument it prunes the active map. Read that file and follow it.
|
|
22
23
|
|
|
23
24
|
The full config **key reference** is a single source, shared with the rest of the skill: `../_shared/configuration-reference.md` (keys + guided setup).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Pruning a map's sub-issue edges
|
|
2
|
+
|
|
3
|
+
`/trailhead:prune [map]` reclaims native sub-issue slots on a map that is near or at **GitHub's hard cap of 100 native sub-issues per issue**, so new tickets can keep attaching to the map's visual sub-issue tree. It never loses anything from the record: it drops only the **native edge**, and every reference stays (the ticket's `Parent:` line, the map's `Decisions so far` pointer, and the `trailhead:map-<n>` label).
|
|
4
|
+
|
|
5
|
+
**Why this is safe.** The frontier is computed from the `trailhead:map-<n>` **label**, never from the native edge (see `../../_shared/substrate.md`). So the native sub-issue edge is purely GitHub's visual rendering: dropping the edge of a closed ticket, or of an open ticket that no longer belongs to this map, changes nothing in engine behaviour (selection, blocking, resolution). Only GitHub's sub-issue tree and progress bar are affected, and a dropped edge can always be re-added later.
|
|
6
|
+
|
|
7
|
+
## 1. Resolve the target map
|
|
8
|
+
|
|
9
|
+
The argument is the map (number or URL). With no argument, use the active map (`.trailhead/active-map`); if there is no active map and more than one `trailhead:map` is open, list them and ask which. Set/refresh `.trailhead/active-map` to the map being pruned (see **Multiple maps on one repo** in `../../_shared/multi-map.md`).
|
|
10
|
+
|
|
11
|
+
## 2. Read the count and list the edges
|
|
12
|
+
|
|
13
|
+
Read the map's native sub-issue count (one cheap field), then list every native sub-issue of the map, **paginated** (never a single page), capturing each one's number, id, and state. The exact `gh` commands are in `../../_shared/substrate-commands.md` (section "Sub-issue cap and pruning"); this protocol only says how to use them.
|
|
14
|
+
|
|
15
|
+
## 3. Classify each edge (bidirectional reconcile)
|
|
16
|
+
|
|
17
|
+
Classify every edge by its target, so the pass converges the map's edges to exactly "its open, still-labelled tickets":
|
|
18
|
+
- **Target is CLOSED** -> **remove** the edge (reclaim the slot; references kept). This is mostly resolved tickets: since #151 a superseded / out-of-scope close already sheds its edge eagerly at close (see the eager drop in `../../_shared/substrate-commands.md`), so a closed ticket still carrying its edge here is normally a resolved one (or a pre-#151 / fallback legacy edge).
|
|
19
|
+
- **Target is OPEN and still carries `trailhead:map-<n>` for this map** -> **keep** the edge.
|
|
20
|
+
- **Target is OPEN but does NOT carry `trailhead:map-<n>` for this map** (an orphan: a ticket that no longer belongs to this map) -> **remove** the edge. It does not belong here; its own map, if any, holds its own edge. Confirm the label is genuinely absent before removing (read the ticket's labels), so a ticket that is legitimately mid-wiring is never stripped.
|
|
21
|
+
|
|
22
|
+
Also compute the **missing** set: open tickets that **do** carry `trailhead:map-<n>` for this map but have **no** edge in the list above (re-derived by diffing, so no stored state is needed).
|
|
23
|
+
|
|
24
|
+
## 4. Preview and confirm (never mutate silently)
|
|
25
|
+
|
|
26
|
+
Show a preview and wait for an explicit go-ahead before any change:
|
|
27
|
+
- edges to **remove**: how many closed, how many orphan-open, and the resulting `total` (X -> Y);
|
|
28
|
+
- edges to **add**: how many missing edges for open labelled tickets, and how many will fit under the cap after the removals.
|
|
29
|
+
|
|
30
|
+
This choice is advisory (a process choice): offer the delegate option per `../../_shared/choices.md`. On anything but an explicit yes, stop and change nothing.
|
|
31
|
+
|
|
32
|
+
## 5. Apply
|
|
33
|
+
|
|
34
|
+
Remove each classified-for-removal edge with the DELETE from `../../_shared/substrate-commands.md` (section "Sub-issue cap and pruning"), passing the edge's internal `id` captured in step 2. Then **re-read** `sub_issues_summary.total` (do not trust the pre-removal count) and add the missing edges up to the remaining capacity (100 - current total), each independently, with the re-add POST from that same section. The missing set came from a label diff, so it holds issue *numbers*, not internal ids: resolve each missing ticket's internal id first (the POST snippet in the cookbook shows the `$(gh api .../<ticket> --jq .id)` substitution).
|
|
35
|
+
|
|
36
|
+
Because each id came from step 2's listing, a 404 here means the edge is no longer native (a detach that raced this run, for example a concurrent prune): the slot is already free, so count it toward the reconciled/removed result and keep going, per the 404-tolerance documented in the cookbook. That is distinct from a genuine failure.
|
|
37
|
+
|
|
38
|
+
Report **per edge**: each removal and each add, success or failure. A single edge failing (for example a concurrent creation consuming the last slot) is never fatal: record it and continue.
|
|
39
|
+
|
|
40
|
+
## 6. Report
|
|
41
|
+
|
|
42
|
+
Say plainly what happened: slots reclaimed (closed + orphan removed), edges re-added, and the new `total` against 100. An already-detached edge caught as a 404 is a clean no-op, not a failed edge; keep it out of any failure count, distinct from a genuine edge failure (a transient API error or a permissions error), which is what "record it and continue" covers. If open labelled tickets still lack an edge because capacity ran out, **list them** and note that a later `/trailhead:prune` (once more tickets close) will attach them. Never close, reopen, relabel, or otherwise touch any ticket: prune only ever adds or removes the map's native sub-issue edges.
|
|
43
|
+
|
|
44
|
+
## Notes
|
|
45
|
+
|
|
46
|
+
- Prune touches **only this map's** native edges; it never mutates another map's tree, and never a ticket's body, labels, or state.
|
|
47
|
+
- The dashboard flags a map `near cap · <total>/100` (total >= 90) or `at cap` (total >= 100), appending `· prunable` when the map has closed edges to reclaim, and offers `/trailhead:prune`; it never auto-prunes (see the dashboard render in `../../trailhead-view/SKILL.md`).
|
|
48
|
+
- At edge-add time the engine pre-checks the cap and offers prune instead of silently dropping the edge (see the child->map procedure in `../../_shared/substrate-commands.md`).
|
|
@@ -69,7 +69,7 @@ So `Decisions so far` / `Not yet specified` / `Out of scope` refer to these bloc
|
|
|
69
69
|
A repo's trailhead surface is spread across issues: one or more maps, a whiteboard, an inbox. The **dashboard** is the single pinned entry point that indexes all of it: a permanent, repo-scoped issue labelled **`trailhead:dashboard`**, created once at the first chart/adopt and occupying the **fixed 3rd pinned slot** alongside `trailhead:codebase` and `trailhead:conventions` (see `../_shared/substrate.md`). Maps themselves are **never pinned**: a pin is server-side and global to the repo, but the active map is client-side per-checkout state (`.trailhead/active-map`, gitignored), so no single map is "the" map to pin. The dashboard gives one stable pinned index that scales to any number of maps and to the whiteboard.
|
|
70
70
|
|
|
71
71
|
It holds:
|
|
72
|
-
- a link to **every open `trailhead:map`** (found via the `trailhead:map` label); GitHub renders each map's sub-issue **progress bar natively**, so the dashboard only links, it never recomputes per-ticket progress. **Per open map it also runs one cheap `0 open scoped issues?` pre-filter** and, for a candidate confirmed to have no remaining fog (see the exhaustion check in `../_shared/substrate-commands.md`), flags an **exhausted-but-open** map (destination reached, map issue never closed) as `exhausted · closeable`, so the render can offer to close it (below);
|
|
72
|
+
- a link to **every open `trailhead:map`** (found via the `trailhead:map` label); GitHub renders each map's sub-issue **progress bar natively**, so the dashboard only links, it never recomputes per-ticket progress. **Per open map it also runs one cheap `0 open scoped issues?` pre-filter** and, for a candidate confirmed to have no remaining fog (see the exhaustion check in `../_shared/substrate-commands.md`), flags an **exhausted-but-open** map (destination reached, map issue never closed) as `exhausted · closeable`, so the render can offer to close it (below). It also flags each open map **`near cap · <total>/100`** (total >= 90) or **`at cap`** (total >= 100) from `sub_issues_summary.total`, appending **`· prunable`** when the map has closed edges to reclaim, so the render can offer `/trailhead:prune` (below);
|
|
73
73
|
- the **whiteboard** as its own section (frontier / in-progress / blocked, or a link to the `whiteboard` view);
|
|
74
74
|
- dynamic **counts**: untriaged inbox size, whiteboard frontier size.
|
|
75
75
|
|
|
@@ -77,6 +77,8 @@ It holds:
|
|
|
77
77
|
|
|
78
78
|
**Exhausted-but-open maps: flag and offer to close.** The one-shot "close this map?" prompt fires only at the exhaustion moment (a `work` handoff, or a `map` render on the exhausted map). If that moment was skipped, or the user chose to leave the map open, an exhausted map lingers with nothing re-surfacing it. So the dashboard is the recurring surface that catches it: for every open map flagged `exhausted · closeable` (the `0 open scoped issues?` pre-filter confirmed against remaining fog, see `../_shared/substrate-commands.md`), the render **offers to close it** (route any un-parked deferred *Out of scope* line first, exactly as the `map` exhaustion check does), `gh issue close` on a yes, leave it open on a no. Never auto-close: flag + offer only. The count runs per map, but the body read (fog confirm + deferred routing) is bounded to the exhausted candidates, so this stays within the dashboard-only-links spirit.
|
|
79
79
|
|
|
80
|
+
**Near/at-cap maps: flag and offer to prune.** The same advisory posture applies to GitHub's 100 native sub-issue cap: a map flagged `near cap · <total>/100` or `at cap · prunable` gets the render's offer to run `/trailhead:prune`, never an auto-prune (see the cap-aware edge wiring and the `gh` mechanics in `../_shared/substrate-commands.md`).
|
|
81
|
+
|
|
80
82
|
**Freshness: structural events + on demand.** The pinned issue is rewritten when the surface changes **structurally** and **on demand** via `dashboard`. The structural events are: a map is **charted** or **exhausted** (a map appears or disappears), and a **whiteboard ticket is born or resolved**. It is **not** rewritten on every *map* ticket resolve: that would churn a pinned issue (and its notifications) constantly, and each map's native sub-issue progress bar already tracks its per-ticket progress. **The whiteboard has no such native progress bar** (its tickets are sub-issues of nothing), so the dashboard's whiteboard section and count are the only place whiteboard state shows: a whiteboard ticket appearing or being resolved *is* a structural change to the indexed surface, and refreshes the dashboard. **One timing exception for `quick`:** a ticket born by `quick "<text>"` is born *and* resolved in the same session, so refreshing at its birth would only be duplicated (and superseded) by the resolution refresh a moment later. So a `quick`-born ticket refreshes at the session **handoff** (Resolve), or at **`/trailhead:pause`** if the session pauses before Resolve, **not** at the birth instant; a ticket born by a **terminal capture** (a `bug`/`todo`/`idea` routed to the whiteboard, whose session ends at creation) still refreshes at birth, since that is its only refresh point. Links + counts stay good enough refreshed at these events and on demand.
|
|
81
83
|
|
|
82
84
|
**Self-heal (read-only entry points).** A repo that adopted trailhead before the dashboard existed, or one whose pin was later dropped, has no pinned index until the next chart/adopt or an explicit `dashboard`. To close that gap, the frequent **read-only entry points self-heal the pin**: the bare `/trailhead` smart entry, the `map` render, and the `whiteboard` render each ensure the dashboard exists and is pinned before rendering, idempotently and cheaply, mirroring the "ensure the label exists before applying it" pattern: ensure the `trailhead:dashboard` label, then
|
|
@@ -90,7 +92,7 @@ It holds:
|
|
|
90
92
|
|
|
91
93
|
## Render the whiteboard: `whiteboard`
|
|
92
94
|
|
|
93
|
-
Not all work belongs to a map. A **bug**, a **todo**, or a one-off **task** may be about something else entirely, or simply not worth charting a map for. The **whiteboard** is where that loose work lives: tickets labelled **`trailhead:whiteboard`**, map-less (no `trailhead:map-<n>` label, no `Parent:` line, no native sub-issue edge). A ticket is on a map **or** on the whiteboard, never both. Loose tickets are created there by a capture routed to the whiteboard or born there by `quick`, and worked with `
|
|
95
|
+
Not all work belongs to a map. A **bug**, a **todo**, or a one-off **task** may be about something else entirely, or simply not worth charting a map for. The **whiteboard** is where that loose work lives: tickets labelled **`trailhead:whiteboard`**, map-less (no `trailhead:map-<n>` label, no `Parent:` line, no native sub-issue edge). A ticket is on a map **or** on the whiteboard, never both. Loose tickets are created there by a capture routed to the whiteboard or born there by `quick`, and worked with `work <n>` (the anchor: works the ticket whole and can split) or `quick <n>` (the no-split variant); the capture and work clusters own those paths. This engine only **renders** the whiteboard.
|
|
94
96
|
|
|
95
97
|
It has its **own frontier**, off every map's (see **Map frontier vs whiteboard frontier** in `../_shared/substrate.md`):
|
|
96
98
|
```bash
|
|
@@ -37,26 +37,28 @@ This guard is the **explicit-number paths only**. Bare `work` selects from the l
|
|
|
37
37
|
|
|
38
38
|
The user invokes with a map (URL or number). A ticket is optional: without one, you pick the next decision.
|
|
39
39
|
|
|
40
|
-
1. 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 **no `trailhead:codebase` issue** (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**,
|
|
40
|
+
1. 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 **no `trailhead:codebase` issue** (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.
|
|
41
41
|
2. 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:whiteboard` one: `work <n>` works it whole, off the map, with the normal cycle and split allowed; use `quick <n>` for the no-split variant). Otherwise take a frontier ticket to act on, **scoped to this map** (add its `trailhead:map-<n>` label to the frontier query when several maps are live) and **never a whiteboard ticket** (bare `work` stays on the map; the whiteboard frontier is reached via `/trailhead:whiteboard` and `quick`). Bare `work` may act on the first ticket the query returns, but that order is `gh`'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).
|
|
42
|
-
- **First,
|
|
43
|
-
- **
|
|
42
|
+
- **First, resolve the effective isolation mode.** The `isolation:` header defaults to **`auto`** (an absent key means `auto`; the engine never writes the key). Under **`auto`**, 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 like `none` (the scope-free check below applies). **Yes**: resolve by repo shape, a plain repo -> `worktree`, a **superrepo** (a `.gitmodules` at the checkout root declaring at least one submodule) -> `clone`; that "yes" **is** the clone gate's answer, so the resolved `clone` path 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 **explicit** `none`/`worktree`/`clone`, honour it as-is (never subject it to auto's ask): explicit `clone` keeps its own gate, explicit `worktree` is eager. See `../_shared/teamwork.md`.
|
|
43
|
+
- **When the resolved mode is the current checkout (`isolation: none`, or `auto` answered 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`.
|
|
44
44
|
- **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-ticket` as **one line** `#<n> <ticket title>`. **Keep it gitignored** (add `.trailhead/session-ticket` to that repo's `.gitignore` if 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-map`** marker (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`.
|
|
45
|
-
- **
|
|
46
|
-
3.
|
|
47
|
-
4.
|
|
48
|
-
5.
|
|
49
|
-
6.
|
|
45
|
+
- **When the resolved mode is `worktree` or `clone` (explicit, or `auto` answered 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: worktree` is escalated to `clone`** (a superproject worktree cannot safely carry submodule state; see `../_shared/teamwork.md`). For `worktree` (no submodules), create/enter this ticket's `git worktree` on a `trailhead/t<n>` branch for the package/path the ticket's `Scope:` names (a submodule-bearing repo would have escalated to `clone` above), e.g. `git worktree add ../<repo>-t<n> -b trailhead/t<n>`; for `clone`, make an independent working copy at `../<repo>-t<n>` (branch `trailhead/t<n>`) by whichever is faster: `git clone` (add `--recurse-submodules` when the repo has submodules) + the install step, or a **folder copy that brings `node_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-dir` resolves inside it, and the same inside every submodule, else commits leak back to the original); a clone with submodules is provisioned via `git submodule update --init --recursive` (nested included), each scoped submodule pinned at the superproject's recorded commit. **If this `clone` was reached via `auto`'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 explicit `isolation: clone`, run the gate as follows.** **Because `clone` is 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, and `git:` still decides the branch as usual); if **yes**, clone as above. The gate is **`clone`-only**: `worktree` is 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 (the `clone` gate-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`.
|
|
46
|
+
3. **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 in `references/auto.md`: `auto` proceeds (takes the delegate option) without stopping to brief.
|
|
47
|
+
4. 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 a `decision` ticket, run **Grilling** + **Domain vocabulary**. If in doubt on a `build`, **stop and ask** (see the Discuss step): never auto-grill.
|
|
48
|
+
5. 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 its `trailhead:blocked` label so it graduates onto the frontier.
|
|
49
|
+
6. 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 a `seed`/`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.
|
|
50
|
+
7. **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 `/clear` first** (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:map` if 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** (a `seed` gated on its trigger, an `idea`, or a `todo`, 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 close` it 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.
|
|
50
51
|
|
|
51
52
|
## Work one ticket whole, off the map: `quick`
|
|
52
53
|
|
|
53
54
|
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>"`.
|
|
54
55
|
|
|
55
|
-
It runs the **full engine** (the `build`/`bug` cycle in `references/ticket-engines.md`: Discuss → Plan → Execute → Verify → Resolve), differing from `work` on
|
|
56
|
+
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:
|
|
57
|
+
- **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.
|
|
56
58
|
- **Grill only if needed** (Discuss): never auto-grill; start from Plan and stop to ask only if a blocking ambiguity surfaces, exactly as the `build` Discuss. The user may ask to grill at any time.
|
|
57
59
|
- **Never splits.** If the ticket turns out large, work it whole anyway: that is the point of `quick`. Do not `split` it 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.)
|
|
58
|
-
- **No map narrative book-keeping.** A whiteboard ticket has no map, so there is no `Decisions so far` update, no fog to graduate, no frontier re-scan. Everything else holds: atomic commits with `Refs: #<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** (remove `trailhead:blocked` from any dependent whose last blocker just closed, **Work the map** step
|
|
59
|
-
- **Same isolation setup as `work`.** `quick` **writes the `.trailhead/session-ticket` marker 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). Under `isolation: worktree`/`clone` it
|
|
60
|
+
- **No map narrative book-keeping.** A whiteboard ticket has no map, so there is no `Decisions so far` update, no fog to graduate, no frontier re-scan. Everything else holds: atomic commits with `Refs: #<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** (remove `trailhead:blocked` from 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 prefer `work <n>` for a map ticket that has dependents or belongs in the map record.
|
|
61
|
+
- **Same isolation setup as `work`.** `quick` **writes the `.trailhead/session-ticket` marker 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). Under `auto` it runs the same work-start concurrency ask **before any edit** and resolves to current-checkout / worktree / clone by the same rule; under an explicit `isolation: worktree`/`clone` it sets up the ticket's `trailhead/t<n>` workspace before any edit (including the submodule-forces-`clone` escalation and submodule provisioning) and re-checks the claim; under `isolation: none` (or `auto` answered no), the scope-free check applies too.
|
|
60
62
|
- **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 work `quick` exists 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`.
|
|
61
63
|
- **Honours `config.effort`.** `quick` is the natural home for the cost-aware path: under `effort: lean` it runs the **same size triage and ask-to-skip offer defined canonically under effort in `../_shared/configuration-reference.md`** (the same one the `build`/`bug` engine 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 (atomic `Refs:` commits, tests/criterion, post-commit self-check, Resolve) is never skipped. Under `effort: standard` (default), and on any non-yes answer, the full cycle runs, exactly as today.
|
|
62
64
|
|
|
@@ -19,7 +19,7 @@ Independent of and **above** the advisory boundary below, `auto` **never perform
|
|
|
19
19
|
|
|
20
20
|
When the next step of a ticket would trip the rail, auto **sets that ticket aside and continues** (it does not perform the action and does not block the whole run): it hands the outward action back to the human, recorded in the run-end summary. **Atomic local commits to `main` are not rail-tripping**: they are the normal Execute and continue automatically. The rail is **hardcoded**, not a disableable toggle.
|
|
21
21
|
|
|
22
|
-
**Under a `git: pr` convention the rail bites at integration (by design).** When the repo's `trailhead:conventions` sets `git: pr` (or a ticket works in an `isolation: worktree`/`clone` branch), finishing a `build`/`bug` ticket ends in a **branch push + opening a PR**, both rail actions. So on such a repo auto drives each ticket through its **whole local cycle** (discuss / plan / execute / verify, atomic commits on the ticket's branch) and then **sets the push / PR / integration step aside for the human**, listing every ready-to-integrate branch in the run-end summary. The dependents of a ticket whose PR is not yet merged stay blocked until the human integrates, so a run on a `git: pr` repo drains less of the frontier per invocation than one on `git: main` (where a resolved ticket closes and unblocks immediately). This is the rail working as intended, not a failure; auto is simply most productive under `git: main`.
|
|
22
|
+
**Under a `git: pr` convention the rail bites at integration (by design).** When the repo's `trailhead:conventions` sets `git: pr` (or a ticket works in an `isolation: worktree`/`clone` branch), finishing a `build`/`bug` ticket ends in a **branch push + opening a PR**, both rail actions. So on such a repo auto drives each ticket through its **whole local cycle** (discuss / plan / execute / verify, atomic commits on the ticket's branch) and then **sets the push / PR / integration step aside for the human**, listing every ready-to-integrate branch in the run-end summary. The dependents of a ticket whose PR is not yet merged stay blocked until the human integrates, so a run on a `git: pr` repo drains less of the frontier per invocation than one on `git: main` (where a resolved ticket closes and unblocks immediately). This is the rail working as intended, not a failure; auto is simply most productive under `git: main`. **Isolation under a run:** `isolation: auto` (the default) resolves to the **current checkout** during an autonomous run without asking (the run is sequential, one ticket at a time, so there is no concurrency and no process question to put to the human); an explicit `worktree`/`clone` still applies as the conventions set it, and its branch push/PR is a rail action set aside for the human exactly as above.
|
|
23
23
|
|
|
24
24
|
## The advisory boundary (the `choices.md` one-line test, no confirm gate)
|
|
25
25
|
Wherever the engine would put a **forced choice** to the human, apply the one-line test in `../../_shared/choices.md`:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ticket types and their engine
|
|
2
2
|
|
|
3
|
-
The per-type engines the `work` verb dispatches to (Mode 2 step
|
|
3
|
+
The per-type engines the `work` verb dispatches to (Mode 2 step 4, in `../SKILL.md`). The `build`/`bug` engine is this cluster's single-cluster reference; `decision`/`research`/`prototype`/`task` round out the six types. `quick` (the off-map, no-split variant) lives inline in `../SKILL.md`.
|
|
4
4
|
|
|
5
5
|
Every ticket is **HITL** (human in the loop, speaking for themselves) or **AFK** (driven by the agent alone). A HITL ticket resolves only through the live exchange: the agent never stands in for the human's side.
|
|
6
6
|
|
|
@@ -29,7 +29,7 @@ The ticket that *builds*. Lean cycle, all in **ticket comments**:
|
|
|
29
29
|
2. **Plan**: a `PLAN` comment: the steps, the seams, the files touched, the verification criteria. Apply **TDD** per `config.tdd` (`seams`/`on`/`off`). Produce this plan via a *planner subagent* on `config.models.plan` (dispatch the **`trailhead-plan`** agent, read-only, passing `config.models.plan` as the model override; inline when the model is unset or equals the session model; Execute then runs as its own subagent, see below). If `config.plan_review` is on, run **Cross-AI plan review** before Execute: send the plan to external AI CLIs and converge on their concerns. Now that the files/seam are known, run the **undeclared-coupling check** (`../../_shared/teamwork.md`) against in-progress tickets. **Plan to sufficiency:** stop once you can name the steps, seams, and files with confidence, and never re-read a file already read in this planning pass; act when you have enough to plan, rather than re-deriving what the pass already established. **The single exception:** under an accepted `lean` skip (see the Size triage above), produce a short inline plan here instead of the planner subagent, and skip Cross-AI plan review; absent that accepted skip, this step runs in full as described.
|
|
30
30
|
3. **Execute**: **if this ticket changes user-facing UI (a new screen, a redesign, a changed layout), run the Prototype technique FIRST, before any real UI code**: surface a mockup to react to, routed by `config.design` (disk / claude.ai/design / stitch) and gated by `config.design.approval` (`explicit` waits for a go-ahead, `auto` proceeds after surfacing). **Route by `config.design` and never shortcut a hosted mode to a local disk mockup**: when `config.design` is `claude.ai/design` or `stitch`, the mockup is built there (reusing a cached `design.project`); `disk` is only for `config.design: disk` or the technique's explicit, announced fallback, never a convenience default. Read `config.design` before producing anything. For a redesign or new screen the default is to offer mockups; don't jump straight to code on a UI ticket. **Once the mockup is approved, reproduce it with 100% fidelity** (layout, spacing, sizing, colour, type, copy, and every state), never a loose approximation "in the spirit of" it; if part of the approved mockup is genuinely infeasible, stop and raise it rather than silently deviate (full statement in the **Prototype** technique, `../../_shared/techniques/prototype.md`). Then implement with **atomic commits** (conventional commits, each with a `Refs: #<n>` trailer for this ticket, see **Principles** in `../../_shared/principles.md`), following the repo's `trailhead:conventions` `git:`, straight to `main` by default, or a feature branch + PR when `git: pr`. **Under `isolation: worktree`, do the work in this ticket's `git worktree`** (`trailhead/t<n>` branch), integrating to the trunk at Resolve (see `../../_shared/teamwork.md`). One commit = one verifiable step. Run this via an **executor subagent on `config.models.execute`** (dispatch the **`trailhead-executor`** agent, full tools that can Edit/Write/commit, passing `config.models.execute` as the model override; inline when the model is unset or equals the session model); the commits land on `main`, visible. **Before installing any new dependency, run the verify-before-install gate** (slopsquatting barrier): check the package's legitimacy (name / typosquat similarity to popular packages, age / first-seen date, download counts, repository metadata present and matching) and **stop and ask the user on a suspicious result** (block-and-ask), never installing on your own judgement. On a host with a hook bus the `trailhead-install-guard` `PreToolUse` hook enforces this, blocking a named public-registry install (npm / PyPI / crates.io / Go / RubyGems) until it is vetted; once vetted, proceed by re-running the exact install command prefixed with `TRAILHEAD_VERIFIED_INSTALL=1`. New scope surfacing mid-execute is captured/split, not worked here.
|
|
31
31
|
4. **Verify**: a `VERIFY` comment: run the tests / the plan's criterion; then the **Code review** technique (its subagent reviews *adversarially*: it doesn't defer to a `CLAUDE.md` convention or a "settled decision" memory), **run in the background and collected by polling, never a blind blocking wait** (a hard engine default on every host, see *Reviews never block the session* in `../../_shared/techniques.md`); **read the review's leading `status:` tag to branch deterministically: `status: clean` -> proceed to Resolve, `status: issues_found` -> iterate the fix-and-re-review loop (or run the HITL checkpoint) before resolving**; then the **Goal-backward verification** technique on `config.models.verify` (does the change deliver what the ticket promised, backward from its `Question` and the `PLAN`'s criteria; the read-only `trailhead-verify` subagent), **run in the background and collected by polling** like Code review, branching on its leading `status:` tag (`status: met` -> proceed, `status: gaps_found` -> loop back to Execute for the gap or run the HITL checkpoint before resolving, `status: inconclusive` as the leading tag (all gaps cleared) -> a per-reason-class HITL checkpoint with no auto-retry, never auto-resolving over the unevaluated goal); then **Acceptance testing** if the change is user-facing (browser-drive it, or **walk the user through a guided UAT conversationally, one step at a time**, never a checklist dumped in a comment): interactive by design, so it runs inline, not backgrounded. Report the outcome honestly (if a test fails, say so). **The single exception:** under an accepted `lean` skip, run a lighter inline review pass instead of the full Code-review subagent and a quick inline goal check instead of the full Goal-backward verification subagent (or skip either only when the change is truly cosmetic); the tests / criterion and the honest outcome report still run regardless. At a HITL checkpoint here, an advisory sub-choice (which fix, how to proceed) **offers** the delegate option, per the injection recipe in `../../_shared/choices.md`; the accept/resolve decision stays the human's.
|
|
32
|
-
5. **Resolve**: a resolution comment with what was done → `gh issue close` → update `Decisions so far` on the map. **Pending-UAT guard (before the close).** If Verify left acceptance testing **deferred** (the human accepted closing now and running the UAT later), **open the follow-up ticket carrying the owed UAT first**, linked back to this parent (a `task` on the frontier or a `trailhead:seed` gated on the run, per *Pending UAT must stay tracked* in `../../_shared/techniques/acceptance-testing.md`); never `gh issue close` on deferred UAT with nothing opened. **Post-commit self-check (before the comment goes out).** For each commit the comment cites, confirm the hash resolves (`git cat-file -e <hash>^{commit}`, or `git show --stat <hash>`) and that every file the comment names as touched actually appears in that commit's diff (`git show --name-only --format= <hash
|
|
32
|
+
5. **Resolve**: a resolution comment with what was done → `gh issue close` → update `Decisions so far` on the map (a body read-modify-write: obey the **Fail-safe body rewrites** rule in `../../_shared/substrate.md`). **Pending-UAT guard (before the close).** If Verify left acceptance testing **deferred** (the human accepted closing now and running the UAT later), **open the follow-up ticket carrying the owed UAT first**, linked back to this parent (a `task` on the frontier or a `trailhead:seed` gated on the run, per *Pending UAT must stay tracked* in `../../_shared/techniques/acceptance-testing.md`); never `gh issue close` on deferred UAT with nothing opened. **Post-commit self-check (before the comment goes out).** For each commit the comment cites, confirm the hash resolves (`git cat-file -e <hash>^{commit}`, or `git show --stat <hash>`) and that every file the comment names as touched actually appears in that commit's diff (`git -c core.quotepath=false show --name-only --format= <hash>`, with `core.quotepath=false` so a non-ASCII filename compares plainly instead of in git's C-quoted form); correct or drop any citation that fails the check, so a hallucinated or mis-stated commit never lands in the tracker record. **If this change materially altered the codebase** (a new module/layer, dependency, seam, or build/test command), **patch the affected facet of the `trailhead:codebase` issue** (see **Codebase** in `../../_shared/substrate.md`) in place (a line or two, not a re-map). Then close the session with the **Session handoff** ritual (`../../_shared/session-handoff.md`): `/clear` + next command.
|
|
33
33
|
|
|
34
34
|
New scope that surfaces mid-build (while planning, executing, or testing) → **capture or split, never expand this ticket in flight** (see **Scope that surfaces while working a ticket**, served by the capture cluster).
|
|
35
35
|
|
|
@@ -48,7 +48,7 @@ Then run the cycle, all in ticket comments:
|
|
|
48
48
|
2. **Diagnose**: run the **Debug** technique. A `DIAGNOSIS` comment with the root cause **stated as a falsifiable hypothesis plus the falsification test that confirmed it**. **Pre-fix checkpoint: only a confirmed cause proceeds to Fix** (a cause that has not survived its falsification test stays in diagnosis, looping the hypothesis steps); the full gate lives in the **Debug** technique (`../../_shared/techniques/debug.md`).
|
|
49
49
|
3. **Fix**: implement the correction with **atomic commits** (each with a `Refs: #<n>` trailer for this ticket, see **Principles** in `../../_shared/principles.md`; per the repo's `trailhead:conventions` `git:`, `main` by default, else a branch + PR; in this ticket's `git worktree` under `isolation: worktree`), via an **executor subagent on `config.models.execute`** (dispatch the **`trailhead-fix`** agent, full tools, passing `config.models.execute` as the model override; inline when unset/same as session). Where sensible, a test that fails before and passes after (**TDD**). The **verify-before-install gate** from the `build` Execute step applies here too: vet any new dependency (typosquat similarity, age, downloads, repository metadata) and stop and ask the user on a suspicious result, before installing.
|
|
50
50
|
4. **Verify**: a `VERIFY` comment: the repro now passes, no regressions; **Code review** on the fix, **run in the background and collected by polling, never a blind blocking wait** (a hard engine default on every host, see *Reviews never block the session* in `../../_shared/techniques.md`); **read the review's leading `status:` tag to branch deterministically (`status: clean` -> Resolve, `status: issues_found` -> iterate the loop or run the checkpoint first)**; then the **Goal-backward verification** technique on `config.models.verify` (does the fix deliver what the ticket promised, backward from its `Question`; the read-only `trailhead-verify` subagent), background and collected by polling, branching on its leading `status:` tag (`status: met` -> proceed, `status: gaps_found` -> loop back to Fix for the gap or run the checkpoint, `status: inconclusive` as the leading tag (all gaps cleared) -> a per-reason-class HITL checkpoint with no auto-retry, never auto-resolving over the unevaluated goal); **Acceptance testing** if the bug was user-facing (browser-drive the fixed flow, or **walk the user through a guided UAT conversationally, step by step**, not a checklist to self-serve): interactive by design, so it runs inline, not backgrounded. At a HITL checkpoint here, an advisory sub-choice (which fix, how to proceed) **offers** the delegate option, per the injection recipe in `../../_shared/choices.md`; the accept/resolve decision stays the human's.
|
|
51
|
-
5. **Resolve**: a resolution comment (cause + fix) → `gh issue close` → update `Decisions so far
|
|
51
|
+
5. **Resolve**: a resolution comment (cause + fix) → `gh issue close` → update `Decisions so far` (a body read-modify-write: obey the **Fail-safe body rewrites** rule in `../../_shared/substrate.md`). **Pending-UAT guard (before the close).** If Verify left acceptance testing **deferred** (the human accepted closing now and running the UAT later), **open the follow-up ticket carrying the owed UAT first**, linked back to this parent (a `task` on the frontier or a `trailhead:seed` gated on the run, per *Pending UAT must stay tracked* in `../../_shared/techniques/acceptance-testing.md`); never `gh issue close` on deferred UAT with nothing opened. **Run the post-commit self-check before posting, exactly as in the `build` Resolve step** (each cited hash resolves and the files it names appear in that commit's diff; correct or drop any citation that fails). **If the fix materially altered the codebase**, patch the affected facet of the `trailhead:codebase` issue (see **Codebase** in `../../_shared/substrate.md`; also a body read-modify-write, same **Fail-safe body rewrites** rule) in place (a line or two). Then close the session with the **Session handoff** ritual (`../../_shared/session-handoff.md`): `/clear` + next command.
|
|
52
52
|
|
|
53
53
|
A **blocking** bug that halts other work is worked immediately; an isolated one is a normal frontier ticket. New scope surfacing while fixing → capture or split, never expand this ticket in flight (see **Scope that surfaces while working a ticket**).
|
|
54
54
|
|