@marcomigozzi/trailhead 0.14.0 → 0.16.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 (156) hide show
  1. package/README.it.md +158 -34
  2. package/README.md +147 -33
  3. package/bin/auto-intro-docs.test.js +72 -0
  4. package/bin/auto-merge-docs.test.js +33 -0
  5. package/bin/auto-merge-ticket-docs.test.js +32 -0
  6. package/bin/auto-review-rounds-docs.test.js +45 -0
  7. package/bin/auto-run-docs.test.js +60 -0
  8. package/bin/auto-scope-docs.test.js +33 -0
  9. package/bin/auto-slots-docs.test.js +45 -0
  10. package/bin/auto-uat-docs.test.js +47 -0
  11. package/bin/base-preflight-docs.test.js +65 -0
  12. package/bin/execute-split-docs.test.js +146 -0
  13. package/bin/grilling-holistic-review-docs.test.js +90 -0
  14. package/bin/isolation-answer-docs.test.js +41 -0
  15. package/bin/isolation-copy-docs.test.js +28 -0
  16. package/bin/lib/codex-projection.js +6 -6
  17. package/bin/lib/codex-projection.test.js +43 -2
  18. package/bin/needs-you-docs.test.js +41 -0
  19. package/bin/pick-stress-test-docs.test.js +105 -0
  20. package/bin/report-status-docs.test.js +195 -0
  21. package/bin/resolve-helpers-docs.test.js +36 -0
  22. package/bin/scope-confirm-skip-docs.test.js +70 -0
  23. package/bin/trailhead.js +6 -6
  24. package/bin/trailhead.test.js +355 -16
  25. package/bin/uat-setup-docs.test.js +65 -0
  26. package/package.json +1 -1
  27. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  28. package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
  29. package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
  30. package/plugins/trailhead/agents/trailhead-debug.md +20 -1
  31. package/plugins/trailhead/agents/trailhead-executor.md +29 -2
  32. package/plugins/trailhead/agents/trailhead-fix.md +29 -2
  33. package/plugins/trailhead/agents/trailhead-plan.md +20 -1
  34. package/plugins/trailhead/agents/trailhead-research.md +22 -3
  35. package/plugins/trailhead/agents/trailhead-verify.md +15 -2
  36. package/plugins/trailhead/commands/auto.md +2 -2
  37. package/plugins/trailhead/hooks/lib/auto-merge.test.js +467 -0
  38. package/plugins/trailhead/hooks/lib/auto-queue.test.js +245 -0
  39. package/plugins/trailhead/hooks/lib/auto-run.test.js +318 -0
  40. package/plugins/trailhead/hooks/lib/auto-slot.test.js +278 -0
  41. package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
  42. package/plugins/trailhead/hooks/lib/checkout-in.test.js +173 -0
  43. package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
  44. package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
  45. package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
  46. package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
  47. package/plugins/trailhead/hooks/lib/dashboard.test.js +472 -0
  48. package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
  49. package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
  50. package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
  51. package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
  52. package/plugins/trailhead/hooks/lib/merge-ticket.test.js +394 -0
  53. package/plugins/trailhead/hooks/lib/needs-you.test.js +177 -0
  54. package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +319 -0
  55. package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
  56. package/plugins/trailhead/hooks/lib/shell-scan.js +36 -1
  57. package/plugins/trailhead/hooks/lib/shell-scan.test.js +14 -0
  58. package/plugins/trailhead/hooks/lib/statusline-context.test.js +140 -0
  59. package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
  60. package/plugins/trailhead/hooks/lib/uat-followup.test.js +169 -0
  61. package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
  62. package/plugins/trailhead/hooks/lib/unblock.test.js +207 -0
  63. package/plugins/trailhead/hooks/lib/work-state.test.js +45 -0
  64. package/plugins/trailhead/hooks/mod/README.md +209 -36
  65. package/plugins/trailhead/hooks/mod/auto-run-view.ts +93 -0
  66. package/plugins/trailhead/hooks/mod/auto-run.ts +165 -0
  67. package/plugins/trailhead/hooks/mod/map-pane-view.ts +304 -26
  68. package/plugins/trailhead/hooks/mod/map-pane.tsx +763 -160
  69. package/plugins/trailhead/hooks/mod/register.tsx +312 -40
  70. package/plugins/trailhead/hooks/mod/repo-pin.ts +167 -0
  71. package/plugins/trailhead/hooks/mod/session-registry.ts +65 -0
  72. package/plugins/trailhead/hooks/mod/turn-state.ts +14 -0
  73. package/plugins/trailhead/hooks/mod/whiteboard-follow.ts +98 -0
  74. package/plugins/trailhead/hooks/mod/work-state-locate.ts +191 -0
  75. package/plugins/trailhead/hooks/mod/work-state-prompt.ts +13 -15
  76. package/plugins/trailhead/hooks/mod/work-state-view.ts +38 -5
  77. package/plugins/trailhead/hooks/mod/worktree-porcelain.ts +17 -0
  78. package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
  79. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +22 -16
  80. package/plugins/trailhead/hooks/trailhead-install-guard.js +169 -34
  81. package/plugins/trailhead/hooks/trailhead-install-guard.test.js +180 -1
  82. package/plugins/trailhead/hooks/trailhead-labels-guard.js +231 -0
  83. package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +212 -0
  84. package/plugins/trailhead/hooks/trailhead-search-guard.js +43 -7
  85. package/plugins/trailhead/hooks/trailhead-search-guard.test.js +66 -2
  86. package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +296 -22
  87. package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +253 -0
  88. package/plugins/trailhead/skills/_shared/choices.md +11 -4
  89. package/plugins/trailhead/skills/_shared/configuration-reference.md +17 -5
  90. package/plugins/trailhead/skills/_shared/load-first.md +3 -3
  91. package/plugins/trailhead/skills/_shared/multi-map.md +2 -2
  92. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  93. package/plugins/trailhead/skills/_shared/substrate-commands.md +47 -14
  94. package/plugins/trailhead/skills/_shared/substrate.md +9 -4
  95. package/plugins/trailhead/skills/_shared/teamwork.md +17 -8
  96. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +18 -10
  97. package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
  98. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
  99. package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
  100. package/plugins/trailhead/skills/_shared/techniques/grilling.md +4 -2
  101. package/plugins/trailhead/skills/_shared/techniques/plan-review.md +3 -1
  102. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
  103. package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
  104. package/plugins/trailhead/skills/_shared/techniques.md +12 -2
  105. package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
  106. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
  107. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +3 -3
  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 +2 -2
  111. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
  112. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
  113. package/plugins/trailhead/skills/trailhead-view/SKILL.md +9 -6
  114. package/plugins/trailhead/skills/trailhead-work/SKILL.md +31 -18
  115. package/plugins/trailhead/skills/trailhead-work/references/auto.md +70 -22
  116. package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
  117. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +29 -13
  118. package/plugins/trailhead/templates/statusline-trailhead.sh +77 -12
  119. package/plugins/trailhead/templates/trailhead-auto-merge.js +318 -0
  120. package/plugins/trailhead/templates/trailhead-auto-queue.js +186 -0
  121. package/plugins/trailhead/templates/trailhead-auto-run.js +294 -0
  122. package/plugins/trailhead/templates/trailhead-auto-slot.js +239 -0
  123. package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
  124. package/plugins/trailhead/templates/trailhead-checkout-in.js +88 -0
  125. package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
  126. package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
  127. package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
  128. package/plugins/trailhead/templates/trailhead-dashboard.js +360 -0
  129. package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
  130. package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
  131. package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
  132. package/plugins/trailhead/templates/trailhead-merge-ticket.js +366 -0
  133. package/plugins/trailhead/templates/trailhead-needs-you.js +193 -0
  134. package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +289 -0
  135. package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
  136. package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
  137. package/plugins/trailhead/templates/trailhead-uat-followup.js +128 -0
  138. package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
  139. package/plugins/trailhead/templates/trailhead-unblock.js +111 -0
  140. package/plugins/trailhead/templates/trailhead-work-state.js +25 -7
  141. package/plugins/trailhead/tests/auto-run-ui.test.ts +444 -0
  142. package/plugins/trailhead/tests/auto-run-view.test.ts +111 -0
  143. package/plugins/trailhead/tests/map-pane-ui.test.ts +2519 -382
  144. package/plugins/trailhead/tests/map-pane-view.test.ts +600 -18
  145. package/plugins/trailhead/tests/map-pane-write.test.ts +51 -0
  146. package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +77 -0
  147. package/plugins/trailhead/tests/repo-pin.test.ts +123 -0
  148. package/plugins/trailhead/tests/session-registry.test.ts +50 -0
  149. package/plugins/trailhead/tests/whiteboard-follow.test.ts +90 -0
  150. package/plugins/trailhead/tests/work-state-locate.test.ts +33 -0
  151. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +276 -2
  152. package/plugins/trailhead/tests/work-state-prompt.test.ts +29 -35
  153. package/plugins/trailhead/tests/work-state-ui.test.ts +73 -1
  154. package/plugins/trailhead/tests/work-state-view.test.ts +44 -1
  155. package/plugins/trailhead/tests/work-state-worktree.test.ts +310 -0
  156. package/plugins/trailhead/types/index.d.ts +62 -3
@@ -0,0 +1,310 @@
1
+ import type { On } from 'claude-code'
2
+ import { expect, test } from 'claude-code/testing'
3
+
4
+ import { resetLocate } from '../hooks/mod/work-state-locate'
5
+
6
+ const SESSION = 'sess-A'
7
+ const START = { cwd: '/repo', surface: 'terminal', isInteractive: true } as const
8
+ const FILE = '/.trailhead/work-state.json'
9
+
10
+ const stateText = (session: string | null, over: Record<string, unknown> = {}): string =>
11
+ JSON.stringify({ map: 198, ticket: 270, title: 'Isolated', step: 'execute', auto: false, autoDone: 0, session, updatedAt: '2026-10-16T00:00:00.000Z', ...over })
12
+
13
+ const porcelain = (...paths: string[]) => paths.map(p => `worktree ${p}\nHEAD abc\nbranch refs/heads/x\n`).join('\n')
14
+
15
+ // A repo with several checkouts: `files` maps a checkout root to its work state text; `cwd` is where the
16
+ // session sits (the pin follows its toplevel).
17
+ const rig = (on: On) => {
18
+ const r = {
19
+ files: {} as Record<string, string>,
20
+ cwd: '/repo',
21
+ remote: 'git@github.com:o/r.git',
22
+ now: 1_000_000,
23
+ worktrees: porcelain('/repo', '/repo-t270') as string | Error,
24
+ siblings: [] as string[],
25
+ ls: [] as string[],
26
+ runs: [] as string[],
27
+ statuses: [] as (string | undefined)[],
28
+ }
29
+ on('session.repo', () => ({ value: { root: r.cwd.startsWith('/other') ? '/other' : '/repo', remote: r.remote, internal: false } }))
30
+ on('clock.now', () => ({ value: r.now }))
31
+ on('session.cwd', () => ({ value: r.cwd }))
32
+ on('session.id', () => ({ value: SESSION }))
33
+ on('process.run', (_$, e) => {
34
+ const line = e.argv.join(' ')
35
+ r.runs.push(line)
36
+ const out = (stdout: string, exitCode = 0) => ({ value: { exitCode, stdout, stderr: '', isStdoutTruncated: false, isStderrTruncated: false } })
37
+ if (line === 'git rev-parse --show-toplevel') return out(`${r.cwd}\n`)
38
+ if (line === 'git worktree list --porcelain') return r.worktrees instanceof Error ? { deny: r.worktrees.message } : out(r.worktrees)
39
+ return out('')
40
+ })
41
+ on('fs.list', (_$, e) => {
42
+ r.ls.push(e.path ?? '')
43
+ return { value: r.siblings.map(name => ({ name, kind: 'dir' as const, size: 0, mtimeMs: 0, isLink: false })) }
44
+ })
45
+ on('fs.read', (_$, e) => {
46
+ const root = e.path.endsWith(FILE) ? e.path.slice(0, -FILE.length) : null
47
+ const text = root === null ? undefined : r.files[root]
48
+ return text === undefined ? { deny: `ENOENT: no such file or directory, open '${e.path}'` } : { value: text }
49
+ })
50
+ on('ui.status', (_$, e) => {
51
+ r.statuses.push(e.text)
52
+ return { value: undefined }
53
+ })
54
+ on('session.start', (_$, e) => ({ cwd: e.cwd }))
55
+ on('turn.start', (_$, e) => ({ turnId: e.turnId }))
56
+ on('turn.complete', () => ({ text: '' }))
57
+ on('tool.call', () => ({ result: '' }) as never)
58
+ return r
59
+ }
60
+ const last = (r: ReturnType<typeof rig>) => r.statuses[r.statuses.length - 1]
61
+ const wtRuns = (r: ReturnType<typeof rig>) => r.runs.filter(line => line.startsWith('git worktree'))
62
+ let turns = 0
63
+ const turn = ($: any) => $.turn.start({ text: 'go', turnId: `t${(turns += 1)}` })
64
+
65
+ test('own-session state in a worktree shows while the session sits in the main checkout', async ($, on) => {
66
+ resetLocate()
67
+ const r = rig(on)
68
+ r.files['/repo-t270'] = stateText(SESSION)
69
+ await $.session.start(START)
70
+ expect(last(r)).toContain('#270')
71
+ })
72
+
73
+ test('another session\'s state, or one with no session, in a worktree is not shown', async ($, on) => {
74
+ resetLocate()
75
+ const r = rig(on)
76
+ r.files['/repo-t270'] = stateText('sess-other')
77
+ await $.session.start(START)
78
+ expect(last(r)).toBeUndefined()
79
+ r.files['/repo-t270'] = stateText(null)
80
+ await turn($)
81
+ expect(last(r)).toBeUndefined()
82
+ })
83
+
84
+ test('a state in the own checkout runs no lookup', async ($, on) => {
85
+ resetLocate()
86
+ const r = rig(on)
87
+ r.files['/repo'] = stateText(SESSION)
88
+ r.files['/repo-t270'] = stateText(SESSION, { ticket: 999 })
89
+ await $.session.start(START)
90
+ await turn($)
91
+ expect(last(r)).toContain('#270')
92
+ expect(wtRuns(r)).toEqual([])
93
+ expect(r.ls).toEqual([])
94
+ })
95
+
96
+ test('a sibling clone found by name is read', async ($, on) => {
97
+ resetLocate()
98
+ const r = rig(on)
99
+ r.worktrees = porcelain('/repo')
100
+ r.siblings = ['repo', 'repo-t5', 'repo-tmp']
101
+ r.files['/repo-t5'] = stateText(SESSION, { ticket: 5 })
102
+ await $.session.start(START)
103
+ expect(r.ls).toEqual(['/'])
104
+ expect(last(r)).toContain('#5')
105
+ })
106
+
107
+ test('a failing worktree list falls back to no ticket without throwing', async ($, on) => {
108
+ resetLocate()
109
+ const r = rig(on)
110
+ r.worktrees = new Error('git exploded')
111
+ r.files['/repo-t270'] = stateText(SESSION)
112
+ await $.session.start(START)
113
+ expect(last(r)).toBeUndefined()
114
+ await turn($)
115
+ expect(last(r)).toBeUndefined()
116
+ })
117
+
118
+ test('a cd into the worktree and back keeps the status steady, and the remembered checkout skips the scan', async ($, on) => {
119
+ resetLocate()
120
+ const r = rig(on)
121
+ r.files['/repo-t270'] = stateText(SESSION)
122
+ await $.session.start(START)
123
+ expect(last(r)).toContain('#270')
124
+ const scans = wtRuns(r).length
125
+ r.cwd = '/repo-t270'
126
+ await turn($)
127
+ r.cwd = '/repo'
128
+ await turn($)
129
+ await turn($)
130
+ expect(r.statuses.filter(s => s === undefined)).toEqual([])
131
+ expect(last(r)).toContain('#270')
132
+ expect(wtRuns(r).length).toBe(scans)
133
+ })
134
+
135
+ test('a state found in one repo is not shown after moving to another repo', async ($, on) => {
136
+ resetLocate()
137
+ const r = rig(on)
138
+ r.files['/repo-t270'] = stateText(SESSION)
139
+ await $.session.start(START)
140
+ expect(last(r)).toContain('#270')
141
+ r.cwd = '/other'
142
+ r.remote = 'git@github.com:o/other.git'
143
+ r.worktrees = porcelain('/other')
144
+ await turn($)
145
+ expect(last(r)).toBeUndefined()
146
+ })
147
+
148
+ test('a scan that found nothing is not repeated inside the window; a helper call scans anyway', async ($, on) => {
149
+ resetLocate()
150
+ const r = rig(on)
151
+ await $.session.start(START)
152
+ await turn($)
153
+ r.now += 5_000
154
+ await turn($)
155
+ expect(wtRuns(r).length).toBe(1)
156
+
157
+ r.files['/repo-t270'] = stateText(SESSION)
158
+ await $.tool.call({ tool: 'Bash', command: 'node trailhead-work-state.js set --repo . --step execute' } as never)
159
+ expect(wtRuns(r).length).toBe(2)
160
+ expect(last(r)).toContain('#270')
161
+ })
162
+
163
+ test('the no-state memory ends after its window', async ($, on) => {
164
+ resetLocate()
165
+ const r = rig(on)
166
+ await $.session.start(START)
167
+ r.now += 11_000
168
+ await turn($)
169
+ expect(wtRuns(r).length).toBe(2)
170
+ })
171
+
172
+ test('a broken lookup keeps the last state once the state was found elsewhere; a clean miss clears it', async ($, on) => {
173
+ resetLocate()
174
+ const r = rig(on)
175
+ r.files['/repo-t270'] = stateText(SESSION)
176
+ await $.session.start(START)
177
+ expect(last(r)).toContain('#270')
178
+ r.worktrees = new Error('timeout')
179
+ r.files['/repo-t270'] = 'x'
180
+ await turn($)
181
+ expect(r.statuses.filter(s => s === undefined)).toEqual([])
182
+ expect(last(r)).toContain('#270')
183
+
184
+ r.worktrees = porcelain('/repo', '/repo-t270')
185
+ r.files = {}
186
+ r.now += 4_000
187
+ await turn($)
188
+ expect(last(r)).toBeUndefined()
189
+ })
190
+
191
+ // The state found in the worktree, then the lookup breaks for good (the file there no longer names this session).
192
+ const breakLookup = async ($: any, r: ReturnType<typeof rig>) => {
193
+ r.files['/repo-t270'] = stateText(SESSION)
194
+ await $.session.start(START)
195
+ r.files['/repo-t270'] = stateText('sess-other')
196
+ r.worktrees = new Error('git exploded')
197
+ }
198
+
199
+ test('a persistent lookup failure scans at most once per window', async ($, on) => {
200
+ resetLocate()
201
+ const r = rig(on)
202
+ await breakLookup($, r)
203
+ const before = wtRuns(r).length
204
+ await turn($)
205
+ await turn($)
206
+ r.now += 2_000
207
+ await turn($)
208
+ expect(wtRuns(r).length).toBe(before + 1)
209
+ r.now += 2_000
210
+ await turn($)
211
+ expect(wtRuns(r).length).toBe(before + 2)
212
+ })
213
+
214
+ test('after three failed lookups in a row the band clears', async ($, on) => {
215
+ resetLocate()
216
+ const r = rig(on)
217
+ await breakLookup($, r)
218
+ for (const kept of [true, true, false]) {
219
+ r.now += 4_000
220
+ await turn($)
221
+ expect(last(r) !== undefined).toBe(kept)
222
+ }
223
+ })
224
+
225
+ test('a successful lookup resets the failure count', async ($, on) => {
226
+ resetLocate()
227
+ const r = rig(on)
228
+ await breakLookup($, r)
229
+ for (let i = 0; i < 2; i += 1) {
230
+ r.now += 4_000
231
+ await turn($)
232
+ }
233
+ r.worktrees = porcelain('/repo', '/repo-t270')
234
+ r.files['/repo-t270'] = stateText(SESSION)
235
+ r.now += 4_000
236
+ await turn($)
237
+ expect(last(r)).toContain('#270')
238
+ r.files['/repo-t270'] = stateText('sess-other')
239
+ r.worktrees = new Error('git exploded')
240
+ for (const kept of [true, true, false]) {
241
+ r.now += 4_000
242
+ await turn($)
243
+ expect(last(r) !== undefined).toBe(kept)
244
+ }
245
+ })
246
+
247
+ test('a hit on the own checkout ends the run of failures', async ($, on) => {
248
+ resetLocate()
249
+ const r = rig(on)
250
+ await breakLookup($, r)
251
+ for (let i = 0; i < 2; i += 1) {
252
+ r.now += 4_000
253
+ await turn($)
254
+ }
255
+ r.files['/repo-t270'] = stateText(SESSION)
256
+ r.cwd = '/repo-t270'
257
+ await turn($)
258
+ r.cwd = '/repo'
259
+ r.files['/repo-t270'] = stateText('sess-other')
260
+ for (const kept of [true, true, false]) {
261
+ r.now += 4_000
262
+ await turn($)
263
+ expect(last(r) !== undefined).toBe(kept)
264
+ }
265
+ })
266
+
267
+ test('three failures spaced well apart still clear the band', async ($, on) => {
268
+ resetLocate()
269
+ const r = rig(on)
270
+ await breakLookup($, r)
271
+ for (const kept of [true, true, false]) {
272
+ r.now += 20_000
273
+ await turn($)
274
+ expect(last(r) !== undefined).toBe(kept)
275
+ }
276
+ })
277
+
278
+ test('another session\'s state in the own checkout does not hide this session\'s state in a worktree', async ($, on) => {
279
+ resetLocate()
280
+ const r = rig(on)
281
+ r.files['/repo'] = stateText('sess-other')
282
+ r.files['/repo-t270'] = stateText(SESSION, { ticket: 999 })
283
+ await $.session.start(START)
284
+ expect(last(r)).toContain('#999')
285
+ expect(last(r)).not.toContain('orphaned')
286
+ await turn($)
287
+ expect(last(r)).toContain('#999')
288
+ })
289
+
290
+ test('another session\'s state in the own checkout and nothing elsewhere stays orphaned', async ($, on) => {
291
+ resetLocate()
292
+ const r = rig(on)
293
+ r.files['/repo'] = stateText('sess-other')
294
+ await $.session.start(START)
295
+ expect(last(r)).toContain('#270')
296
+ r.worktrees = new Error('git exploded')
297
+ r.now += 20_000
298
+ await turn($)
299
+ expect(last(r)).toContain('#270')
300
+ })
301
+
302
+ test('a state with no session in the own checkout runs no lookup', async ($, on) => {
303
+ resetLocate()
304
+ const r = rig(on)
305
+ r.files['/repo'] = stateText(null)
306
+ r.files['/repo-t270'] = stateText(SESSION, { ticket: 999 })
307
+ await $.session.start(START)
308
+ expect(last(r)).toContain('#270')
309
+ expect(wtRuns(r)).toEqual([])
310
+ })
@@ -12,10 +12,20 @@ export type WorkState = {
12
12
 
13
13
  // The latched system-prompt section: the key it was built for and its text (null: no section).
14
14
  export type PromptSnapshot = { key: string; text: string | null }
15
+ // The repo the session works in: its checkout root, remote host and owner/name. Pinned so a shell cd never loses it.
16
+ export type RepoPin = { root: string; host: string; slug: string }
17
+
18
+ // The autonomous run seen from the main checkout: the most slots done, and the slots still in flight.
19
+ export type AutoRun = { done: number; inFlight: number }
20
+ // The scope's open tickets waiting on the person and paused ones (scope counts, not run-only counts).
21
+ export type RunCounts = { needsYou: number; paused: number }
15
22
 
16
23
  // The map pane (#208): what the last `gh` read of the active map gave.
17
24
  export type TicketType = 'decision' | 'research' | 'prototype' | 'build' | 'bug' | 'task'
18
- export type TicketState = 'frontier' | 'in-progress' | 'blocked'
25
+ export type TicketState = 'frontier' | 'in-progress' | 'paused' | 'blocked'
26
+
27
+ // The parent a pending UAT ticket tests (from its `UAT of:` body line); title is empty for a bare `#<n>`.
28
+ export type UatParent = { number: number; title: string }
19
29
 
20
30
  export type PaneTicket = {
21
31
  number: number
@@ -24,10 +34,25 @@ export type PaneTicket = {
24
34
  state: TicketState
25
35
  assignees: readonly string[]
26
36
  url: string
37
+ // Carries `trailhead:uat`: a marker beside `state`, only the grouping moves it (paused still wins).
38
+ isUat: boolean
39
+ // Carries `trailhead:needs-you`: a marker beside `state`, it wins the grouping over every other group.
40
+ isNeedsYou: boolean
41
+ // The parent it tests, once read; null when unknown (no body read, or no usable `UAT of:` line).
42
+ uatOf: UatParent | null
27
43
  }
28
44
 
29
- // One open map of the repo, as the maps list shows it.
30
- export type MapSummary = { number: number; title: string }
45
+ // A map's sub-issue counts: `open` still to do, `total` all of them.
46
+ export type MapCounts = { open: number; total: number }
47
+ // One open map of the repo, as the maps list shows it; `counts` null = unknown, drawn name-only.
48
+ export type MapSummary = { number: number; title: string; counts: MapCounts | null }
49
+
50
+ // What the maps read gives: the open maps and the whiteboard's counts (null when they were unreadable).
51
+ // `isMapsCapped`: more open maps exist than the read returned, so a sum over them is a lower bound.
52
+ export type MapsRead = { maps: readonly MapSummary[]; whiteboard: MapCounts | null; isMapsCapped: boolean }
53
+
54
+ // The open tickets across every open map plus the whiteboard, with the map numbers it was computed over.
55
+ export type OpenTotal = { total: number; maps: readonly number[]; isMapsCapped: boolean }
31
56
 
32
57
  export type MapInfo = { number: number; title: string; destination: string | null; url: string }
33
58
 
@@ -45,6 +70,10 @@ export type MapPaneData = {
45
70
  isCapped: boolean
46
71
  // The repo's open maps (empty until a first good read, then the last good one).
47
72
  maps: readonly MapSummary[]
73
+ // The whiteboard's open and total tickets; null = unknown, the Whiteboard row is drawn name-only.
74
+ whiteboard: MapCounts | null
75
+ // The open tickets across every open map plus the whiteboard; null = unknown, the line is hidden.
76
+ openTotal: OpenTotal | null
48
77
  history: BurndownHistory | null
49
78
  historyAt: number | null
50
79
  fetchedAt: number | null
@@ -52,18 +81,48 @@ export type MapPaneData = {
52
81
  isLoading: boolean
53
82
  }
54
83
 
84
+ // The pinned per-repo index issue (`trailhead:dashboard`), as the pane links it.
85
+ export type DashboardRef = { number: number; url: string }
86
+
87
+ // The pane's dashboard view: the whiteboard tickets (map-less) and the pinned index; the open maps
88
+ // stay in `MapPaneData.maps`. Same failure rule: the last good read is kept, `error` says why.
89
+ export type DashboardPaneData = {
90
+ dashboard: DashboardRef | null
91
+ tickets: readonly PaneTicket[]
92
+ unverifiedCount: number
93
+ isCapped: boolean
94
+ fetchedAt: number | null
95
+ error: string | null
96
+ isLoading: boolean
97
+ }
98
+
55
99
  declare module 'claude-code' {
56
100
  interface PluginState {
57
101
  trailhead: {
58
102
  workState: WorkState | null
59
103
  isOrphaned: boolean
104
+ // The foreign state's session is not known gone: neutral wording, no resume hint.
105
+ isElsewhere: boolean
106
+ // The ticket this session works, kept when another session's state takes over the shared file.
107
+ ownTicket: number | null
60
108
  promptSnapshot: PromptSnapshot | null
61
109
  mapPane: MapPaneData
62
110
  activeMap: number | null
63
111
  // The map the pane shows when it is not the active one; null follows the active map.
64
112
  viewedMap: number | null
113
+ // The pane shows the dashboard (maps index + whiteboard) instead of a map.
114
+ isDashboard: boolean
115
+ dashboardPane: DashboardPaneData
65
116
  pendingWork: number | null
117
+ // Whether the pending press waits for the turn to end or has been sent.
118
+ pendingPhase: 'queued' | 'starting' | null
66
119
  paneAutoOpened: boolean
120
+ // The repo the pane and band read from; null until a GitHub working copy was seen.
121
+ repoPin: RepoPin | null
122
+ // The autonomous run found beside the main checkout (null: none), its scope counts, and the Esc/compaction flag.
123
+ autoRun: AutoRun | null
124
+ runCounts: RunCounts | null
125
+ autoInterrupted: boolean
67
126
  }
68
127
  }
69
128
  }