@marcomigozzi/trailhead 0.11.1 → 0.12.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 +6 -3
- package/README.md +6 -3
- package/bin/lib/codex-projection.js +2 -1
- package/bin/lib/codex-projection.test.js +4 -1
- package/bin/trailhead.js +23 -13
- package/bin/trailhead.test.js +307 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/commands/adopt.md +5 -1
- package/plugins/trailhead/commands/audit.md +10 -0
- package/plugins/trailhead/commands/auto.md +5 -1
- package/plugins/trailhead/commands/bug.md +5 -1
- package/plugins/trailhead/commands/config.md +5 -1
- package/plugins/trailhead/commands/dashboard.md +5 -1
- package/plugins/trailhead/commands/grill.md +5 -1
- package/plugins/trailhead/commands/idea.md +5 -1
- package/plugins/trailhead/commands/inbox.md +5 -1
- package/plugins/trailhead/commands/map.md +5 -1
- package/plugins/trailhead/commands/new.md +5 -1
- package/plugins/trailhead/commands/note.md +5 -1
- package/plugins/trailhead/commands/pause.md +5 -1
- package/plugins/trailhead/commands/prune.md +5 -1
- package/plugins/trailhead/commands/quick.md +6 -2
- package/plugins/trailhead/commands/resume.md +5 -1
- package/plugins/trailhead/commands/seed.md +5 -1
- package/plugins/trailhead/commands/split.md +5 -1
- package/plugins/trailhead/commands/ticket.md +5 -1
- package/plugins/trailhead/commands/todo.md +5 -1
- package/plugins/trailhead/commands/update.md +5 -1
- package/plugins/trailhead/commands/whiteboard.md +5 -1
- package/plugins/trailhead/commands/work.md +5 -1
- package/plugins/trailhead/hooks/hooks.json +9 -0
- package/plugins/trailhead/hooks/lib/audit-walk.test.js +802 -0
- package/plugins/trailhead/hooks/lib/mockup-link.js +50 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +56 -4
- package/plugins/trailhead/hooks/lib/model-defaults.json +5 -1
- package/plugins/trailhead/hooks/trailhead-mockup-link-guard.js +95 -0
- package/plugins/trailhead/hooks/trailhead-mockup-link-guard.test.js +145 -0
- package/plugins/trailhead/hooks/trailhead-mockup-link-stop.js +38 -0
- package/plugins/trailhead/hooks/trailhead-mockup-link-stop.test.js +64 -0
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +151 -6
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +56 -0
- package/plugins/trailhead/skills/_shared/choices.md +2 -0
- package/plugins/trailhead/skills/_shared/configuration-reference.md +3 -3
- package/plugins/trailhead/skills/_shared/principles.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +8 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +1 -1
- package/plugins/trailhead/skills/trailhead/SKILL.md +2 -2
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +5 -4
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +132 -0
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +1 -0
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +1 -1
- package/plugins/trailhead/templates/trailhead-audit-walk.js +685 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Auditing closed work and open overlap
|
|
2
|
+
|
|
3
|
+
`/trailhead:audit [map] [--all]` answers two questions about a repo's tickets: is each closed one **still true against today's code** (it hunts **regressions over time**, not just close-time delivery), and are any open ones duplicates or small enough to land together? Unchanged code is checked cheaply and deterministically, with no agent; the full agent verification is kept for the tickets whose code actually moved. The first run (no marker) covers the whole closed history; later runs keep the marker window. It is **read-only until you confirm**: the verification and the comparison only read, the result is one consolidated report, and every change that follows is a separate proposal applied only on an explicit yes. Nothing is written to GitHub before that.
|
|
4
|
+
|
|
5
|
+
All `gh` mechanics are in `../../_shared/substrate-commands.md`; this protocol only says how to use them. Only the purely process choices (the >20 cost-guard ask, the re-run offer for unverifiable tickets) are advisory, so offer the delegate option on those per `../../_shared/choices.md`. The reopen, duplicate-close and merge proposals reshape the map, so they are **human-owned**: do not offer the delegate option on them. The marker advance stays an explicit yes too.
|
|
6
|
+
|
|
7
|
+
## 1. Scope
|
|
8
|
+
|
|
9
|
+
- **No argument** (a whole-repo run): the **open-ticket pass** (step 7) covers every open `trailhead:map` plus the whiteboard (the loose, map-less tickets). The **closed-ticket window** (step 2) is wider: every `trailhead:ticket` closed in the window repo-wide, under any map (open or closed, so a map exhausted and closed between audits is still audited) plus the whiteboard, minus the skip list.
|
|
10
|
+
- **`[map]`** (number or URL): that map's tickets only, meaning the tickets carrying its `trailhead:map-<n>` label, for both the closed and the open pass.
|
|
11
|
+
|
|
12
|
+
`--all` widens the **window** (below), never the scope; it combines with a map.
|
|
13
|
+
|
|
14
|
+
## 2. The window and the cutoff
|
|
15
|
+
|
|
16
|
+
Capture the **cutoff** first, before any listing: the current time, ISO-8601 UTC. Everything this run decides is relative to that one instant.
|
|
17
|
+
|
|
18
|
+
Read the last-audit marker from the pinned dashboard body: a hidden line `<!-- trailhead:last-audit <ISO-8601 UTC> -->` (see Body generation in `../../_shared/substrate-commands.md`, which preserves it through every regeneration).
|
|
19
|
+
|
|
20
|
+
- **Default set**: closed tickets in scope (repo-wide for a whole-repo run, see step 1) with `marker < closedAt <= cutoff`. Narrow with a coarse `closed:>=<marker date>` search, then filter exactly and locally on each ticket's `closedAt`.
|
|
21
|
+
- **No marker, or `--all`**: every closed ticket in scope (repo-wide for a whole-repo run), up to the cutoff.
|
|
22
|
+
|
|
23
|
+
**Skip only what delivered nothing by design**: `trailhead:superseded`, `trailhead:out-of-scope`, and tickets closed with GitHub's native duplicate state reason (read it through GraphQL or `gh --json stateReason`, which gives `DUPLICATE`, or through REST `state_reason`, which gives `duplicate`; the audit's own duplicate close below sets it). Every other closed ticket is checked, **type-aware** (step 4 does the routing):
|
|
24
|
+
|
|
25
|
+
- `build` / `bug`: the **fast path** (no agent) against the commits that carry `Refs: #<n>`; the full agent verification only where that path finds the code changed or has nothing to compare.
|
|
26
|
+
- `decision` / `research` / `task` / `prototype`: a **light inline check** (no agent) of the recorded answer or outcome. Whether code honours a decision is covered by the build tickets that implemented it.
|
|
27
|
+
|
|
28
|
+
## 3. Enumerate exhaustively
|
|
29
|
+
|
|
30
|
+
Every listing the audit relies on (the closed set, the open set, blockers, dependents) must be **complete**, never `gh issue list`'s default page. Every search query carries `is:issue label:trailhead:ticket`, and any REST `/issues` listing drops items that have a `pull_request` key, so a PR is never audited as a ticket. Use `gh api --paginate` (search or issues endpoints, `per_page=100`), or the native `blocked_by` / `blocking` endpoints with `--paginate`. The search API caps at 1000 results and `--paginate` stops there silently, so for every search compare the response `total_count` with the number of items fetched and check `incomplete_results`. If `total_count` exceeds 1000 or `incomplete_results` is true, **split the window by date ranges** (bisect the `closed:` range) until each slice is complete, then merge the slices. If a slice cannot be made complete, or any listing errors or comes back truncated, **stop and report it**, and never propose a marker advance from an incomplete run.
|
|
31
|
+
|
|
32
|
+
## 4. Fast path, light check, and cost guard
|
|
33
|
+
|
|
34
|
+
Run in this order, so the cost guard counts only what the cheap passes leave over. The audit judges **HEAD** (committed code).
|
|
35
|
+
|
|
36
|
+
**Fast path** for `build` / `bug` (deterministic, no agent):
|
|
37
|
+
|
|
38
|
+
- **The ticket's commits** (those carrying `Refs: #<n>`): run the trailer scan **once per audit**, a single `git log --format='%H%x00%(trailers:key=Refs,valueonly,separator=%x2C)' HEAD` pass indexed by ticket number (never one scan per ticket), keeping for each ticket the HEAD-reachable commits whose **parsed `Refs:` trailer** names `#<n>` as a delimited token. `Refs: #17, #192` counts for #192, `#19` never matches `#192`, and a `Refs:` line in the prose body is ignored. The same trailer parse reads a producer's `Refs:` in the attribution walk (step 6). A ticket with **no `Refs:` commits** has nothing to compare: it is **agent-bound** (and can never be classed evolved, step 6).
|
|
39
|
+
- **Baseline**: `<last>` is the newest ticket commit and must contain every other one (`git merge-base --is-ancestor <c_i> <last>` for each). The ticket is **assumed delivered at `<last>`**: its lines are what blame attributes to its commits there. Cheap guard: any non-ticket commit on the chain between the first ticket commit and `<last>` that touches one of the ticket's files makes the ticket agent-bound (conservative). A **non-linear** set (ticket commits on incomparable branches) has no single baseline: the ticket is agent-bound and ineligible for evolved.
|
|
40
|
+
- **The ticket's lines**: the touched files are the union of `git show --name-only` over its commits. For each file, the ticket's lines at `<last>` are the lines `git blame --porcelain <last> -- <file>` attributes to one of the ticket's commits.
|
|
41
|
+
- **Pure deletions**: for a ticket hunk `-a,k +b,0` in ticket commit `c_i`, the **anchor** is the gap after post-image line `b` of `c_i` (file start: `b=0`, so line 1). It moves from `c_i` to `<last>` along the same chain-and-replay procedure as step 6 (chain from `<last>` back to `c_i` via the first parent descending from `c_i`, replayed forward), and any hunk on that stretch touching the anchor, ticket commit or not, is handled by the usual shift: a later ticket commit's own edits are part of the ticket, while a non-ticket commit touching the anchor before `<last>` makes the ticket agent-bound (conservative). At `<last>`, a later hunk whose old-side range covers line `b` or `b+1`, or a pure insertion at old-side position `b` (`-b,0`), touches the anchor. A file the ticket deleted counts as changed only if the path exists again at HEAD. An empty file at `<last>` carries no lines; it is changed if HEAD differs.
|
|
42
|
+
- **Touching** (one rule for the fast path and the walk): a hunk touches the ticket when its old-side range overlaps a tracked line, or covers or sits at an anchor, or is a **pure insertion** (`-b,0`) with line `b` or `b+1` tracked, so an insertion inside or at the edge of the ticket's span counts. In the walk, a touching insertion's new lines become tracked and inherit the lineage of their tracked neighbours (and any anchor they sit at) plus `c`.
|
|
43
|
+
- **Decide**: `git diff -U0 <last>..HEAD -- <files>`. Any touching hunk, a touched file deleted or renamed since, a file where `git diff --quiet <last> HEAD -- <file>` reports a difference but `-U0` yields no textual hunk (binary, mode-only), or any git error: **changed**, agent-bound. Otherwise **implemented (unchanged)**, whatever the close-time `VERIFY` looked like (old formats vary, so no close-time verify is required).
|
|
44
|
+
- **Shallow clone** (`git rev-parse --is-shallow-repository` prints `true`): say so and send every `build` / `bug` to the agent. Such tickets are also **ineligible for evolved**: blame at the shallow boundary inflates the tracked set.
|
|
45
|
+
- **Dirty working tree**: the audit judges HEAD and ignores uncommitted work, and the step 5 agent reads code at HEAD, never the working tree. If `git status --porcelain` lists any of a ticket's touched files (modified or untracked), the report notes them (the user may want to commit first). That changes nothing else: no agent-bound effect, no evolved ineligibility. A clean tree needs nothing.
|
|
46
|
+
|
|
47
|
+
**Run it with the shipped script.** The fast path above is implemented by `node <templates>/trailhead-audit-walk.js`, where `<templates>` is the first of `${CLAUDE_PLUGIN_ROOT}/templates/` (skip it when `CLAUDE_PLUGIN_ROOT` is unset), `~/.claude/trailhead/templates/`, `~/.codex/skills/trailhead/templates/` that contains it. `index --repo <root>` prints the trailer index as JSON (save it and pass it back with `--index <file>` so the whole audit scans once; an index taken at another HEAD is rejected with exit 2). `fastpath --repo <root> [--index <file>] <n>...` prints `{shallow, tickets:[{ticket, class, reason, evolvedEligible, last, commits, files, dirtyFiles}]}`: `class` is `unchanged` or `changed` (agent-bound), `reason` is null when unchanged, else one of `no-refs`, `non-linear`, `shallow`, `guard`, `touching-hunk`, `deleted-or-renamed`, `recreated`, `non-textual`, `git-error`, `evolvedEligible` says whether step 6 may class the ticket evolved, and `dirtyFiles` feeds the report's dirty-file note. The prose stays the spec: if the script is missing or errors, follow the steps above by hand.
|
|
48
|
+
|
|
49
|
+
**Light check** for `decision` / `research` / `task` / `prototype` (inline, no agent): the recorded answer or outcome exists and answers the Question: a resolution comment, or its map `Decisions so far` line, or the linked artifact or stated resulting facts. A missing one is **not implemented**; an unclear one is unverifiable. There is no code check.
|
|
50
|
+
|
|
51
|
+
**Cost guard**: count the **agent-bound set** only (never the fast-path or light-check tickets). When it **exceeds 20** tickets, show the count and ask whether to proceed, narrow to one map, or cancel. At 20 or fewer, go straight on.
|
|
52
|
+
|
|
53
|
+
## 5. Verify the closed set
|
|
54
|
+
|
|
55
|
+
Dispatch each agent-bound ticket to a **`trailhead-verify`** agent (see `../../_shared/techniques.md` for dispatch, the `config.models.verify` model override, and the inline fallback), in parallel **batches of 5**, background and polled: reviews never block the session. The verification protocol is `../../_shared/techniques/verify.md`; this audit states its own brief on top of it, and `verify.md` itself is unchanged.
|
|
56
|
+
|
|
57
|
+
**Audit-mode brief**, handed to each agent:
|
|
58
|
+
- Work goal-backward against the **current HEAD code** (read it with `git show HEAD:<file>`, never the working tree, so every range it returns is a HEAD range) and the ticket's own Question / Outcome / PLAN criteria.
|
|
59
|
+
- Use the ticket's `Refs: #<n>` commits as evidence when present. For the audit, this brief **overrides two rules of `techniques/verify.md`**: an **empty Refs set is evidence, not an error** (old or manually closed tickets carry none, so verify against HEAD and the recorded answer or outcome instead), and the agent **posts nothing** (it returns its verdict to the caller, never a `VERIFY` comment).
|
|
60
|
+
- Return a result **per claim** (met / unmet / inconclusive, each inconclusive with its reason) **plus whether the deliverable is present**: code for `build` / `bug`.
|
|
61
|
+
- For each **unmet** claim, return the **HEAD `file:line` ranges** that carry (or should carry) its evidence. For an **absent deliverable**, return the HEAD ranges (or the deletion gaps) where the deliverable used to live. Without ranges, neither can ever be classed evolved (step 6).
|
|
62
|
+
- Read-only means **no writes of any kind** (no comments, edits, labels, or state changes), on the ticket or anywhere else. **Reading is expected**: the ticket and its comments, the map issue body (glossary, Decisions so far, Notes), linked or blocking tickets, and the repo code and history. A claim whose evidence lives in an issue must be evaluated by reading it, never left inconclusive for that reason.
|
|
63
|
+
|
|
64
|
+
## 6. Classify
|
|
65
|
+
|
|
66
|
+
From the per-claim results and deliverable presence, in this precedence:
|
|
67
|
+
|
|
68
|
+
1. Deliverable absent: **evolved by #m** if the absence is attributed (below, over the HEAD ranges the agent returned for the deliverable (step 5)), else **not implemented**.
|
|
69
|
+
2. Else any claim unmet: **evolved by #m** only when **every** unmet claim is attributed, else **implemented wrong**.
|
|
70
|
+
3. Else all claims met: **implemented**.
|
|
71
|
+
|
|
72
|
+
The agent still runs on every changed ticket; there is no shortcut that classes "changed only by other tickets" as evolved without verifying. A ticket with no `Refs:` commits, a non-linear set, or a shallow clone has no trustworthy baseline and can never be classed evolved: an unmet claim there stays **implemented wrong** (the conservative side: the regression stays visible as a proposal the user can decline).
|
|
73
|
+
|
|
74
|
+
**Attribution contract** (deterministic, by the caller, one mechanism for edits and deletions): a **forward walk** from `<last>` to HEAD.
|
|
75
|
+
|
|
76
|
+
- **Start** from the ticket's tracked lines and deletion anchors at `<last>` (the fast-path set). For each file that the unmet claim's (or the absent deliverable's) HEAD ranges name, walk the **path from `<last>` to HEAD**: from HEAD, step back to the first parent `p` with `git merge-base --is-ancestor <last> p` true, until `<last>`, then replay that chain forward.
|
|
77
|
+
- **Per commit** `c` on the chain: first read `git diff -M --name-status p c` with **no pathspec**, so a rename `R<score> <file> <new>` is seen with both names, and skip `c` when it does not list the tracked path. Otherwise read `git diff -M -U0 p c -- <file> [<new>]` against its chosen parent `p`: a **touching** hunk (rule in step 4) makes `c` a **producer**. Then shift the tracked lines and anchors through the hunk: the new-side lines of a producer hunk (replacements and insertions) **become tracked**, deleted tracked lines leave an anchor that **inherits the deleted lines' lineage plus `c`**, and lines outside any hunk move by the hunk's line delta.
|
|
78
|
+
- **Lineage**: each tracked line and anchor carries the producers that touched it on the way to HEAD. A new-side line inherits the lineage of the tracked lines and anchors its hunk replaced, plus `c`. A deleted tracked line's anchor inherits the deleted lines' lineage plus `c`. A touching pure insertion's new lines inherit the lineage of their tracked neighbours (and any anchor they sit at) plus `c`.
|
|
79
|
+
- **Delete and rename**: a file deleted in `c` makes `c` a producer and ends the walk for that file. A rename updates the tracked path to `<new>` and the walk keeps the same chain; `c` is a producer only if its `p..c` diff also has a touching hunk (a pure rename is not).
|
|
80
|
+
- **Merge** `c` on the chain: expand it to the side commits that produced its touching hunks. For the **new-side lines** of a touching hunk, `git log --format='%H%x00%(trailers:key=Refs,valueonly,separator=%x2C)' -L<s>,<e>:<file> p..c` (range in `c`'s coordinates, history limited to what `c` brings beyond `p`) lists every commit that touched those lines, intermediate overwritten ones included, plus `c` itself when the merge changed them (conflict resolution); all of them are producers. Trust that trace only on a simple side history, checked **without any pathspec**: if `git rev-list --merges p..c` lists a merge other than `c` itself (a nested merge), or `git log -M --name-status --format= p..c` lists an `R` line naming the file on either side, or any command fails, the producer is **`c` itself** instead (conservative). For **old-side tracked lines a touching hunk deletes** there is no surviving line to follow, so the producer is **`c` itself**, judged by its own trailer (usually none). Coordinates still advance by the `p..c` diff, so the chain stays single.
|
|
81
|
+
- **Only the claim's own lineage counts**: the producers are those in the lineage of the tracked lines and anchors that the claim's HEAD ranges overlap (for an absent deliverable, the HEAD ranges where the deliverable was), so an unrelated later edit elsewhere in the same file never attributes the claim.
|
|
82
|
+
- **Attributed** when that lineage holds at least one producer and **every** producer carries a parsed `Refs:` trailer naming some `#m` with m != n. A producer with no `Refs:` trailer (an edit no ticket tracks), or no producer at all, means not attributed. Name every evolving ticket.
|
|
83
|
+
|
|
84
|
+
**Run it with the shipped script.** Same script, same `<templates>` resolution as step 4: `node <templates>/trailhead-audit-walk.js attribute --repo <root> [--index <file>] --ticket <n> --claim <spec> [--claim <spec>...]`. A `<spec>` is a comma list of `<path>:<s>-<e>` (a HEAD line range) or `<path>:@<g>` (the deletion gap after HEAD line `g`), in HEAD paths and coordinates, one `--claim` per unmet claim (or per deliverable range). It prints `{ticket, eligible, reason, claims:[{spec, attributed, evolvedBy, producers:[{sha, refs}]}]}`: `eligible` is false (with `reason`) for the tickets that can never be classed evolved, and every claim is then not attributed; otherwise `attributed` and `evolvedBy` are the contract's verdict, and `producers` lists the commits behind it. The prose stays the spec: if the script is missing or errors, walk by hand as described above.
|
|
85
|
+
|
|
86
|
+
Independently, **any inconclusive claim flags the ticket unverifiable in part**. It is reported with its reason class beside its class (a mixed result shows both), and it rewinds the marker (step 10). A ticket whose deliverable is present, with only inconclusive claims and no unmet one, is plain **unverifiable**: no fix proposal, offered for a re-run instead.
|
|
87
|
+
|
|
88
|
+
## 7. Compare the open tickets
|
|
89
|
+
|
|
90
|
+
Open tickets in scope are compared inline, in two stages that favour recall:
|
|
91
|
+
|
|
92
|
+
- **(a)** Read every ticket's title and its `## Question` / `## Outcome` sections (one paginated listing with bodies, so no per-pair cost) and judge them all together **by meaning, not by shared words**. Keep **every** pair or cluster that might be a duplicate or groupable, uncertain ones included.
|
|
93
|
+
- **(b)** Re-read the full body and comments only for those candidates, and confirm or discard each.
|
|
94
|
+
|
|
95
|
+
Findings:
|
|
96
|
+
- **Duplicates**: the same deliverable.
|
|
97
|
+
- **Groupable clusters**: the same scope or seam, small enough to land as one ticket, and **the same type**. Only tickets of one type merge, so the merged ticket keeps that type and its engine. Mixed-type overlaps are reported as related, never proposed for grouping.
|
|
98
|
+
- Grouping never crosses a map boundary. A cross-map duplicate is **reported only**.
|
|
99
|
+
- **Claimed tickets are related only.** Treat any open ticket with any assignee (trailhead runs as your own `gh` account, so a ticket another live session of yours claimed with `--add-assignee @me` counts too) or holding a `PAUSED` checkpoint as claimed: another session is working on it, so report it as related only and never propose it for duplicate-close or supersede-and-merge. This holds for the pair or group as a whole: if any member of a duplicate pair or merge group is claimed, the whole pair or group is reported as related only and never proposed, so a claimed kept ticket never gets blockers added or dependents re-pointed onto it.
|
|
100
|
+
|
|
101
|
+
Over 60 open tickets in scope, run stage (a) per map plus one cross-map pass over titles and Questions only, and say so.
|
|
102
|
+
|
|
103
|
+
## 8. Report
|
|
104
|
+
|
|
105
|
+
One consolidated report: counts per class, which must sum to the audited set (each ticket is counted **once by its primary class**, the step 6 precedence, and plain unverifiable only when no unmet claim exists): implemented (by the agent or by the light check, giving the light-check share), **unchanged**, **evolved**, implemented wrong, not implemented, unverifiable, then each evolved ticket by name and number with the evolving ticket(s), then each other non-implemented or unverifiable ticket by name and number with a one-line reason, the tickets **also unverifiable in part** listed separately, **outside the sum**, then the duplicate and groupable findings, and any dirty-file note or shallow-clone note from step 4. Nothing has changed yet.
|
|
106
|
+
|
|
107
|
+
## 9. Proposals, each on an explicit yes
|
|
108
|
+
|
|
109
|
+
One proposal per finding, shown one at a time, applied only on an explicit yes. On anything else, skip it and change nothing. Before a duplicate or merge proposal is even shown, **simulate the resulting dependency graph** (the transferred blockers and re-pointed dependents) and drop or reshape any proposal that would create a cycle, saying why. Just before applying any proposal, **re-read** what it touches (the ticket's state, labels, blockers and dependents, and for a duplicate or a group every ticket involved, including the check for any assignee or a `PAUSED` checkpoint: a ticket claimed since the report is reported as related only and dropped from the proposal, and if any member of the pair or group is claimed the whole proposal is dropped to related only, and for an implemented-wrong proposal the existing `Regression of:` search below is repeated). If an earlier fix in the same run, or a concurrent change, made it stale, say so and re-propose or drop it; never apply a stale proposal. The reopen, duplicate and merge proposals are **human-owned**: ask them as plain yes/no, never with the delegate option.
|
|
110
|
+
|
|
111
|
+
- **Implemented wrong** → a new `bug` carrying `Regression of: <ticket>`, via the capture cluster's `bug --of <n>` (a bug in already-closed work is always a new ticket; see the capture cluster's `bug` engine in `../../trailhead-capture/`). Before proposing it for ticket <n>, search exhaustively (`is:issue`, open, or closed after <n>'s `closedAt`) for an existing ticket whose body carries `Regression of:` pointing at <n> (match the number or its URL). If one exists, report <n> as already tracked, naming that ticket, instead of proposing a new bug; this prevents duplicates when a rewound marker re-audits tickets.
|
|
112
|
+
- **Not implemented** → `gh issue reopen <n>` plus a comment stating the gap (the premature-close branch of new-bug-versus-reopen).
|
|
113
|
+
- **Evolved** → no proposal: listed in the report only, because a later ticket changed the code on purpose.
|
|
114
|
+
- **Duplicate** → first **carry the duplicate's wiring onto the kept ticket**, in all three views (the `## Blocked by` line, the native `blocked_by` edge, the `trailhead:blocked` label): each open dependent of the duplicate is re-pointed to the kept ticket unless already wired there, and the union of blockers is built by adding each open blocker of the duplicate that the kept ticket lacks, both **excluding the pair itself** so the kept ticket is never re-pointed onto itself or made to block on itself. Then close the newer one natively as a duplicate of the kept one, `gh issue close <n> --duplicate-of <k>` (this sets the `DUPLICATE` state reason the window filter reads), plus a comment naming the kept ticket. If the duplicate was one of the kept ticket's blockers, that edge is removed (the native edge auto-reflects the close; also drop it from the `## Blocked by` line) instead of transferred, and the kept ticket's `trailhead:blocked` label is re-evaluated only after the duplicate's blockers have been added to it: drop `trailhead:blocked` if that was its last open blocker. Its native sub-issue edge stays: the eager drop is reserved to superseded and out-of-scope closes, and `/trailhead:prune` reclaims closed edges under cap pressure.
|
|
115
|
+
- **Groupable** → **supersede-and-merge**, the reverse of the split in `../../_shared/teamwork.md`. Open one merged ticket on the same map or whiteboard (create-then-wire). Its blockers are the union of the originals' open blockers **minus the group's own members**, wired in all three views (the `## Blocked by` line, the native `blocked_by` edge, and the `trailhead:blocked` label at creation). Every dependent of an original **outside the group** is re-pointed onto the merged ticket in all three views (body line edited, native edge added to the merged ticket; the original's edge reflects its close on its own). Intra-group edges are dropped, never re-pointed, so the merged ticket never blocks on, or depends on, itself. The merged ticket's body carries a `Merged from: <name+link>, ...` provenance line (mirroring split's `Split from:`). Then close each original as `trailhead:superseded` with the eager edge drop and a closing comment linking the merged ticket.
|
|
116
|
+
|
|
117
|
+
## 10. The marker advance (last, and confirmed too)
|
|
118
|
+
|
|
119
|
+
The marker advance is the **last proposal** of the run, shown with its value and applied **only on an explicit yes**, like every other change: a declined advance leaves the window exactly as it was.
|
|
120
|
+
|
|
121
|
+
- Write it only when the run covered **every map plus the whiteboard**. A `[map]`-scoped run never advances the repo-wide marker, or the other maps would skip closes nobody audited.
|
|
122
|
+
- Never advance from an incomplete run (step 3).
|
|
123
|
+
- The value is the **cutoff captured in step 2**, never the completion time, so a ticket closed mid-run falls into the next window.
|
|
124
|
+
- An **unchanged** or **evolved** ticket never rewinds the marker.
|
|
125
|
+
- If any ticket was flagged **unverifiable**, in whole or in part (step 4 or step 6), write instead the earliest unverifiable ticket's `closedAt` minus one second (never past the cutoff), so the next default run picks it up again. No extra state, nothing silently dropped.
|
|
126
|
+
- Write it into the dashboard body with the fail-safe read-modify-write (see Body generation in `../../_shared/substrate-commands.md`). `--all` advances it the same way.
|
|
127
|
+
|
|
128
|
+
## Notes
|
|
129
|
+
|
|
130
|
+
- The audit never edits a ticket's body, labels, or state except through a confirmed proposal above.
|
|
131
|
+
- The marker lives in the pinned dashboard body, so it is repo-scoped and survives across machines.
|
|
132
|
+
- A closed ticket the audit classes **implemented**, **unchanged**, or **evolved** is left alone: no comment, no label.
|
|
@@ -72,6 +72,7 @@ It holds:
|
|
|
72
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
|
+
- a hidden `<!-- trailhead:last-audit <ISO-8601 UTC> -->` line, when the body already has one, is **preserved verbatim** on every regeneration (it is the `/trailhead:audit` window marker; see Body generation in `../_shared/substrate-commands.md`).
|
|
75
76
|
|
|
76
77
|
**`dashboard`** regenerates the pinned issue body from the live tracker and shows it, both the render and the on-demand refresh, consistent with `map` and `whiteboard`. It creates and pins the issue if the repo doesn't have one yet (ensuring the `trailhead:dashboard` label first, idempotently). The exact gh commands (find, pin/re-pin/unpin, self-heal, body generation with the count queries) are in `../_shared/substrate-commands.md`.
|
|
77
78
|
|
|
@@ -42,7 +42,7 @@ The user invokes with a map (URL or number). A ticket is optional: without one,
|
|
|
42
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**, first run the read-only **assigned-ticket pre-check** (full rule in `../_shared/teamwork.md`): other open tickets already assigned to you **pre-bias** the ask toward `worktree`/`clone` (still asking, easy neutral), and the ask is **skipped** only when the session is already inside this ticket's own linked worktree (`trailhead/t<N>`, `N` the current ticket). Then 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 and never runs the pre-check (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
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**: 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 lets an external tool show "what am I on" offline (the tracker stays the source of truth), but it is not merely that cheap hint: while it is present, the git commit-msg hook also requires every commit to carry a `Refs: #<n>` trailer matching it, so a stale marker left behind would wrongly demand that trailer on unrelated commits. 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
|
-
- **Sync the commit-msg hook**: at the working root, run `node <templates>/trailhead-commit-msg-sync.js --repo <working-root>` (where `<templates>` lives and what each status means: the commit-msg hook paragraph in `../_shared/teamwork.md`). Stay silent when it reports installed or current. On **upgraded**, give the user one result-oriented line: this clone's commit-msg hook was updated to the current rules. On **foreign**, one line that the user's own hook was left untouched and git does not enforce trailhead's rules here. On **skipped**, one line that nothing was set up here (a `core.hooksPath` without a trailhead hook, or a working root that is not a git repo) and git does not enforce trailhead's rules. On a missing script or a node error, inspect the hooks dir (`git rev-parse --git-path hooks`) and report per the fallback in that same paragraph of `../_shared/teamwork.md` (trailhead hook present and executable: enforced but freshness unchecked, update the install; present but not executable: git skips it, not enforced; foreign hook: untouched, not enforced; no hook: not enforced, update the install), never a blind "not enforced"; it never blocks the work.
|
|
45
|
+
- **Sync the commit-msg hook**: at the working root, run `node <templates>/trailhead-commit-msg-sync.js --repo <working-root>` (where `<templates>` lives and what each status means: the commit-msg hook paragraph in `../_shared/teamwork.md`). Stay silent when it reports installed or current. On **upgraded**, give the user one result-oriented line: this clone's commit-msg hook was updated to the current rules. On **foreign**, one line that the user's own hook was left untouched and git does not enforce trailhead's rules here. On **skipped**, one line that nothing was set up here (a `core.hooksPath` without a trailhead hook, or a working root that is not a git repo) and git does not enforce trailhead's rules. On a missing script or a node error (missing means none of the three template dirs has it: try them all before concluding that), inspect the hooks dir (`git rev-parse --git-path hooks`) and report per the fallback in that same paragraph of `../_shared/teamwork.md` (trailhead hook present and executable: enforced but freshness unchecked, update the install; present but not executable: git skips it, not enforced; foreign hook: untouched, not enforced; no hook: not enforced, update the install), never a blind "not enforced"; it never blocks the work.
|
|
46
46
|
- **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`.
|
|
47
47
|
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. **If the user defers the choice rather than settling it here, mark the divergent follow-up pending for the Pending follow-up guard (`../_shared/session-handoff.md`), so it is re-posed at the handoff rather than lost.** **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.
|
|
48
48
|
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.
|