@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.
- package/README.it.md +158 -34
- package/README.md +147 -33
- package/bin/auto-intro-docs.test.js +72 -0
- package/bin/auto-merge-docs.test.js +33 -0
- package/bin/auto-merge-ticket-docs.test.js +32 -0
- package/bin/auto-review-rounds-docs.test.js +45 -0
- package/bin/auto-run-docs.test.js +60 -0
- package/bin/auto-scope-docs.test.js +33 -0
- package/bin/auto-slots-docs.test.js +45 -0
- package/bin/auto-uat-docs.test.js +47 -0
- package/bin/base-preflight-docs.test.js +65 -0
- package/bin/execute-split-docs.test.js +146 -0
- package/bin/grilling-holistic-review-docs.test.js +90 -0
- package/bin/isolation-answer-docs.test.js +41 -0
- package/bin/isolation-copy-docs.test.js +28 -0
- package/bin/lib/codex-projection.js +6 -6
- package/bin/lib/codex-projection.test.js +43 -2
- package/bin/needs-you-docs.test.js +41 -0
- package/bin/pick-stress-test-docs.test.js +105 -0
- package/bin/report-status-docs.test.js +195 -0
- package/bin/resolve-helpers-docs.test.js +36 -0
- package/bin/scope-confirm-skip-docs.test.js +70 -0
- package/bin/trailhead.js +6 -6
- package/bin/trailhead.test.js +355 -16
- package/bin/uat-setup-docs.test.js +65 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
- package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
- package/plugins/trailhead/agents/trailhead-debug.md +20 -1
- package/plugins/trailhead/agents/trailhead-executor.md +29 -2
- package/plugins/trailhead/agents/trailhead-fix.md +29 -2
- package/plugins/trailhead/agents/trailhead-plan.md +20 -1
- package/plugins/trailhead/agents/trailhead-research.md +22 -3
- package/plugins/trailhead/agents/trailhead-verify.md +15 -2
- package/plugins/trailhead/commands/auto.md +2 -2
- package/plugins/trailhead/hooks/lib/auto-merge.test.js +467 -0
- package/plugins/trailhead/hooks/lib/auto-queue.test.js +245 -0
- package/plugins/trailhead/hooks/lib/auto-run.test.js +318 -0
- package/plugins/trailhead/hooks/lib/auto-slot.test.js +278 -0
- package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +173 -0
- package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
- package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
- package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
- package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
- package/plugins/trailhead/hooks/lib/dashboard.test.js +472 -0
- package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
- package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
- package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
- package/plugins/trailhead/hooks/lib/merge-ticket.test.js +394 -0
- package/plugins/trailhead/hooks/lib/needs-you.test.js +177 -0
- package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +319 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
- package/plugins/trailhead/hooks/lib/shell-scan.js +36 -1
- package/plugins/trailhead/hooks/lib/shell-scan.test.js +14 -0
- package/plugins/trailhead/hooks/lib/statusline-context.test.js +140 -0
- package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
- package/plugins/trailhead/hooks/lib/uat-followup.test.js +169 -0
- package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
- package/plugins/trailhead/hooks/lib/unblock.test.js +207 -0
- package/plugins/trailhead/hooks/lib/work-state.test.js +45 -0
- package/plugins/trailhead/hooks/mod/README.md +209 -36
- package/plugins/trailhead/hooks/mod/auto-run-view.ts +93 -0
- package/plugins/trailhead/hooks/mod/auto-run.ts +165 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +304 -26
- package/plugins/trailhead/hooks/mod/map-pane.tsx +763 -160
- package/plugins/trailhead/hooks/mod/register.tsx +312 -40
- package/plugins/trailhead/hooks/mod/repo-pin.ts +167 -0
- package/plugins/trailhead/hooks/mod/session-registry.ts +65 -0
- package/plugins/trailhead/hooks/mod/turn-state.ts +14 -0
- package/plugins/trailhead/hooks/mod/whiteboard-follow.ts +98 -0
- package/plugins/trailhead/hooks/mod/work-state-locate.ts +191 -0
- package/plugins/trailhead/hooks/mod/work-state-prompt.ts +13 -15
- package/plugins/trailhead/hooks/mod/work-state-view.ts +38 -5
- package/plugins/trailhead/hooks/mod/worktree-porcelain.ts +17 -0
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +22 -16
- package/plugins/trailhead/hooks/trailhead-install-guard.js +169 -34
- package/plugins/trailhead/hooks/trailhead-install-guard.test.js +180 -1
- package/plugins/trailhead/hooks/trailhead-labels-guard.js +231 -0
- package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +212 -0
- package/plugins/trailhead/hooks/trailhead-search-guard.js +43 -7
- package/plugins/trailhead/hooks/trailhead-search-guard.test.js +66 -2
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +296 -22
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +253 -0
- package/plugins/trailhead/skills/_shared/choices.md +11 -4
- package/plugins/trailhead/skills/_shared/configuration-reference.md +17 -5
- package/plugins/trailhead/skills/_shared/load-first.md +3 -3
- package/plugins/trailhead/skills/_shared/multi-map.md +2 -2
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +47 -14
- package/plugins/trailhead/skills/_shared/substrate.md +9 -4
- package/plugins/trailhead/skills/_shared/teamwork.md +17 -8
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +18 -10
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +3 -1
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques.md +12 -2
- package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +3 -3
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +9 -6
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +31 -18
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +70 -22
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +29 -13
- package/plugins/trailhead/templates/statusline-trailhead.sh +77 -12
- package/plugins/trailhead/templates/trailhead-auto-merge.js +318 -0
- package/plugins/trailhead/templates/trailhead-auto-queue.js +186 -0
- package/plugins/trailhead/templates/trailhead-auto-run.js +294 -0
- package/plugins/trailhead/templates/trailhead-auto-slot.js +239 -0
- package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +88 -0
- package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
- package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
- package/plugins/trailhead/templates/trailhead-dashboard.js +360 -0
- package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
- package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
- package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
- package/plugins/trailhead/templates/trailhead-merge-ticket.js +366 -0
- package/plugins/trailhead/templates/trailhead-needs-you.js +193 -0
- package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +289 -0
- package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
- package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
- package/plugins/trailhead/templates/trailhead-uat-followup.js +128 -0
- package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
- package/plugins/trailhead/templates/trailhead-unblock.js +111 -0
- package/plugins/trailhead/templates/trailhead-work-state.js +25 -7
- package/plugins/trailhead/tests/auto-run-ui.test.ts +444 -0
- package/plugins/trailhead/tests/auto-run-view.test.ts +111 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +2519 -382
- package/plugins/trailhead/tests/map-pane-view.test.ts +600 -18
- package/plugins/trailhead/tests/map-pane-write.test.ts +51 -0
- package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +77 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +123 -0
- package/plugins/trailhead/tests/session-registry.test.ts +50 -0
- package/plugins/trailhead/tests/whiteboard-follow.test.ts +90 -0
- package/plugins/trailhead/tests/work-state-locate.test.ts +33 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +276 -2
- package/plugins/trailhead/tests/work-state-prompt.test.ts +29 -35
- package/plugins/trailhead/tests/work-state-ui.test.ts +73 -1
- package/plugins/trailhead/tests/work-state-view.test.ts +44 -1
- package/plugins/trailhead/tests/work-state-worktree.test.ts +310 -0
- 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
|
-
//
|
|
30
|
-
export type
|
|
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
|
}
|