@marcomigozzi/trailhead 0.16.1 → 0.18.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.
Files changed (154) hide show
  1. package/README.it.md +82 -25
  2. package/README.md +82 -25
  3. package/bin/base-preflight-docs.test.js +4 -1
  4. package/bin/bulk-cross-reference-docs.test.js +36 -0
  5. package/bin/capture-action.test.js +167 -0
  6. package/bin/capture-docs.test.js +32 -0
  7. package/bin/engine-route-docs.test.js +155 -0
  8. package/bin/facet-stamp-docs.test.js +79 -0
  9. package/bin/forced-mutation-docs.test.js +38 -0
  10. package/bin/grill-model-docs.test.js +155 -0
  11. package/bin/grilling-holistic-review-docs.test.js +7 -3
  12. package/bin/guard-axis-docs.test.js +69 -0
  13. package/bin/inbox-needs-info-docs.test.js +68 -0
  14. package/bin/lib/codex-projection.js +28 -19
  15. package/bin/lib/codex-projection.test.js +77 -20
  16. package/bin/lib/host-descriptor.js +1 -1
  17. package/bin/lib/host-descriptor.test.js +6 -0
  18. package/bin/needs-you-docs.test.js +9 -2
  19. package/bin/options-in-chat-docs.test.js +65 -0
  20. package/bin/pick-stress-test-docs.test.js +1 -1
  21. package/bin/pipeline-review-docs.test.js +118 -0
  22. package/bin/plan-review-budget-docs.test.js +63 -0
  23. package/bin/repo-target-docs.test.js +41 -0
  24. package/bin/report-status-docs.test.js +10 -2
  25. package/bin/seam-coverage-docs.test.js +46 -0
  26. package/bin/session-trust-docs.test.js +37 -0
  27. package/bin/standards-brief-docs.test.js +112 -0
  28. package/bin/surfaced-scope-docs.test.js +57 -0
  29. package/bin/test-once-docs.test.js +101 -0
  30. package/bin/test-run-docs.test.js +47 -0
  31. package/bin/trailhead.js +15 -8
  32. package/bin/trailhead.test.js +71 -17
  33. package/bin/unblock-docs.test.js +56 -0
  34. package/bin/yes-accepts-recommended-docs.test.js +54 -0
  35. package/package.json +1 -1
  36. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  37. package/plugins/trailhead/agents/trailhead-code-review.md +2 -1
  38. package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
  39. package/plugins/trailhead/agents/trailhead-grill.md +41 -0
  40. package/plugins/trailhead/agents/trailhead-plan.md +1 -1
  41. package/plugins/trailhead/agents/trailhead-verify.md +1 -0
  42. package/plugins/trailhead/hooks/lib/audit-walk.test.js +62 -0
  43. package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
  44. package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
  45. package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
  46. package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
  47. package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
  48. package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
  49. package/plugins/trailhead/hooks/lib/git-env-mod.test.js +55 -0
  50. package/plugins/trailhead/hooks/lib/git-env-spawn.test.js +73 -0
  51. package/plugins/trailhead/hooks/lib/git-env.test.js +51 -0
  52. package/plugins/trailhead/hooks/lib/global-config-dir.test.js +48 -0
  53. package/plugins/trailhead/hooks/lib/heredoc-mod.test.js +85 -0
  54. package/plugins/trailhead/hooks/lib/inbox-needs-info.test.js +179 -0
  55. package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
  56. package/plugins/trailhead/hooks/lib/issue-body.test.js +165 -0
  57. package/plugins/trailhead/hooks/lib/model-defaults-review.js +22 -1
  58. package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +64 -1
  59. package/plugins/trailhead/hooks/lib/model-route.js +207 -0
  60. package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
  61. package/plugins/trailhead/hooks/lib/needs-you.test.js +45 -1
  62. package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +272 -0
  63. package/plugins/trailhead/hooks/lib/repo-target.test.js +256 -0
  64. package/plugins/trailhead/hooks/lib/report-status.test.js +56 -1
  65. package/plugins/trailhead/hooks/lib/statusline-git-env.test.js +47 -0
  66. package/plugins/trailhead/hooks/lib/test-run.test.js +757 -0
  67. package/plugins/trailhead/hooks/lib/trailhead-git-env.js +40 -0
  68. package/plugins/trailhead/hooks/mod/README.md +42 -17
  69. package/plugins/trailhead/hooks/mod/auto-run.ts +2 -1
  70. package/plugins/trailhead/hooks/mod/git-env.ts +31 -0
  71. package/plugins/trailhead/hooks/mod/heredoc.ts +79 -0
  72. package/plugins/trailhead/hooks/mod/map-pane-view.ts +3 -1
  73. package/plugins/trailhead/hooks/mod/map-pane.tsx +204 -49
  74. package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
  75. package/plugins/trailhead/hooks/mod/repo-pin.ts +113 -9
  76. package/plugins/trailhead/hooks/mod/work-state-locate.ts +2 -1
  77. package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
  78. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +28 -14
  79. package/plugins/trailhead/hooks/trailhead-check-update.js +20 -2
  80. package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
  81. package/plugins/trailhead/hooks/trailhead-labels-guard.js +2 -2
  82. package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +1 -1
  83. package/plugins/trailhead/hooks/trailhead-test-guard.js +232 -0
  84. package/plugins/trailhead/hooks/trailhead-test-guard.test.js +172 -0
  85. package/plugins/trailhead/skills/_shared/capture-core.md +123 -0
  86. package/plugins/trailhead/skills/_shared/choices.md +2 -2
  87. package/plugins/trailhead/skills/_shared/configuration-reference.md +30 -16
  88. package/plugins/trailhead/skills/_shared/configuration.md +1 -1
  89. package/plugins/trailhead/skills/_shared/load-first.md +2 -0
  90. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  91. package/plugins/trailhead/skills/_shared/substrate-commands.md +19 -6
  92. package/plugins/trailhead/skills/_shared/substrate.md +4 -3
  93. package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
  94. package/plugins/trailhead/skills/_shared/teamwork.md +11 -5
  95. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
  96. package/plugins/trailhead/skills/_shared/techniques/code-review-pipeline.md +24 -0
  97. package/plugins/trailhead/skills/_shared/techniques/code-review.md +22 -6
  98. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +6 -3
  99. package/plugins/trailhead/skills/_shared/techniques/debug.md +5 -2
  100. package/plugins/trailhead/skills/_shared/techniques/grilling.md +28 -3
  101. package/plugins/trailhead/skills/_shared/techniques/plan-review.md +16 -4
  102. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
  103. package/plugins/trailhead/skills/_shared/techniques/tdd.md +4 -2
  104. package/plugins/trailhead/skills/_shared/techniques/verify.md +5 -1
  105. package/plugins/trailhead/skills/_shared/techniques.md +8 -6
  106. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
  107. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
  108. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
  109. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
  110. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +11 -6
  111. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +18 -3
  112. package/plugins/trailhead/skills/trailhead-work/SKILL.md +12 -9
  113. package/plugins/trailhead/skills/trailhead-work/references/auto.md +3 -1
  114. package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +2 -0
  115. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +13 -13
  116. package/plugins/trailhead/templates/statusline-trailhead.sh +10 -3
  117. package/plugins/trailhead/templates/trailhead-audit-walk.js +56 -4
  118. package/plugins/trailhead/templates/trailhead-auto-merge.js +2 -1
  119. package/plugins/trailhead/templates/trailhead-auto-queue.js +2 -1
  120. package/plugins/trailhead/templates/trailhead-auto-slot.js +2 -1
  121. package/plugins/trailhead/templates/trailhead-base-preflight.js +10 -5
  122. package/plugins/trailhead/templates/trailhead-capture.js +419 -0
  123. package/plugins/trailhead/templates/trailhead-checkout-find.js +257 -0
  124. package/plugins/trailhead/templates/trailhead-checkout-in.js +78 -21
  125. package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +2 -1
  126. package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
  127. package/plugins/trailhead/templates/trailhead-git-env.js +40 -0
  128. package/plugins/trailhead/templates/trailhead-inbox-needs-info.js +161 -0
  129. package/plugins/trailhead/templates/trailhead-isolation-answer.js +2 -1
  130. package/plugins/trailhead/templates/trailhead-isolation-copy.js +9 -4
  131. package/plugins/trailhead/templates/trailhead-issue-body.js +161 -12
  132. package/plugins/trailhead/templates/trailhead-merge-ticket.js +2 -1
  133. package/plugins/trailhead/templates/trailhead-needs-you.js +38 -4
  134. package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +261 -7
  135. package/plugins/trailhead/templates/trailhead-repo-target.js +302 -0
  136. package/plugins/trailhead/templates/trailhead-report-status.js +57 -9
  137. package/plugins/trailhead/templates/trailhead-test-run.js +752 -0
  138. package/plugins/trailhead/templates/trailhead-uat-backfill.js +2 -1
  139. package/plugins/trailhead/templates/trailhead-uat-followup.js +2 -1
  140. package/plugins/trailhead/templates/trailhead-uat-setup.js +9 -4
  141. package/plugins/trailhead/templates/trailhead-unblock.js +2 -1
  142. package/plugins/trailhead/tests/auto-run-ui.test.ts +2 -1
  143. package/plugins/trailhead/tests/git-env.test.ts +20 -0
  144. package/plugins/trailhead/tests/map-pane-ui.test.ts +386 -50
  145. package/plugins/trailhead/tests/map-pane-view.test.ts +6 -0
  146. package/plugins/trailhead/tests/map-pane-write.test.ts +57 -1
  147. package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
  148. package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +3 -2
  149. package/plugins/trailhead/tests/repo-pin-relocate.test.ts +175 -0
  150. package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
  151. package/plugins/trailhead/tests/unscrub.ts +8 -0
  152. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +3 -2
  153. package/plugins/trailhead/tests/work-state-worktree.test.ts +2 -1
  154. package/plugins/trailhead/types/index.d.ts +7 -0
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+ // Repo-location environment for git spawns.
3
+ //
4
+ // Git gives these variables precedence over `-C` and the cwd, so an exported
5
+ // GIT_DIR (a hook, `git rebase --exec`, a parent shell) silently redirects every
6
+ // git call to another repository. Each git spawn in the plugin builds its env
7
+ // through gitEnv() so it always acts on the checkout it was pointed at.
8
+ //
9
+ // The names are `git rev-parse --local-env-vars` minus GIT_CONFIG*: those carry
10
+ // the parent's `-c` options and are meant to be inherited. A static list keeps
11
+ // this free of a git spawn; refresh it from that command when git adds a name.
12
+ // Names must be deleted: an empty string is not "unset" (`fatal: '' is not a git repository`).
13
+ //
14
+ // hooks/lib/trailhead-git-env.js is a byte-identical copy for the hooks (they
15
+ // install without the templates); a test keeps the two in sync.
16
+
17
+ const GIT_LOCATION_VARS = Object.freeze([
18
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
19
+ 'GIT_OBJECT_DIRECTORY',
20
+ 'GIT_DIR',
21
+ 'GIT_WORK_TREE',
22
+ 'GIT_IMPLICIT_WORK_TREE',
23
+ 'GIT_GRAFT_FILE',
24
+ 'GIT_INDEX_FILE',
25
+ 'GIT_NO_REPLACE_OBJECTS',
26
+ 'GIT_REPLACE_REF_BASE',
27
+ 'GIT_PREFIX',
28
+ 'GIT_SHALLOW_FILE',
29
+ 'GIT_COMMON_DIR',
30
+ ]);
31
+
32
+ // A new env object: `base` without the repo-location names, then `extra` on top.
33
+ // Never mutates `base` (process.env by default).
34
+ function gitEnv(base = process.env, extra = {}) {
35
+ const out = {};
36
+ for (const [k, v] of Object.entries(base)) if (!GIT_LOCATION_VARS.includes(k)) out[k] = v;
37
+ return { ...out, ...extra };
38
+ }
39
+
40
+ module.exports = { GIT_LOCATION_VARS, gitEnv };
@@ -96,11 +96,21 @@ command file (`commands/panel.md`), because a command registered by a mod cannot
96
96
  only where no mod does (it says the panel is not available). Claude Code only: the Codex
97
97
  projection leaves the `panel` verb out (`CODEX_MOD_ONLY_VERBS` in `bin/lib/codex-projection.js`).
98
98
 
99
- - **What it shows**: the map as a link with its destination (dim), then `Needs you · n`, `Frontier · n`,
100
- `In progress · n`, `UAT · n`, `Paused · n` and `Blocked · n` (an empty group is left out). A row is `#n title`
101
- (a link to the issue) and the type, dim. Every assigned row shows its holder as `@login`; the ticket
99
+ - **What it shows**: the map as a link with its destination (dim), then `In progress · n` (first, right under
100
+ the maps list), `Needs you · n`, `Frontier · n`, `UAT · n`, `Paused · n` and `Blocked · n` (an empty group is left out). Each group, the
101
+ maps list included, has its own glyph and colour (`◈` Maps violet, `▶` In progress accent, `!` Needs you orange, `◆` Frontier green,
102
+ `◎` UAT cyan, `‖` Paused grey, `■` Blocked red): the heading is the glyph, the bold name in that colour and a dim rule to the pane's edge, and
103
+ every ticket's block (its row and the dim lines under it) sits after a gutter in the same colour, so the groups tell apart without
104
+ colour too: in the terminal a `│ ` on each of its lines, on the desktop one bar as tall as the block; the lines under the row are indented past it. A row is `#n title`
105
+ (a link to the issue) and the type (`build` cyan, `decision` orange, any other dim; dim on a blocked row). The unverified-count line is amber. Every assigned row shows its holder as `@login`; the ticket
102
106
  in play in this session is marked `▸` in the accent and says `this session` in place of its holder;
103
- blocked rows are dim. The `ownTicket` atom keeps that mark when another session's work state takes
107
+ blocked rows are dim. `In progress` is global for you: after the shown scope's own claimed tickets it lists your
108
+ open in-progress tickets (assigned to the `gh` login) of every other open map and of the whiteboard, each with a dim
109
+ `on map #<n> <title>` or `on whiteboard` line under it, in the map view and the Dashboard view alike (never a
110
+ ticket the view already lists; paused, blocked, UAT and needs-you ones stay in their own scope's groups). The login is
111
+ read once per host (`viewer{login}`), the tickets by one paged `gh api graphql` read of the open `trailhead:ticket`
112
+ issues with `filterBy:{assignee:<login>}`, in the same refresh as the view's reads; a failed or unreadable read keeps
113
+ the last list (the `myTickets` atom) and sets no footer error. The `ownTicket` atom keeps that mark when another session's work state takes
104
114
  over the shared file (a state stamped with another session leaves it; a missing file clears it only
105
115
  when the last state read was this session's, i.e. its own Resolve); with no own ticket the work
106
116
  state decides, as before. A ticket carrying `trailhead:paused` (set by pause, cleared by resume or a
@@ -108,7 +118,7 @@ projection leaves the `panel` verb out (`CODEX_MOD_ONLY_VERBS` in `bin/lib/codex
108
118
  button in place of its holder (it runs like `work`, with `/trailhead:resume <n>`, which checks the
109
119
  claim); the label wins over `trailhead:blocked` and an assignee.
110
120
  A ticket carrying `trailhead:needs-you` (an autonomous run parked it for the human; a `work`/`quick` claim
111
- clears it) shows once, under `Needs you`, above Frontier. The label wins over paused, UAT, blocked and an
121
+ clears it) shows once, under `Needs you`, between In progress and Frontier. The label wins over paused, UAT, blocked and an
112
122
  assignee. Any assignee is shown as its holder, never a button, even on a paused or blocked row. A blocked
113
123
  row with no assignee is dim with no button. Any other row gets a `work` button, never `resume`, even when
114
124
  paused: the claim clears the mark and resume would not. A
@@ -128,13 +138,14 @@ projection leaves the `panel` verb out (`CODEX_MOD_ONLY_VERBS` in `bin/lib/codex
128
138
  across the reset too. One press at a time: another while one is held or starting is ignored. A session that ends
129
139
  while a press is held drops it. A press while the session is not in the pinned repo (a shell `cd` moved it) still
130
140
  starts: the mod cannot move the session, so it appends the pinned checkout to the verb's args as `--in=<token>`
131
- (base64url of the root) and the verb `cd`s there first. With no repo pinned, or a checkout that cannot be read
141
+ (base64url of the root, a `.`, then base64url of the repo as `host/owner/name`) and the verb `cd`s there first;
142
+ if the root has moved, the verb finds the checkout again by the repo's `origin` remote. With no repo pinned, or a checkout that cannot be read
132
143
  (a removed worktree included), the press is refused with a toast before the `/clear`; both are checked again when
133
144
  a held press is sent. A refused `/clear`, or one that
134
145
  did not change the session, runs no work command. `trailhead:unverified` tickets are never listed,
135
146
  only counted in one dim line; `trailhead:whiteboard` tickets are dropped. The footer is a `refresh`
136
147
  button and `updated <age>`, or `⚠ <reason> · list from <age>` after a `gh` failure (the last good list is kept;
137
- reasons: `gh not installed`, `gh not authenticated (run gh auth login)`,
148
+ reasons: `gh not installed`, `checkout not found: <path>` (the pinned folder is gone and no other checkout of the repo was found), `gh not authenticated (run gh auth login)`,
138
149
  `offline: cannot reach GitHub`, `gh failed (exit <n>)`), and a `close` button at the right edge that
139
150
  closes the panel. A status too long for the room beside the buttons moves to a row of its own under them. Titles come from GitHub and are untrusted:
140
151
  flattened, stripped of control, zero-width and bidi characters, only drawn. Other states: `Loading tickets…`
@@ -185,7 +196,7 @@ projection leaves the `panel` verb out (`CODEX_MOD_ONLY_VERBS` in `bin/lib/codex
185
196
  shown before (the view is kept), a `Maps · n` group with every open map pressable (a press leaves the
186
197
  dashboard and views that map, the active one marked `●`), and a `Whiteboard · <n> open · <m> assigned` section (counted
187
198
  over the listed tickets; `200+ open` when the list is capped): the open
188
- `trailhead:ticket` + `trailhead:whiteboard` tickets as Needs you / Frontier / In progress / UAT / Paused / Blocked, with the same rows,
199
+ `trailhead:ticket` + `trailhead:whiteboard` tickets as In progress / Needs you / Frontier / UAT / Paused / Blocked, with the same rows,
189
200
  holders and `work` buttons (the work verb takes a whiteboard ticket as is). Unverified ones are counted only.
190
201
  Its reads replace the map's in a refresh: the whiteboard list (the GraphQL open read above), `gh issue list --label trailhead:dashboard
191
202
  --json number,url` and the maps list; a failed list keeps the last one with the footer reason, a failed index
@@ -245,14 +256,17 @@ projection leaves the `panel` verb out (`CODEX_MOD_ONLY_VERBS` in `bin/lib/codex
245
256
  from it), and `.trailhead/active-map` and `.trailhead/work-state.json` are read by absolute path under
246
257
  it, so a shell `cd` out of the repo changes nothing. A check outside any repo, or in one with no usable
247
258
  remote, keeps the pin; a move into another repo moves it, drops the reads in flight and empties what the
248
- pane held for the old one. With nothing ever pinned the pane says
259
+ pane held for the old one. A pinned checkout deleted since (a finished worktree) moves the pin, silently, to a live checkout of
260
+ the same repo: the main working tree, else another `git worktree`, else a same-remote clone beside it (probes run as `git -C`
261
+ from `/`, retried at most once a minute). With nothing ever pinned the pane says
249
262
  `Not in a GitHub repository: open the session in a trailhead repo` and makes no `gh` call.
250
263
  - **When it refreshes**: only while the pane is open (a closed pane costs no `gh` call). Opening it,
251
264
  `session.start`, `turn.complete`, a Bash call that writes to the tracker, the `refresh` button, and a slow
252
265
  timer started on open. A write is `gh issue edit|close|create|comment|reopen|delete|transfer`, a `gh api` call on a
253
266
  `repos/.../issues` path with a write method (`-X`/`--method` POST, PATCH, PUT, DELETE) or an implicit POST
254
267
  (`-f`, `-F`, `--field`, `--raw-field`, `--input`) and no `-X GET`, or the `trailhead-issue-body.js` helper
255
- (`GH_ISSUE_WRITE`); reads never refresh. Never two fetches inside `MIN_REFRESH_MS` (save the turn-end read below): a request inside the floor
268
+ (`GH_ISSUE_WRITE`), outside any heredoc body (`isTrackerWrite`: a body is data, so a `gh issue close` line in a
269
+ `cat <<EOF` writes nothing); reads never refresh. Never two fetches inside `MIN_REFRESH_MS` (save the turn-end read below): a request inside the floor
256
270
  keeps one trailing refresh for the rest of it. A write that reads at once also arms that trailing refresh, so one
257
271
  more read follows when the floor ends, after the search index (the history) caught up; a burst of writes still costs one
258
272
  read per floor plus one trailing, and one more when the turn ends after it. The host drops a refused timer without a word (a refused `every` period ends
@@ -264,14 +278,25 @@ projection leaves the `panel` verb out (`CODEX_MOD_ONLY_VERBS` in `bin/lib/codex
264
278
  involved; it leaves an armed trailing refresh alone, so the read after the search index caught up still follows.
265
279
  A read asked while the pane is undrawn is dropped; a render
266
280
  whose list (with no error) is older than `TIMER_MS + TIMER_GRACE_MS` asks for a read at once, as an empty view does.
267
- - **Auto-open**: once, in fullscreen (`e.viewport.isFullscreen === true` in the `AbovePrompt` render),
268
- when a map is active. Never on the main screen, never again after the pane was opened or closed
269
- by the person (a press on the `close` button counts as closing it). The open runs from a zero-delay timer because a render may not write state.
270
- Being unasked, it waits undrawn below 144 columns (`isPlaced: false`), typical of a fresh session; `/trailhead:panel`
271
- then opens it again as the person's request (drawn at any width) instead of closing it, and closes it only when even
272
- that leaves it undrawn (no surface places panes).
281
+ - **Auto-open**: in fullscreen (`e.viewport.isFullscreen === true` in the `AbovePrompt` render, or in the
282
+ viewport of a `session.attach`, so a surface that attaches later tries again), when a map is active. Never on
283
+ the main screen, never again after the person acted on the pane (the command, closing it by hand, the `close`
284
+ button). The open runs from a zero-delay timer because a render may not write state.
285
+ The auto-open counts as done only once the pane is actually drawn (seen placed by a later check, or by the pane's
286
+ own render, which fires only when drawn). Being unasked, the open can stay undrawn (`isPlaced: false`): below
287
+ 144 columns, or on a surface that has not reported its layout yet, typical of a fresh session. That does not burn
288
+ the auto-open: while the pane is listed undrawn nothing opens it again (the host places it itself when it can),
289
+ and if it vanishes undrawn the next trigger opens it again. `/trailhead:panel` closes a pane only when it is
290
+ listed and drawn; a listed but undrawn pane is opened again as the person's request (drawn at any width), and if
291
+ even that leaves it undrawn the pane stays open and the command says so in a fixed sentence (`Map panel is open but not shown yet: ...`; the host's reason goes to the
292
+ debug log only), so it is never closed unseen. With no surface attached yet (`no-surface`, the start of a session) the command answers
293
+ that the panel will open once the screen is ready, again on every repeat; the pane's first render on the surface
294
+ that attaches asks for its read. A desktop session starts at its first message, not when it is created, so nothing
295
+ can open the pane on a session where nothing was typed, and a `/trailhead:panel` sent as that first message lands
296
+ right after the auto-open. Within `UNSEEN_MS` of a drawn auto-open the command keeps the pane and answers
297
+ `Map panel opened.` instead of closing a pane nobody could have seen.
273
298
  - **Constants** (`map-pane.tsx`): `PANE_ID = 'trailhead-map'`, `PANE_TITLE = 'Map'`,
274
- `MIN_REFRESH_MS = 15_000`, `TIMER_MS = 180_000`, `GH_TIMEOUT_MS = 20_000`, `TIMER_GRACE_MS = 30_000`,
299
+ `MIN_REFRESH_MS = 15_000`, `TIMER_MS = 180_000`, `GH_TIMEOUT_MS = 20_000`, `TIMER_GRACE_MS = 30_000`, `UNSEEN_MS = 2_000`,
275
300
  `PANEL_COMMAND = 'trailhead:panel'`, `MAPS_LIMIT = MAPS_READ_LIMIT = 100` (`MAPS_LIMIT` not exported); `worktree-porcelain.ts`: `MAX_FAILURES = 3` (shared by the work-state lookup and the run scan); `auto-run-view.ts`: `RESUME_HINT`; `map-pane-view.ts`: `MAPS_READ_LIMIT = 100`, `LIST_LIMIT = 200`, `HISTORY_LIMIT = 5000`.
276
301
  - **Atoms**: `mapPane` (with `maps`, the open maps), `activeMap`, `viewedMap` (`null` follows the active map), `isDashboard`, `dashboardPane`, `pendingWork`, `pendingPhase` (`queued` or `starting`), `paneAutoOpened`. Module variables hold the queue and
277
302
  the timers, what `/clear` carries over (`carried`), the held press (`held`) and the one being sent (`inFlight`),
@@ -4,6 +4,7 @@
4
4
  import type { EngineInterface } from 'claude-code'
5
5
 
6
6
  import type { AutoRun, RepoPin, RunCounts, WorkState } from '../../types'
7
+ import { gitArgv } from './git-env'
7
8
  import { GH_TIMEOUT_MS, MIN_REFRESH_MS } from './map-pane'
8
9
  import { aggregateRun, isRunMember, nextRunAfterFailure, parseRunCounts, parseRunScope, runCountsArgs, slotTicket } from './auto-run-view'
9
10
  import type { Where } from './auto-run-view'
@@ -48,7 +49,7 @@ async function gate($: EngineInterface, main: string, own: WorkState | null): Pr
48
49
  // La regola di cloneOnBranch (trailhead-auto-slot.js): git dir dentro la cartella e HEAD sul branch dello slot.
49
50
  // undefined: git non è riuscito a girare.
50
51
  async function cloneOnBranch($: EngineInterface, dir: string, ticket: number): Promise<boolean | undefined> {
51
- const git = (args: string[]) => $.process.run(['git', '-C', dir, ...args], { cwd: dir, timeoutMs: GIT_TIMEOUT_MS })
52
+ const git = (args: string[]) => $.process.run(gitArgv('-C', dir, ...args), { cwd: dir, timeoutMs: GIT_TIMEOUT_MS })
52
53
  try {
53
54
  const g = await git(['rev-parse', '--absolute-git-dir'])
54
55
  if (g.exitCode !== 0) return false
@@ -0,0 +1,31 @@
1
+ // Leaf without imports: git argv for the mod's process spawns.
2
+ //
3
+ // Git gives the repo-location variables precedence over `-C` and the cwd, and
4
+ // `$.process.run` can only add or override env, never delete. So an inherited
5
+ // GIT_DIR would redirect every git spawn: wrap the argv in `env -u NAME ...`.
6
+ // The list is templates/trailhead-git-env.js's GIT_LOCATION_VARS (git rev-parse
7
+ // --local-env-vars minus GIT_CONFIG*); hooks/lib/git-env-mod.test.js keeps them equal.
8
+ export const GIT_LOCATION_VARS: readonly string[] = [
9
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
10
+ 'GIT_OBJECT_DIRECTORY',
11
+ 'GIT_DIR',
12
+ 'GIT_WORK_TREE',
13
+ 'GIT_IMPLICIT_WORK_TREE',
14
+ 'GIT_GRAFT_FILE',
15
+ 'GIT_INDEX_FILE',
16
+ 'GIT_NO_REPLACE_OBJECTS',
17
+ 'GIT_REPLACE_REF_BASE',
18
+ 'GIT_PREFIX',
19
+ 'GIT_SHALLOW_FILE',
20
+ 'GIT_COMMON_DIR',
21
+ ]
22
+
23
+ // `argv` run with the repo-location variables removed from its env.
24
+ export const scrubGitEnv = (argv: readonly string[]): string[] => [
25
+ 'env',
26
+ ...GIT_LOCATION_VARS.flatMap(name => ['-u', name]),
27
+ ...argv,
28
+ ]
29
+
30
+ // `git <args>` that acts on the checkout it is pointed at (-C / cwd), never on an inherited GIT_DIR.
31
+ export const gitArgv = (...args: readonly string[]): string[] => scrubGitEnv(['git', ...args])
@@ -0,0 +1,79 @@
1
+ // Leaf without imports: heredoc bodies out of a Bash command (#369).
2
+ //
3
+ // A heredoc body is data (a file being written), not commands, so a `gh issue close` line inside
4
+ // one is no tracker write. Only the opener line stays, with every body and terminator line dropped.
5
+ // The mod imports nothing from hooks/lib, so this is a small twin of shell-scan.js's
6
+ // stripHeredocBodies; hooks/lib/heredoc-mod.test.js keeps their output equal.
7
+ // When it cannot tell, a line is kept: a lost write leaves a stale pane, an extra read costs one `gh` call.
8
+ // Known difference: an opener inside `$(..)` is not seen here (shell-scan sees it), so those lines stay.
9
+
10
+ // The heredoc openers of one line, in order: [{ tag, isDash }]. A `<<` counts outside quotes and outside
11
+ // `((..))` arithmetic, and not as a here-string (`<<<`); an opener with no word after it is no opener.
12
+ // The scan stops at an unquoted `#` that starts a comment (line start, or after whitespace or `;&|(`).
13
+ // `quoteIn` is the quote still open from an earlier line; the quote open at the end of the line is returned.
14
+ function openersOf(
15
+ line: string,
16
+ quoteIn: string,
17
+ ): { found: Array<{ tag: string; isDash: boolean }>; quote: string } {
18
+ const found: Array<{ tag: string; isDash: boolean }> = []
19
+ let quote = quoteIn
20
+ let arith = 0
21
+ for (let i = 0; i < line.length; i++) {
22
+ const c = line[i]
23
+ if (quote !== '') {
24
+ if (c === '\\' && quote === '"') i++
25
+ else if (c === quote) quote = ''
26
+ } else if (c === '\\') {
27
+ i++
28
+ } else if (line.startsWith('${', i)) {
29
+ // A parameter expansion holds no opener (`${x:-<<EOF}`); skip to its `}`, or the rest of the line.
30
+ const end = line.indexOf('}', i)
31
+ if (end < 0) break
32
+ i = end
33
+ } else if (c === '#' && (i === 0 || /[\s;&|(]/.test(line[i - 1]))) {
34
+ break
35
+ } else if (c === "'" || c === '"') {
36
+ quote = c
37
+ } else if (line.startsWith('((', i)) {
38
+ arith++
39
+ i++
40
+ } else if (arith > 0 && line.startsWith('))', i)) {
41
+ arith--
42
+ i++
43
+ } else if (arith === 0 && line.startsWith('<<', i)) {
44
+ if (line[i + 2] === '<') {
45
+ i += 2
46
+ continue
47
+ }
48
+ const isDash = line[i + 2] === '-'
49
+ const word = /^\s*((?:\\.|'[^']*'|"[^"]*"|[^\s;&|<>()'"\\])+)/.exec(line.slice(i + (isDash ? 3 : 2)))
50
+ if (word !== null) {
51
+ found.push({ tag: word[1].replace(/\\(.)|['"]/g, '$1'), isDash })
52
+ i += (isDash ? 3 : 2) + word[0].length - 1
53
+ } else {
54
+ i++
55
+ }
56
+ }
57
+ }
58
+ return { found, quote }
59
+ }
60
+
61
+ // Every line from an opener to its terminator (the terminator included) goes; a body that never ends
62
+ // takes the rest of the command, as the shell reads it.
63
+ export function stripHeredocBodies(command: string): string {
64
+ const lines = command.split('\n')
65
+ const kept: string[] = []
66
+ let quote = ''
67
+ for (let i = 0; i < lines.length; i++) {
68
+ kept.push(lines[i])
69
+ const scan = openersOf(lines[i], quote)
70
+ quote = scan.quote
71
+ for (const { tag, isDash } of scan.found) {
72
+ while (i + 1 < lines.length) {
73
+ i++
74
+ if ((isDash ? lines[i].replace(/^\t+/, '') : lines[i]) === tag) break
75
+ }
76
+ }
77
+ }
78
+ return kept.join('\n')
79
+ }
@@ -15,7 +15,8 @@ export type TicketGroups = {
15
15
  paused: readonly PaneTicket[]
16
16
  blocked: readonly PaneTicket[]
17
17
  }
18
- export type Failure = { error: unknown } | { exitCode: number; stderr: string }
18
+ // `missingRoot`: the folder `gh` was to run in, when it no longer exists (a spawn there fails like a missing binary).
19
+ export type Failure = { error: unknown; missingRoot?: string } | { exitCode: number; stderr: string }
19
20
  export type RefreshDecision = { run: true } | { run: false; waitMs: number }
20
21
 
21
22
  // The most rows one `gh issue list` is asked for.
@@ -429,6 +430,7 @@ const errorText = (err: unknown): string => {
429
430
  // One short line saying why `gh` gave nothing.
430
431
  export const failureReason = (failure: Failure): string => {
431
432
  if ('error' in failure) {
433
+ if (failure.missingRoot !== undefined) return `checkout not found: ${failure.missingRoot}`
432
434
  const text = errorText(failure.error)
433
435
  if (/ENOENT|not found/i.test(text)) return 'gh not installed'
434
436
  if (/timed? ?out/i.test(text)) return 'offline: cannot reach GitHub'